feat : able to rotate img for mat card
CI / Verify (push) Successful in 1m46s
CI / Build immutable images and deploy (push) Successful in 2m3s

This commit is contained in:
2026-09-19 20:20:02 +07:00 Unverified
parent 26517ea5fd
commit 7c10b2c995
12 changed files with 2962 additions and 41 deletions
+81 -27
View File
@@ -6,6 +6,13 @@ import { DownloadIcon } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { ExportLanguageDialog } from "@/components/public/export-language-dialog";
import {
ExportLanguageToggle,
@@ -57,6 +64,7 @@ export function GuideImageExport({
showLanguageSelector = true,
promptForLanguage = false,
localizeFileName = true,
modeSelection = "dialog",
}: {
children: ReactNode;
fileName: string;
@@ -72,6 +80,7 @@ export function GuideImageExport({
showLanguageSelector?: boolean;
promptForLanguage?: boolean;
localizeFileName?: boolean;
modeSelection?: "dialog" | "inline";
}) {
const captureRef = useRef<HTMLDivElement>(null);
const [captureMode, setCaptureMode] = useState<
@@ -83,6 +92,13 @@ export function GuideImageExport({
const [pendingCaptureMode, setPendingCaptureMode] = useState<
"with-notes" | "without-notes" | null
>(null);
const [modeDialogOpen, setModeDialogOpen] = useState(false);
function requestCapture(mode: "with-notes" | "without-notes") {
setModeDialogOpen(false);
if (promptForLanguage) setPendingCaptureMode(mode);
else setCaptureMode(mode);
}
function selectLanguage(nextLanguage: ExportLanguage) {
if (!pendingCaptureMode) return;
@@ -91,6 +107,37 @@ export function GuideImageExport({
setPendingCaptureMode(null);
}
function exportButton(
mode: "with-notes" | "without-notes" | null,
label: string,
) {
const loading = capturing && (mode === null || captureMode === mode);
return (
<Button
key={mode ?? "export"}
type="button"
variant="outline"
size="sm"
className={mobileIconOnly ? "max-sm:size-7 max-sm:px-0 max-sm:has-data-[icon=inline-start]:pl-0" : undefined}
disabled={capturing || disabled}
onClick={() => {
if (mode) requestCapture(mode);
else if (allowWithoutNotes) setModeDialogOpen(true);
else requestCapture("with-notes");
}}
>
{loading ? (
<Spinner data-icon="inline-start" />
) : (
<DownloadIcon data-icon="inline-start" />
)}
<span className={mobileIconOnly ? "sr-only sm:not-sr-only" : undefined}>
{loading ? "กำลังส่งออก…" : label}
</span>
</Button>
);
}
useEffect(() => {
if (!capturing) return;
@@ -161,34 +208,41 @@ export function GuideImageExport({
<ExportLanguageToggle language={language} onChange={setStoredLanguage} />
) : null}
<div className="flex flex-wrap gap-2">
{(["with-notes", "without-notes"] as const).filter((mode) => allowWithoutNotes || mode === "with-notes").map((mode) => (
<Button
key={mode}
type="button"
variant="outline"
size="sm"
className={mobileIconOnly ? "max-sm:size-7 max-sm:px-0 max-sm:has-data-[icon=inline-start]:pl-0" : undefined}
disabled={capturing || disabled}
onClick={() => {
if (promptForLanguage) setPendingCaptureMode(mode);
else setCaptureMode(mode);
}}
>
{captureMode === mode ? (
<Spinner data-icon="inline-start" />
) : (
<DownloadIcon data-icon="inline-start" />
)}
<span className={mobileIconOnly ? "sr-only sm:not-sr-only" : undefined}>
{captureMode === mode
? "กำลังส่งออก…"
: mode === "without-notes"
? "ส่งออกภาพโดยไม่รวมโน้ต"
: buttonLabel}
</span>
</Button>
))}
{allowWithoutNotes && modeSelection === "inline"
? [
exportButton("with-notes", buttonLabel),
exportButton("without-notes", "ส่งออกภาพโดยไม่รวมโน้ต"),
]
: exportButton(null, buttonLabel)}
</div>
{allowWithoutNotes && modeSelection === "dialog" ? (
<Dialog open={modeDialogOpen} onOpenChange={setModeDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>เลือกรูปแบบการส่งออก</DialogTitle>
<DialogDescription>เลือกว่าจะรวมโน้ตไว้ในภาพหรือไม่</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2 sm:flex-row">
<Button
type="button"
variant="secondary"
className="flex-1"
onClick={() => requestCapture("with-notes")}
>
ส่งออกพร้อมโน้ต
</Button>
<Button
type="button"
variant="secondary"
className="flex-1"
onClick={() => requestCapture("without-notes")}
>
ส่งออกโดยไม่รวมโน้ต
</Button>
</div>
</DialogContent>
</Dialog>
) : null}
<ExportLanguageDialog
open={pendingCaptureMode !== null}
onOpenChange={(open) => {