114 lines
3.0 KiB
TypeScript
114 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { DownloadIcon } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const EXPORT_WIDTH = 1200;
|
|
|
|
function downloadFileName(value: string): string {
|
|
return value.replace(/[\\/:*?"<>|]/gu, "-");
|
|
}
|
|
|
|
export function GuideImageExport({
|
|
children,
|
|
fileName,
|
|
width = EXPORT_WIDTH,
|
|
height,
|
|
captureClassName,
|
|
}: {
|
|
children: ReactNode;
|
|
fileName: string;
|
|
width?: number;
|
|
height?: number;
|
|
captureClassName?: string;
|
|
}) {
|
|
const captureRef = useRef<HTMLDivElement>(null);
|
|
const [capturing, setCapturing] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!capturing) return;
|
|
|
|
let cancelled = false;
|
|
|
|
async function capture() {
|
|
try {
|
|
const node = captureRef.current;
|
|
if (!node) throw new Error("Capture target is unavailable");
|
|
|
|
const { domToBlob, waitUntilLoad } = await import("modern-screenshot");
|
|
await Promise.all([document.fonts.ready, waitUntilLoad(node)]);
|
|
const blob = await domToBlob(node, {
|
|
backgroundColor: getComputedStyle(node).backgroundColor,
|
|
scale: 1,
|
|
width,
|
|
height,
|
|
});
|
|
if (cancelled) return;
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.download = downloadFileName(fileName);
|
|
link.href = url;
|
|
document.body.append(link);
|
|
link.click();
|
|
link.remove();
|
|
window.setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
} catch {
|
|
if (!cancelled) toast.error("ส่งออกภาพไม่สำเร็จ กรุณาลองอีกครั้ง");
|
|
} finally {
|
|
if (!cancelled) setCapturing(false);
|
|
}
|
|
}
|
|
|
|
void capture();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [capturing, fileName, height, width]);
|
|
|
|
return (
|
|
<>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={capturing}
|
|
onClick={() => setCapturing(true)}
|
|
>
|
|
{capturing ? (
|
|
<Spinner data-icon="inline-start" />
|
|
) : (
|
|
<DownloadIcon data-icon="inline-start" />
|
|
)}
|
|
{capturing ? "กำลังส่งออก…" : "ส่งออกภาพ"}
|
|
</Button>
|
|
{capturing
|
|
? createPortal(
|
|
<div
|
|
aria-hidden="true"
|
|
className="pointer-events-none fixed top-0 -left-[10000px]"
|
|
>
|
|
<div
|
|
ref={captureRef}
|
|
className={cn(
|
|
"bg-background p-10 text-foreground",
|
|
captureClassName,
|
|
)}
|
|
style={{ width, height }}
|
|
>
|
|
{children}
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
)
|
|
: null}
|
|
</>
|
|
);
|
|
}
|