Updates
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
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";
|
||||
import Tag from "../elements/Tag";
|
||||
|
||||
type FileInputUtils = {
|
||||
clearFileInput?: () => void;
|
||||
};
|
||||
|
||||
type ImageUploadProps = DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
onChangeHandler?: (
|
||||
fileData: FileInputToBase64FunctionReturn | undefined,
|
||||
inputRef?: React.RefObject<HTMLInputElement | null>,
|
||||
utils?: FileInputUtils,
|
||||
) => any;
|
||||
changeHandler?: (
|
||||
fileData: FileInputToBase64FunctionReturn | undefined,
|
||||
inputRef?: React.RefObject<HTMLInputElement | null>,
|
||||
utils?: FileInputUtils,
|
||||
) => 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;
|
||||
externalSetFileURL?: React.Dispatch<string | undefined>;
|
||||
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,
|
||||
changeHandler,
|
||||
externalSetFileURL,
|
||||
...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);
|
||||
const tempFileURLRef = React.useRef<string>("");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (existingFileUrl) {
|
||||
setFileUrl(existingFileUrl);
|
||||
}
|
||||
}, [existingFileUrl]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (existingFile) {
|
||||
setFile(existingFile);
|
||||
}
|
||||
}, [existingFile]);
|
||||
|
||||
function clearFileInput() {
|
||||
setFile(undefined);
|
||||
externalSetFile?.(undefined);
|
||||
onChangeHandler?.(undefined);
|
||||
changeHandler?.(undefined);
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
onClear?.();
|
||||
}
|
||||
|
||||
const fileInputUtils: FileInputUtils = { clearFileInput };
|
||||
|
||||
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,
|
||||
inputRef,
|
||||
fileInputUtils,
|
||||
);
|
||||
changeHandler?.(res, inputRef, fileInputUtils);
|
||||
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) => {
|
||||
clearFileInput();
|
||||
}}
|
||||
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}
|
||||
>
|
||||
<Stack className="w-full">
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
className="w-full py-2 text-sm"
|
||||
>
|
||||
{fileUrl}
|
||||
</Tag>
|
||||
</Stack>
|
||||
|
||||
<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);
|
||||
changeHandler?.(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);
|
||||
changeHandler?.(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>
|
||||
{externalSetFileURL ? (
|
||||
<Row
|
||||
className="flex-nowrap gap-0 items-stretch"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add Media URL"
|
||||
className="text-sm"
|
||||
wrapperProps={{ className: "h-full" }}
|
||||
wrapperWrapperProps={{
|
||||
className: "h-full",
|
||||
}}
|
||||
changeHandler={(value) => {
|
||||
tempFileURLRef.current = value;
|
||||
}}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key == "Enter") {
|
||||
if (tempFileURLRef.current) {
|
||||
setFileUrl(
|
||||
tempFileURLRef.current,
|
||||
);
|
||||
externalSetFileURL(
|
||||
tempFileURLRef.current,
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="Add Media URL"
|
||||
variant="outlined"
|
||||
className="py-0 px-3"
|
||||
color="gray"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (tempFileURLRef.current) {
|
||||
setFileUrl(
|
||||
tempFileURLRef.current,
|
||||
);
|
||||
externalSetFileURL(
|
||||
tempFileURLRef.current,
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="text-2xl">+</span>
|
||||
</Button>
|
||||
</Row>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Center>
|
||||
</Card>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user