244 lines
5.6 KiB
TypeScript
244 lines
5.6 KiB
TypeScript
"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;
|