92 lines
4.0 KiB
TypeScript
92 lines
4.0 KiB
TypeScript
"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>
|
|
</>;
|
|
}
|