feat : update
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Cropper, CropperCropArea, CropperDescription, CropperImage } from "@/components/ui/cropper";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
type CropArea = { x: number; y: number; width: number; height: number };
|
||||
|
||||
export function ProfileImageInput({ id, disabled, onChange, onError }: {
|
||||
id: string;
|
||||
disabled?: boolean;
|
||||
onChange: (file: File) => void;
|
||||
onError: (message: string) => void;
|
||||
}) {
|
||||
const [pending, setPending] = useState<{ url: string } | null>(null);
|
||||
const [cropArea, setCropArea] = useState<CropArea | null>(null);
|
||||
const [cropping, setCropping] = useState(false);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (pending) URL.revokeObjectURL(pending.url);
|
||||
}, [pending]);
|
||||
|
||||
function select(file: File | null) {
|
||||
if (!file) return;
|
||||
if (file.size > 6 * 1024 * 1024 || !["image/png", "image/jpeg", "image/webp"].includes(file.type)) {
|
||||
onError("ใช้รูป PNG, JPEG หรือ WebP ขนาดไม่เกิน 6 MB");
|
||||
return;
|
||||
}
|
||||
onError("");
|
||||
setCropArea(null);
|
||||
setPending({ url: URL.createObjectURL(file) });
|
||||
}
|
||||
|
||||
async function applyCrop() {
|
||||
if (!pending || !cropArea || cropping) return;
|
||||
setCropping(true);
|
||||
try {
|
||||
const image = await new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const candidate = new Image();
|
||||
candidate.onload = () => resolve(candidate);
|
||||
candidate.onerror = reject;
|
||||
candidate.src = pending.url;
|
||||
});
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 256;
|
||||
canvas.height = 256;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) throw new Error("Canvas unavailable");
|
||||
context.imageSmoothingQuality = "high";
|
||||
context.drawImage(image, cropArea.x, cropArea.y, cropArea.width, cropArea.height, 0, 0, 256, 256);
|
||||
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, "image/png"));
|
||||
if (!blob) throw new Error("Image encoding failed");
|
||||
onChange(new File([blob], "profile.png", { type: "image/png" }));
|
||||
setPending(null);
|
||||
} catch {
|
||||
onError("ไม่สามารถครอปไฟล์ภาพนี้ได้");
|
||||
} finally {
|
||||
setCropping(false);
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<Input id={id} type="file" accept="image/png,image/jpeg,image/webp" disabled={disabled}
|
||||
onChange={(event) => {
|
||||
select(event.target.files?.[0] ?? null);
|
||||
event.target.value = "";
|
||||
}} />
|
||||
<Dialog open={!!pending} onOpenChange={(open) => { if (!open && !cropping) setPending(null); }}>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>ครอปรูปโปรไฟล์ 1:1</DialogTitle>
|
||||
<DialogDescription>ลากภาพเพื่อเลือกส่วนที่ต้องการใช้เป็นรูปโปรไฟล์</DialogDescription>
|
||||
</DialogHeader>
|
||||
{pending && <Cropper key={pending.url} image={pending.url} aspectRatio={1}
|
||||
onCropChange={setCropArea} className="h-80 rounded-lg bg-black sm:h-96">
|
||||
<CropperDescription>ลากภาพหรือใช้ปุ่มลูกศรเพื่อเลือกพื้นที่ครอปสี่เหลี่ยม</CropperDescription>
|
||||
<CropperImage />
|
||||
<CropperCropArea />
|
||||
</Cropper>}
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" disabled={cropping} onClick={() => setPending(null)}>ยกเลิก</Button>
|
||||
<Button type="button" disabled={!cropArea || cropping} onClick={() => void applyCrop()}>
|
||||
{cropping ? "กำลังครอป..." : "ใช้ภาพนี้"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>;
|
||||
}
|
||||
Reference in New Issue
Block a user