Updates
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
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";
|
||||
import Input from "./Input";
|
||||
import Row from "../layout/Row";
|
||||
|
||||
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>>;
|
||||
setImgURL?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
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,
|
||||
setImgURL,
|
||||
...props
|
||||
}: ImageUploadProps) {
|
||||
const [imageObject, setImageObject] = React.useState<
|
||||
ImageInputToBase64FunctionReturn | undefined
|
||||
>(externalImage);
|
||||
const [src, setSrc] = React.useState<string | undefined>(existingImageUrl);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const imageUrlRef = React.useRef("");
|
||||
|
||||
React.useEffect(() => {
|
||||
setImgURL?.(src);
|
||||
}, [src]);
|
||||
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute p-1 top-2 right-2 z-20 bg-background-light dark:bg-background-dark",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
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" />
|
||||
</div>
|
||||
</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>
|
||||
<Stack className="cancel-upload w-full items-stretch gap-1">
|
||||
<Input
|
||||
placeholder="Eg. https://example.com/img.png"
|
||||
className="text-sm twui-image-url-input"
|
||||
title="Enter Image URL"
|
||||
wrapperWrapperProps={{ className: "mt-2" }}
|
||||
changeHandler={(value) => {
|
||||
imageUrlRef.current = value;
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
<Button
|
||||
title="Restore Image Button"
|
||||
size="smaller"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
onClick={() => {
|
||||
if (!imageUrlRef.current) return;
|
||||
setSrc(imageUrlRef.current);
|
||||
}}
|
||||
>
|
||||
Set Image URL
|
||||
</Button>
|
||||
</Stack>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user