feate:update
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user