This commit is contained in:
Benjamin Toby
2025-03-27 07:37:16 +01:00
parent 9dd6c3a70e
commit 8762e2da8d
23 changed files with 968 additions and 88 deletions
+164
View File
@@ -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>
);
}
+21 -25
View File
@@ -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}
/>
+1 -1
View File
@@ -89,7 +89,7 @@ export default function Select({
props.className
)}
ref={componentRef}
defaultValue={
value={
options.flat().find((opt) => opt.default)?.value ||
undefined
}