201 lines
4.9 KiB
TypeScript
201 lines
4.9 KiB
TypeScript
'use client';
|
|
|
|
import type { FormEvent, MouseEventHandler } from 'react';
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
|
import Image from 'next/image';
|
|
|
|
import { UploadCloud, X } from 'lucide-react';
|
|
|
|
import { cn } from '../lib/utils';
|
|
import { Button } from '../shadcn/button';
|
|
import { Label } from '../shadcn/label';
|
|
import { If } from './if';
|
|
|
|
type Props = Omit<React.InputHTMLAttributes<unknown>, 'value'> & {
|
|
image?: string | null;
|
|
onClear?: () => void;
|
|
onValueChange?: (props: { image: string; file: File }) => void;
|
|
visible?: boolean;
|
|
} & React.ComponentPropsWithRef<'input'>;
|
|
|
|
const IMAGE_SIZE = 22;
|
|
|
|
export const ImageUploadInput: React.FC<Props> =
|
|
function ImageUploadInputComponent({
|
|
children,
|
|
image,
|
|
onClear,
|
|
onInput,
|
|
onValueChange,
|
|
ref: forwardedRef,
|
|
visible = true,
|
|
...props
|
|
}) {
|
|
const localRef = useRef<HTMLInputElement>(null);
|
|
|
|
const [state, setState] = useState({
|
|
image,
|
|
fileName: '',
|
|
});
|
|
|
|
const onInputChange = useCallback(
|
|
(e: FormEvent<HTMLInputElement>) => {
|
|
e.preventDefault();
|
|
|
|
const files = e.currentTarget.files;
|
|
|
|
if (files?.length) {
|
|
const file = files[0];
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const data = URL.createObjectURL(file);
|
|
|
|
setState({
|
|
image: data,
|
|
fileName: file.name,
|
|
});
|
|
|
|
if (onValueChange) {
|
|
onValueChange({
|
|
image: data,
|
|
file,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (onInput) {
|
|
onInput(e);
|
|
}
|
|
},
|
|
[onInput, onValueChange],
|
|
);
|
|
|
|
const onRemove = useCallback(() => {
|
|
setState({
|
|
image: '',
|
|
fileName: '',
|
|
});
|
|
|
|
if (localRef.current) {
|
|
localRef.current.value = '';
|
|
}
|
|
|
|
if (onClear) {
|
|
onClear();
|
|
}
|
|
}, [onClear]);
|
|
|
|
const imageRemoved: MouseEventHandler = useCallback(
|
|
(e) => {
|
|
e.preventDefault();
|
|
|
|
onRemove();
|
|
},
|
|
[onRemove],
|
|
);
|
|
|
|
const setRef = useCallback(
|
|
(input: HTMLInputElement) => {
|
|
localRef.current = input;
|
|
|
|
if (typeof forwardedRef === 'function') {
|
|
forwardedRef(localRef.current);
|
|
}
|
|
},
|
|
[forwardedRef],
|
|
);
|
|
|
|
useEffect(() => {
|
|
setState((state) => ({ ...state, image }));
|
|
}, [image]);
|
|
|
|
useEffect(() => {
|
|
if (!image) {
|
|
onRemove();
|
|
}
|
|
}, [image, onRemove]);
|
|
|
|
const Input = () => (
|
|
<input
|
|
{...props}
|
|
className={cn('hidden', props.className)}
|
|
ref={setRef}
|
|
type={'file'}
|
|
onInput={onInputChange}
|
|
accept="image/*"
|
|
aria-labelledby={'image-upload-input'}
|
|
/>
|
|
);
|
|
|
|
if (!visible) {
|
|
return <Input />;
|
|
}
|
|
|
|
return (
|
|
<label
|
|
id={'image-upload-input'}
|
|
className={`border-input bg-background ring-primary ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring relative flex h-10 w-full cursor-pointer rounded-md border border-dashed px-3 py-2 text-sm ring-offset-2 outline-hidden transition-all file:border-0 file:bg-transparent file:text-sm file:font-medium focus:ring-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50`}
|
|
>
|
|
<Input />
|
|
|
|
<div className={'flex items-center space-x-4'}>
|
|
<div className={'flex'}>
|
|
<If condition={!state.image}>
|
|
<UploadCloud className={'text-muted-foreground h-5'} />
|
|
</If>
|
|
|
|
<If condition={state.image}>
|
|
<Image
|
|
loading={'lazy'}
|
|
style={{
|
|
width: IMAGE_SIZE,
|
|
height: IMAGE_SIZE,
|
|
}}
|
|
className={'object-contain'}
|
|
width={IMAGE_SIZE}
|
|
height={IMAGE_SIZE}
|
|
src={state.image!}
|
|
alt={props.alt ?? ''}
|
|
/>
|
|
</If>
|
|
</div>
|
|
|
|
<If condition={!state.image}>
|
|
<div className={'flex flex-auto'}>
|
|
<Label className={'cursor-pointer text-xs'}>{children}</Label>
|
|
</div>
|
|
</If>
|
|
|
|
<If condition={state.image}>
|
|
<div className={'flex flex-auto'}>
|
|
<If
|
|
condition={state.fileName}
|
|
fallback={
|
|
<Label className={'cursor-pointer truncate text-xs'}>
|
|
{children}
|
|
</Label>
|
|
}
|
|
>
|
|
<Label className={'truncate text-xs'}>{state.fileName}</Label>
|
|
</If>
|
|
</div>
|
|
</If>
|
|
|
|
<If condition={state.image}>
|
|
<Button
|
|
size={'icon'}
|
|
className={'h-5! w-5!'}
|
|
onClick={imageRemoved}
|
|
>
|
|
<X className="h-4" />
|
|
</Button>
|
|
</If>
|
|
</div>
|
|
</label>
|
|
);
|
|
};
|