feat(guides): add structured image-first editor
This commit is contained in:
@@ -11,10 +11,8 @@ import { IMAGE_MIME_TYPES, MAX_MEDIA_BYTES } from "@/lib/media/validation";
|
||||
|
||||
export interface UploadedMedia {
|
||||
id: string;
|
||||
fileName: string;
|
||||
mimeType: string;
|
||||
byteSize: number;
|
||||
url: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export function MediaUploader({
|
||||
@@ -23,7 +21,7 @@ export function MediaUploader({
|
||||
onReady: (media: UploadedMedia) => void;
|
||||
}) {
|
||||
const inputId = useId();
|
||||
const [state, setState] = useState<"idle" | "preparing" | "uploading" | "checking">("idle");
|
||||
const [state, setState] = useState<"idle" | "uploading">("idle");
|
||||
const busy = state !== "idle";
|
||||
|
||||
async function upload(file: File) {
|
||||
@@ -36,41 +34,20 @@ export function MediaUploader({
|
||||
return;
|
||||
}
|
||||
|
||||
let mediaId: string | undefined;
|
||||
try {
|
||||
setState("preparing");
|
||||
const presignResponse = await fetch("/api/admin/media/presign", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ fileName: file.name, mimeType: file.type, byteSize: file.size }),
|
||||
});
|
||||
if (!presignResponse.ok) throw new Error("presign");
|
||||
const upload = await presignResponse.json() as {
|
||||
mediaId: string;
|
||||
uploadUrl: string;
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
mediaId = upload.mediaId;
|
||||
|
||||
setState("uploading");
|
||||
const putResponse = await fetch(upload.uploadUrl, {
|
||||
method: "PUT",
|
||||
headers: upload.headers,
|
||||
body: file,
|
||||
const body = new FormData();
|
||||
body.set("file", file);
|
||||
const response = await fetch("/api/admin/media", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
if (!putResponse.ok) throw new Error("upload");
|
||||
|
||||
setState("checking");
|
||||
const completeResponse = await fetch(`/api/admin/media/${mediaId}/complete`, { method: "POST" });
|
||||
if (!completeResponse.ok) throw new Error("complete");
|
||||
const result = await completeResponse.json() as { media: UploadedMedia };
|
||||
onReady(result.media);
|
||||
const result = await response.json() as UploadedMedia & { error?: string };
|
||||
if (!response.ok) throw new Error(result.error || "upload");
|
||||
onReady(result);
|
||||
toast.success("อัปโหลดรูปภาพแล้ว");
|
||||
} catch {
|
||||
if (mediaId) {
|
||||
await fetch(`/api/admin/media/${mediaId}`, { method: "DELETE" }).catch(() => undefined);
|
||||
}
|
||||
toast.error("อัปโหลดไม่สำเร็จ ไฟล์ชั่วคราวถูกเก็บกวาดแล้ว");
|
||||
} catch (cause) {
|
||||
toast.error(cause instanceof Error ? cause.message : "อัปโหลดไม่สำเร็จ");
|
||||
} finally {
|
||||
setState("idle");
|
||||
}
|
||||
@@ -99,9 +76,7 @@ export function MediaUploader({
|
||||
render={<label htmlFor={inputId} />}
|
||||
>
|
||||
{busy ? <Spinner /> : <ImagePlusIcon />}
|
||||
{state === "preparing" && "กำลังเตรียม…"}
|
||||
{state === "uploading" && "กำลังอัปโหลด…"}
|
||||
{state === "checking" && "กำลังตรวจสอบไฟล์…"}
|
||||
{state === "idle" && "อัปโหลดและเพิ่มรูปภาพ"}
|
||||
{!busy ? <UploadCloudIcon className="ml-auto" /> : null}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user