223 lines
8.2 KiB
TypeScript
223 lines
8.2 KiB
TypeScript
import Button from "../layout/Button";
|
|
import Stack from "../layout/Stack";
|
|
import { ImagePlus, X } from "lucide-react";
|
|
import React, { DetailedHTMLProps } from "react";
|
|
import Card from "../elements/Card";
|
|
import Span from "../layout/Span";
|
|
import Center from "../layout/Center";
|
|
import imageInputToBase64, {
|
|
ImageInputToBase64FunctionReturn,
|
|
} from "../utils/form/imageInputToBase64";
|
|
import { twMerge } from "tailwind-merge";
|
|
import Tag from "../elements/Tag";
|
|
|
|
type ImageUploadProps = DetailedHTMLProps<
|
|
React.HTMLAttributes<HTMLDivElement>,
|
|
HTMLDivElement
|
|
> & {
|
|
onChangeHandler?: (
|
|
imgData: ImageInputToBase64FunctionReturn | undefined
|
|
) => any;
|
|
fileInputProps?: DetailedHTMLProps<
|
|
React.InputHTMLAttributes<HTMLInputElement>,
|
|
HTMLInputElement
|
|
>;
|
|
placeHolderWrapper?: DetailedHTMLProps<
|
|
React.HTMLAttributes<HTMLDivElement>,
|
|
HTMLDivElement
|
|
>;
|
|
previewImageWrapperProps?: DetailedHTMLProps<
|
|
React.HTMLAttributes<HTMLDivElement>,
|
|
HTMLDivElement
|
|
>;
|
|
previewImageProps?: DetailedHTMLProps<
|
|
React.ImgHTMLAttributes<HTMLImageElement>,
|
|
HTMLImageElement
|
|
>;
|
|
label?: string;
|
|
disablePreview?: boolean;
|
|
multiple?: boolean;
|
|
existingImageUrl?: string;
|
|
externalSetImage?: React.Dispatch<
|
|
React.SetStateAction<ImageInputToBase64FunctionReturn | undefined>
|
|
>;
|
|
externalSetImages?: React.Dispatch<
|
|
React.SetStateAction<ImageInputToBase64FunctionReturn[] | undefined>
|
|
>;
|
|
setLoading?: React.Dispatch<React.SetStateAction<boolean>>;
|
|
externalImage?: ImageInputToBase64FunctionReturn;
|
|
restoreImageFn?: () => void;
|
|
};
|
|
|
|
/**
|
|
* @note use the `onChangeHandler` prop to grab the parsed base64 image object
|
|
*/
|
|
export default function ImageUpload({
|
|
onChangeHandler,
|
|
fileInputProps,
|
|
placeHolderWrapper,
|
|
previewImageWrapperProps,
|
|
previewImageProps,
|
|
label,
|
|
disablePreview,
|
|
existingImageUrl,
|
|
externalSetImage,
|
|
externalSetImages,
|
|
externalImage,
|
|
multiple,
|
|
restoreImageFn,
|
|
setLoading,
|
|
...props
|
|
}: ImageUploadProps) {
|
|
const [imageObject, setImageObject] = React.useState<
|
|
ImageInputToBase64FunctionReturn | undefined
|
|
>(externalImage);
|
|
const [src, setSrc] = React.useState<string | undefined>(existingImageUrl);
|
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (existingImageUrl) setSrc(existingImageUrl);
|
|
}, [existingImageUrl]);
|
|
|
|
return (
|
|
<Stack
|
|
{...props}
|
|
className={twMerge(
|
|
"w-full h-[300px] overflow-hidden",
|
|
props?.className
|
|
)}
|
|
>
|
|
<input
|
|
type="file"
|
|
className={twMerge("hidden", fileInputProps?.className)}
|
|
multiple={multiple}
|
|
accept="image/*"
|
|
{...fileInputProps}
|
|
onChange={(e) => {
|
|
setLoading?.(true);
|
|
|
|
if (multiple) {
|
|
(async () => {
|
|
const files = e.target.files;
|
|
if (!files?.[0]) return;
|
|
|
|
let imgArr: ImageInputToBase64FunctionReturn[] = [];
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
const fileObj = await imageInputToBase64({
|
|
file,
|
|
});
|
|
imgArr.push(fileObj);
|
|
}
|
|
|
|
externalSetImages?.(imgArr);
|
|
setLoading?.(false);
|
|
})();
|
|
} else {
|
|
imageInputToBase64({ imageInput: e.target }).then(
|
|
(res) => {
|
|
setSrc(res.imageBase64Full);
|
|
onChangeHandler?.(res);
|
|
setImageObject?.(res);
|
|
externalSetImage?.(res);
|
|
fileInputProps?.onChange?.(e);
|
|
setLoading?.(false);
|
|
}
|
|
);
|
|
}
|
|
}}
|
|
ref={inputRef as any}
|
|
/>
|
|
|
|
{src || imageObject?.imageBase64Full ? (
|
|
<Card
|
|
className="w-full relative h-full items-center justify-center"
|
|
{...previewImageWrapperProps}
|
|
>
|
|
{label && (
|
|
<label
|
|
className={twMerge(
|
|
"absolute top-0 left-0 text-xs z-50"
|
|
)}
|
|
>
|
|
<Tag color="gray">
|
|
<span className="opacity-70">{label}</span>
|
|
</Tag>
|
|
</label>
|
|
)}
|
|
{disablePreview ? (
|
|
<Span className="opacity-50" size="small">
|
|
Image Uploaded!
|
|
</Span>
|
|
) : (
|
|
<img
|
|
src={imageObject?.imageBase64Full || src}
|
|
className="w-full h-full object-contain"
|
|
{...previewImageProps}
|
|
/>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
className={twMerge(
|
|
"absolute p-1 top-2 right-2 z-20 bg-background-light dark:bg-background-dark"
|
|
)}
|
|
onClick={(e) => {
|
|
setSrc(undefined);
|
|
onChangeHandler?.(undefined);
|
|
setImageObject?.(undefined);
|
|
externalSetImage?.(undefined);
|
|
if (inputRef.current) {
|
|
inputRef.current.value == "";
|
|
}
|
|
}}
|
|
title="Cancel Image Upload Button"
|
|
>
|
|
<X className="text-slate-950 dark:text-white" />
|
|
</Button>
|
|
</Card>
|
|
) : (
|
|
<Card
|
|
className={twMerge(
|
|
"w-full h-full cursor-pointer hover:bg-slate-100 dark:hover:bg-white/20",
|
|
placeHolderWrapper?.className
|
|
)}
|
|
onClick={(e) => {
|
|
const targetEl = e.target as HTMLElement | undefined;
|
|
if (targetEl?.closest(".cancel-upload")) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
inputRef.current?.click();
|
|
placeHolderWrapper?.onClick?.(e);
|
|
}}
|
|
{...placeHolderWrapper}
|
|
>
|
|
<Center>
|
|
<Stack className="items-center gap-2">
|
|
<ImagePlus className="text-slate-400" />
|
|
<Span size="smaller" variant="faded">
|
|
{label || "Click to Upload Image"}
|
|
</Span>
|
|
{existingImageUrl && (
|
|
<Button
|
|
title="Restore Image Button"
|
|
size="smaller"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
restoreImageFn?.() ||
|
|
setSrc(existingImageUrl);
|
|
}}
|
|
className="cancel-upload"
|
|
>
|
|
Restore Original Image
|
|
</Button>
|
|
)}
|
|
</Stack>
|
|
</Center>
|
|
</Card>
|
|
)}
|
|
</Stack>
|
|
);
|
|
}
|