AUDIT / src /components /DownloadWorkspaceButton.tsx
Arypulka98's picture
feat(audit): deploy full backend cluster node
aea470f verified
Raw
History Blame Contribute Delete
3.81 kB
/**
* DownloadWorkspaceButton — FIX-6
* Bottone compatto stile Replit per scaricare il VFS come ZIP.
* Da montare nella StatusBar (slot destra).
*/
import { useState, useEffect, useCallback } from "react";
import { vfsAsync, onVfsChanged } from "@/lib/vfsDb";
import { downloadBlob } from "@/lib/fileConverter";
export function DownloadWorkspaceButton() {
const [fileCount, setFileCount] = useState(0);
const [state, setState] = useState<"idle" | "loading" | "done">("idle");
const reload = useCallback(() => {
vfsAsync.list().then(f => setFileCount(f.length)).catch(() => {});
}, []);
useEffect(() => {
reload();
return onVfsChanged(reload);
}, [reload]);
useEffect(() => {
if (state !== "done") return;
const t = setTimeout(() => setState("idle"), 2000);
return () => clearTimeout(t);
}, [state]);
if (fileCount === 0) return null;
const handleClick = async () => {
if (state !== "idle") return;
setState("loading");
try {
const { default: JSZip } = await import("jszip");
const zip = new JSZip();
const files = await vfsAsync.list();
for (const f of files) {
const c = f.opfs ? (await vfsAsync.read(f.path) ?? "") : f.content;
zip.file(f.path.startsWith("/") ? f.path.slice(1) : f.path, c);
}
const blob = await zip.generateAsync({ type: "blob", compression: "DEFLATE", compressionOptions: { level: 6 } });
downloadBlob(blob, `workspace-${Date.now()}.zip`);
setState("done");
} catch {
setState("idle");
}
};
const isDone = state === "done";
const isLoading = state === "loading";
return (
<button
onClick={handleClick}
disabled={isLoading}
title={`Scarica workspace (${fileCount} file) come ZIP`}
style={{
all: "unset",
display: "flex",
alignItems: "center",
gap: 4,
height: "100%",
padding: "0 10px",
borderLeft: "1px solid rgba(255,255,255,0.04)",
fontSize: "0.62rem",
fontFamily: '"JetBrains Mono", ui-monospace, monospace',
fontWeight: 600,
letterSpacing: "0.025em",
color: isDone ? "#34d399" : "rgba(148,163,184,0.55)",
cursor: isLoading ? "wait" : "pointer",
transition: "color 0.15s",
whiteSpace: "nowrap",
WebkitTapHighlightColor: "transparent",
} as React.CSSProperties}
onPointerEnter={e => {
if (!isLoading && !isDone)
(e.currentTarget as HTMLButtonElement).style.color = "rgba(148,163,184,0.85)";
}}
onPointerLeave={e => {
if (!isDone)
(e.currentTarget as HTMLButtonElement).style.color = "rgba(148,163,184,0.55)";
}}
>
{isDone ? (
<svg width={9} height={9} viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="20 6 9 17 4 12" />
</svg>
) : isLoading ? (
<svg width={9} height={9} viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
style={{ animation: "sb-spin 0.8s linear infinite" }}>
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
</svg>
) : (
<svg width={9} height={9} viewBox="0 0 24 24" fill="none"
stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
)}
{isDone ? "salvato" : isLoading ? "zip…" : "zip"}
<style>{`@keyframes sb-spin{to{transform:rotate(360deg)}}`}</style>
</button>
);
}
export default DownloadWorkspaceButton;