Files
buzz-sheet/components/auth/profile-image-input.tsx
T
2026-10-03 00:56:31 +07:00

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>
</>;
}