feate:update

This commit is contained in:
TuanVT
2026-05-25 17:42:19 +07:00
parent b0baa509d9
commit 2978cf2db0
9 changed files with 416 additions and 49 deletions
@@ -0,0 +1,243 @@
"use client";
import React, { useEffect, useMemo, useState } from "react";
import Image from "next/image";
import clsx from "clsx";
import { UploadCloud, X } from "lucide-react";
import { toastError, toastWarn } from "@/common/funcs/toast";
import { UploadImageProps } from "./interface";
const MAXIMUM_FILE = 10; // MB
const ACCEPT_TYPES = ["image/jpeg", "image/jpg", "image/png"];
/* =========================
COMPONENT
========================= */
const UploadImage = ({
label,
name,
path,
file,
setFile,
resetPath,
isWidthFull = true,
disabled = false,
}: UploadImageProps) => {
const [dragging, setDragging] = useState(false);
/* =========================
PREVIEW IMAGE
========================= */
const imagePreview = useMemo(() => {
if (!file) return "";
return URL.createObjectURL(file);
}, [file]);
/* =========================
CLEANUP OBJECT URL
========================= */
useEffect(() => {
return () => {
if (imagePreview) {
URL.revokeObjectURL(imagePreview);
}
};
}, [imagePreview]);
/* =========================
VALIDATE FILE
========================= */
const validateFile = (selectedFile: File | null | undefined) => {
if (!selectedFile) return;
const { size, type } = selectedFile;
/**
* CHECK SIZE
*/
if (size / 1000000 > MAXIMUM_FILE) {
return toastError({
msg: `Kích thước tối đa của ảnh là ${MAXIMUM_FILE} MB`,
});
}
/**
* CHECK TYPE
*/
if (!ACCEPT_TYPES.includes(type)) {
return toastWarn({
msg: "Định dạng không hợp lệ. Chỉ chấp nhận JPG, JPEG, PNG",
});
}
/**
* SET FILE
*/
setFile(selectedFile);
};
/* =========================
DRAG EVENTS
========================= */
const handleDragEnter = (e: React.DragEvent<HTMLLabelElement>): void => {
e.preventDefault();
if (disabled) return;
setDragging(true);
};
const handleDragLeave = (): void => {
setDragging(false);
};
const handleDrop = (e: React.DragEvent<HTMLLabelElement>): void => {
e.preventDefault();
if (disabled) return;
setDragging(false);
const droppedFile = e.dataTransfer.files?.[0];
validateFile(droppedFile);
};
/* =========================
SELECT FILE
========================= */
const handleSelectImage = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (disabled) return;
const selectedFile = e.target.files?.[0];
validateFile(selectedFile);
};
/* =========================
REMOVE IMAGE
========================= */
const handleRemoveImage = (): void => {
setFile(null);
resetPath?.();
};
/* =========================
IMAGE SOURCE
========================= */
const imageSrc = imagePreview || path || "";
/* =========================
UI
========================= */
return (
<div className="w-full">
{/* LABEL */}
{label && (
<div className="mb-2">
{typeof label === "string" ? (
<p className="text-sm font-medium text-gray-700">{label}</p>
) : (
label
)}
</div>
)}
{/* PREVIEW */}
{imageSrc ? (
<div
className={clsx(
"relative overflow-hidden rounded-2xl border border-dashed border-blue-300 bg-blue-50/20",
"h-[240px]",
isWidthFull ? "w-full" : "w-[600px]",
)}
>
<Image
src={imageSrc}
alt="Preview image"
fill
className="object-cover"
sizes="100vw"
/>
{/* REMOVE BUTTON */}
{!disabled && (
<button
type="button"
onClick={handleRemoveImage}
className="absolute right-3 top-3 z-10 flex h-9 w-9 items-center justify-center rounded-full bg-white shadow-md transition hover:bg-gray-100 active:scale-95"
>
<X size={18} className="text-gray-700" />
</button>
)}
</div>
) : (
<label
htmlFor={`upload-image-${name}`}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
className={clsx(
"flex h-[240px] flex-col items-center justify-center gap-4 rounded-2xl border border-dashed transition",
isWidthFull ? "w-full" : "w-[600px]",
disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer",
dragging
? "border-blue-500 bg-white"
: "border-blue-300 bg-blue-50/20",
)}
>
{/* ICON */}
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-white shadow-sm">
<UploadCloud className="h-8 w-8 text-blue-500" />
</div>
{/* TEXT */}
<div className="space-y-1 text-center">
<p className="text-sm font-medium text-gray-800">
Kéo & thả nh vào đây
</p>
<p className="text-xs text-gray-500">
Hỗ trợ JPG, JPEG, PNG (Tối đa {MAXIMUM_FILE}MB)
</p>
</div>
{/* INPUT */}
<input
hidden
id={`upload-image-${name}`}
type="file"
accept="image/png,image/jpeg,image/jpg"
disabled={disabled}
onChange={handleSelectImage}
onClick={(e) => {
(e.target as HTMLInputElement).value = "";
}}
/>
</label>
)}
</div>
);
};
export default UploadImage;