329 lines
12 KiB
TypeScript
329 lines
12 KiB
TypeScript
import Button from "../layout/Button";
|
|
import Stack from "../layout/Stack";
|
|
import { FileArchive, FilePlus2, X } from "lucide-react";
|
|
import React, { ComponentProps, DetailedHTMLProps, ReactNode } from "react";
|
|
import Card from "../elements/Card";
|
|
import Span from "../layout/Span";
|
|
import Center from "../layout/Center";
|
|
import { FileInputToBase64FunctionReturn } from "../utils/form/fileInputToBase64";
|
|
import { twMerge } from "tailwind-merge";
|
|
import fileInputToBase64 from "../utils/form/fileInputToBase64";
|
|
import Row from "../layout/Row";
|
|
import Input from "./Input";
|
|
import Loading from "../elements/Loading";
|
|
|
|
type ImageUploadProps = DetailedHTMLProps<
|
|
React.HTMLAttributes<HTMLDivElement>,
|
|
HTMLDivElement
|
|
> & {
|
|
onChangeHandler?: (
|
|
fileData: FileInputToBase64FunctionReturn | undefined
|
|
) => any;
|
|
onClear?: () => void;
|
|
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 | ReactNode;
|
|
disablePreview?: boolean;
|
|
allowedRegex?: RegExp;
|
|
externalSetFile?: React.Dispatch<
|
|
React.SetStateAction<FileInputToBase64FunctionReturn | undefined>
|
|
>;
|
|
externalSetFiles?: React.Dispatch<
|
|
React.SetStateAction<FileInputToBase64FunctionReturn[] | undefined>
|
|
>;
|
|
existingFile?: FileInputToBase64FunctionReturn;
|
|
existingFileUrl?: string;
|
|
icon?: ReactNode;
|
|
labelSpanProps?: ComponentProps<typeof Span>;
|
|
loading?: boolean;
|
|
multiple?: boolean;
|
|
};
|
|
|
|
/**
|
|
* @note use the `onChangeHandler` prop to grab the parsed base64 image object
|
|
*/
|
|
export default function FileUpload({
|
|
onChangeHandler,
|
|
fileInputProps,
|
|
placeHolderWrapper,
|
|
previewImageWrapperProps,
|
|
previewImageProps,
|
|
label,
|
|
disablePreview,
|
|
allowedRegex,
|
|
externalSetFile,
|
|
externalSetFiles,
|
|
existingFile,
|
|
existingFileUrl,
|
|
icon,
|
|
labelSpanProps,
|
|
loading,
|
|
multiple,
|
|
onClear,
|
|
...props
|
|
}: ImageUploadProps) {
|
|
const [file, setFile] = React.useState<
|
|
FileInputToBase64FunctionReturn | undefined
|
|
>(existingFile);
|
|
const [fileUrl, setFileUrl] = React.useState<string | undefined>(
|
|
existingFileUrl
|
|
);
|
|
|
|
const [fileDraggedOver, setFileDraggedOver] = React.useState(false);
|
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (existingFileUrl) {
|
|
setFileUrl(existingFileUrl);
|
|
}
|
|
}, [existingFileUrl]);
|
|
|
|
React.useEffect(() => {
|
|
if (existingFile) {
|
|
setFile(existingFile);
|
|
}
|
|
}, [existingFile]);
|
|
|
|
return (
|
|
<Stack
|
|
{...props}
|
|
className={twMerge("w-full h-[300px]", props?.className)}
|
|
>
|
|
<input
|
|
type="file"
|
|
multiple={multiple}
|
|
className={twMerge("hidden", fileInputProps?.className)}
|
|
{...fileInputProps}
|
|
onChange={(e) => {
|
|
if (multiple) {
|
|
(async () => {
|
|
const files = e.target.files;
|
|
if (!files?.[0]) return;
|
|
|
|
let filesArr: FileInputToBase64FunctionReturn[] =
|
|
[];
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
const fileObj = await fileInputToBase64({
|
|
inputFile: file,
|
|
});
|
|
filesArr.push(fileObj);
|
|
}
|
|
|
|
externalSetFiles?.(filesArr);
|
|
})();
|
|
} else {
|
|
const inputFile = e.target.files?.[0];
|
|
|
|
if (!inputFile) return;
|
|
|
|
fileInputToBase64({ inputFile, allowedRegex }).then(
|
|
(res) => {
|
|
setFile(res);
|
|
externalSetFile?.(res);
|
|
onChangeHandler?.(res);
|
|
fileInputProps?.onChange?.(e);
|
|
}
|
|
);
|
|
}
|
|
}}
|
|
ref={inputRef as any}
|
|
/>
|
|
|
|
{loading ? (
|
|
<Card className={twMerge("w-full h-full ")}>
|
|
<Center>
|
|
<Loading />
|
|
</Center>
|
|
</Card>
|
|
) : file ? (
|
|
<Card
|
|
{...previewImageWrapperProps}
|
|
className={twMerge(
|
|
"w-full relative h-full items-center justify-center overflow-hidden",
|
|
"pb-10",
|
|
previewImageWrapperProps?.className
|
|
)}
|
|
>
|
|
<Stack>
|
|
{disablePreview ? (
|
|
<Span className="opacity-50" size="small">
|
|
Image Uploaded!
|
|
</Span>
|
|
) : file.fileType?.match(/image/i) ? (
|
|
<img
|
|
src={file.fileBase64Full}
|
|
className="w-full object-contain overflow-hidden"
|
|
{...previewImageProps}
|
|
/>
|
|
) : (
|
|
<Stack>
|
|
<FileArchive size={36} strokeWidth={1} />
|
|
<Stack className="gap-0">
|
|
<Span>
|
|
{file.file?.name || file.fileName}
|
|
</Span>
|
|
<Span size="smaller" className="opacity-70">
|
|
{file.fileType}
|
|
</Span>
|
|
</Stack>
|
|
</Stack>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
className={twMerge(
|
|
"absolute p-2 top-2 right-2 z-20 bg-background-light dark:bg-background-dark",
|
|
"hover:bg-white dark:hover:bg-black"
|
|
)}
|
|
onClick={(e) => {
|
|
setFile(undefined);
|
|
externalSetFile?.(undefined);
|
|
onChangeHandler?.(undefined);
|
|
if (inputRef.current) {
|
|
inputRef.current.value = "";
|
|
}
|
|
onClear?.();
|
|
}}
|
|
title="Cancel File Upload Button"
|
|
>
|
|
<X className="text-slate-950 dark:text-white" />
|
|
</Button>
|
|
<Input
|
|
value={file.fileName}
|
|
onChange={(e) => {
|
|
setFile({ ...file, fileName: e.target.value });
|
|
externalSetFile?.({
|
|
...file,
|
|
fileName: e.target.value,
|
|
});
|
|
}}
|
|
/>
|
|
</Stack>
|
|
</Card>
|
|
) : fileUrl ? (
|
|
<Card
|
|
className="w-full relative h-full items-center justify-center overflow-hidden"
|
|
{...previewImageWrapperProps}
|
|
>
|
|
{disablePreview ? (
|
|
<Span className="opacity-50" size="small">
|
|
Image Uploaded!
|
|
</Span>
|
|
) : fileUrl.match(/\.pdf$|\.txt$/) ? (
|
|
<Row>
|
|
<FileArchive size={36} strokeWidth={1} />
|
|
<Stack className="gap-0">
|
|
<Span size="smaller" className="opacity-70">
|
|
{fileUrl}
|
|
</Span>
|
|
</Stack>
|
|
</Row>
|
|
) : (
|
|
<img
|
|
src={fileUrl}
|
|
className="w-full object-contain overflow-hidden"
|
|
{...previewImageProps}
|
|
/>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
className={twMerge(
|
|
"absolute p-2 top-2 right-2 z-20 bg-white dark:bg-black",
|
|
"hover:bg-white dark:hover:bg-black"
|
|
)}
|
|
onClick={(e) => {
|
|
setFile(undefined);
|
|
externalSetFile?.(undefined);
|
|
onChangeHandler?.(undefined);
|
|
setFileUrl(undefined);
|
|
}}
|
|
title="Cancel File Button"
|
|
>
|
|
<X className="text-slate-950 dark:text-white" />
|
|
</Button>
|
|
</Card>
|
|
) : (
|
|
<Card
|
|
className={twMerge(
|
|
"w-full h-full cursor-pointer hover:bg-slate-100/50 dark:hover:bg-white/5",
|
|
"border-dashed border-2",
|
|
fileDraggedOver ? "bg-slate-100 dark:bg-white/10" : "",
|
|
placeHolderWrapper?.className
|
|
)}
|
|
onClick={(e) => {
|
|
inputRef.current?.click();
|
|
placeHolderWrapper?.onClick?.(e);
|
|
}}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
setFileDraggedOver(true);
|
|
}}
|
|
onDragLeave={(e) => {
|
|
setFileDraggedOver(false);
|
|
}}
|
|
onDrop={(e) => {
|
|
e.preventDefault();
|
|
setFileDraggedOver(false);
|
|
let inputFile: File | null = null;
|
|
|
|
if (e.dataTransfer.items) {
|
|
[...e.dataTransfer.items].forEach((item, i) => {
|
|
if (inputFile) return;
|
|
if (item.kind === "file") {
|
|
const file = item.getAsFile();
|
|
inputFile = file;
|
|
}
|
|
});
|
|
} else {
|
|
inputFile = e.dataTransfer.files?.[0];
|
|
}
|
|
|
|
if (!inputFile) return;
|
|
|
|
fileInputToBase64({ inputFile, allowedRegex }).then(
|
|
(res) => {
|
|
setFile(res);
|
|
externalSetFile?.(res);
|
|
onChangeHandler?.(res);
|
|
}
|
|
);
|
|
}}
|
|
{...placeHolderWrapper}
|
|
>
|
|
<Center
|
|
className={twMerge(
|
|
fileDraggedOver ? "pointer-events-none" : ""
|
|
)}
|
|
>
|
|
<Stack className="items-center gap-2">
|
|
{icon || <FilePlus2 className="text-slate-400" />}
|
|
<Span
|
|
size="smaller"
|
|
variant="faded"
|
|
{...labelSpanProps}
|
|
>
|
|
{label || "Click to Upload File"}
|
|
</Span>
|
|
</Stack>
|
|
</Center>
|
|
</Card>
|
|
)}
|
|
</Stack>
|
|
);
|
|
}
|