Updates
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import Button from "../layout/Button";
|
||||
import Stack from "../layout/Stack";
|
||||
import {
|
||||
File,
|
||||
FileArchive,
|
||||
FilePlus,
|
||||
FilePlus2,
|
||||
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, {
|
||||
FileInputToBase64FunctionReturn,
|
||||
} from "../utils/form/fileInputToBase64";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import fileInputToBase64 from "../utils/form/fileInputToBase64";
|
||||
import Row from "../layout/Row";
|
||||
|
||||
type ImageUploadProps = DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
onChangeHandler?: (
|
||||
imgData: FileInputToBase64FunctionReturn | 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;
|
||||
allowedRegex?: RegExp;
|
||||
externalSetFile?: React.Dispatch<
|
||||
React.SetStateAction<FileInputToBase64FunctionReturn | undefined>
|
||||
>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @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,
|
||||
...props
|
||||
}: ImageUploadProps) {
|
||||
const [file, setFile] = React.useState<
|
||||
FileInputToBase64FunctionReturn | undefined
|
||||
>(undefined);
|
||||
const inputRef = React.useRef<HTMLInputElement>();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
{...props}
|
||||
className={twMerge("w-full h-[300px]", props?.className)}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
className={twMerge("hidden", fileInputProps?.className)}
|
||||
{...fileInputProps}
|
||||
onChange={(e) => {
|
||||
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}
|
||||
/>
|
||||
|
||||
{file ? (
|
||||
<Card
|
||||
className="w-full relative h-full items-center justify-center overflow-hidden"
|
||||
{...previewImageWrapperProps}
|
||||
>
|
||||
{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}
|
||||
/>
|
||||
) : (
|
||||
<Row>
|
||||
<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>
|
||||
</Row>
|
||||
)}
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
<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) => {
|
||||
inputRef.current?.click();
|
||||
placeHolderWrapper?.onClick?.(e);
|
||||
}}
|
||||
{...placeHolderWrapper}
|
||||
>
|
||||
<Center>
|
||||
<Stack className="items-center gap-2">
|
||||
<FilePlus2 className="text-slate-400" />
|
||||
<Span size="smaller" variant="faded">
|
||||
{label || "Click to Upload File"}
|
||||
</Span>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Card>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -135,15 +135,7 @@ export default function Input<KeyType extends string>({
|
||||
...props
|
||||
}: InputProps<KeyType>) {
|
||||
const [focus, setFocus] = React.useState(false);
|
||||
const [value, setValue] = React.useState(
|
||||
props.value
|
||||
? String(props.value)
|
||||
: props.defaultValue
|
||||
? String(props.defaultValue)
|
||||
: ""
|
||||
);
|
||||
|
||||
delete props.defaultValue;
|
||||
const [value, setValue] = React.useState(props.defaultValue || props.value);
|
||||
|
||||
const [isValid, setIsValid] = React.useState(true);
|
||||
|
||||
@@ -151,27 +143,28 @@ export default function Input<KeyType extends string>({
|
||||
const finalDebounce = debounce || DEFAULT_DEBOUNCE;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!value.match(/./)) return setIsValid(true);
|
||||
window.clearTimeout(timeout);
|
||||
if (typeof value == "string") {
|
||||
if (!value.match(/./)) return setIsValid(true);
|
||||
window.clearTimeout(timeout);
|
||||
|
||||
if (validationRegex) {
|
||||
timeout = setTimeout(() => {
|
||||
setIsValid(validationRegex.test(value));
|
||||
}, finalDebounce);
|
||||
}
|
||||
if (validationRegex) {
|
||||
timeout = setTimeout(() => {
|
||||
setIsValid(validationRegex.test(value));
|
||||
}, finalDebounce);
|
||||
}
|
||||
|
||||
if (validationFunction) {
|
||||
timeout = setTimeout(() => {
|
||||
validationFunction(value).then((res) => {
|
||||
setIsValid(res);
|
||||
});
|
||||
}, finalDebounce);
|
||||
if (validationFunction) {
|
||||
timeout = setTimeout(() => {
|
||||
validationFunction(value).then((res) => {
|
||||
setIsValid(res);
|
||||
});
|
||||
}, finalDebounce);
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!props.value) return;
|
||||
setValue(String(props.value));
|
||||
setValue(props.value || "");
|
||||
}, [props.value]);
|
||||
|
||||
const targetComponent = istextarea ? (
|
||||
@@ -192,7 +185,10 @@ export default function Input<KeyType extends string>({
|
||||
props?.onBlur?.(e);
|
||||
}}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
props?.onChange?.(e);
|
||||
}}
|
||||
autoComplete={autoComplete}
|
||||
rows={props.height ? Number(props.height) : 4}
|
||||
/>
|
||||
|
||||
@@ -89,7 +89,7 @@ export default function Select({
|
||||
props.className
|
||||
)}
|
||||
ref={componentRef}
|
||||
defaultValue={
|
||||
value={
|
||||
options.flat().find((opt) => opt.default)?.value ||
|
||||
undefined
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user