B2B-88: add starter kit structure and elements
This commit is contained in:
200
packages/ui/src/makerkit/image-upload-input.tsx
Normal file
200
packages/ui/src/makerkit/image-upload-input.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user