Files
lti-web-client/src/components/input/DropFileInput.tsx
T
2025-11-04 15:54:13 +07:00

195 lines
4.8 KiB
TypeScript

import { useEffect } from 'react';
import { useDropzone, type Accept } from 'react-dropzone';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import { cn } from '@/lib/helper';
interface DropFileInputProps {
name: string;
label?: string;
bottomLabel?: string;
caption?: string;
values?: File[];
accept?: Accept;
required?: boolean;
maxFiles?: number; // defaults to 1
maxSize?: number; // defaults to 2097152 (2 MB)
isError?: boolean;
errorMessage?: string;
disabled?: boolean;
onChange?: (files: File[]) => void;
onDelete?: (index: number) => void;
className?: {
wrapper?: string;
inputContainer?: string;
label?: string;
inputWrapper?: string;
caption?: string;
bottomLabel?: string;
errorMessage?: string;
fileItemContainer?: string;
};
}
const DropFileInput: React.FC<DropFileInputProps> = ({
name,
label,
bottomLabel,
caption = 'Seret atau Pilih Dokumen',
values,
accept,
required,
maxFiles = Infinity,
maxSize,
isError,
errorMessage,
disabled,
onChange,
onDelete,
className,
}) => {
const isDisabled =
Boolean(values && maxFiles && values.length >= maxFiles) || disabled;
const {
acceptedFiles,
getRootProps,
getInputProps,
isFocused,
isDragAccept,
isDragReject,
} = useDropzone({
maxSize,
maxFiles,
accept: accept,
disabled: isDisabled,
});
useEffect(() => {
if (values && maxFiles && values.length <= maxFiles) {
onChange?.([...values, ...acceptedFiles]);
}
}, [acceptedFiles]);
return (
<div className={cn('w-full', className?.wrapper)}>
<div
className={cn(
'w-full flex flex-col gap-2 text-start',
className?.inputContainer
)}
>
{label && (
<label
htmlFor={name}
className={cn(
'w-full text-sm font-normal leading-5',
className?.label
)}
>
{label}
{required && (
<>
{' '}
<span className='tooltip tooltip-error' data-tip='required'>
<span className='text-error'>*</span>
</span>
</>
)}
</label>
)}
<div
{...getRootProps({
'aria-disabled': isDisabled,
className: cn(
'dropzone w-full px-4 py-2 border border-dashed border-gray-300 rounded cursor-pointer transition-all',
'hover:border-primary hover:bg-primary/10',
{
'border-success bg-success/10': isDragAccept,
'border-error bg-error/10': isDragReject || isError,
'border-primary bg-primary/10': isFocused,
'bg-gray-200/20 cursor-not-allowed': isDisabled,
},
className?.inputWrapper
),
})}
>
<input
{...getInputProps({
id: name,
name,
disabled: isDisabled,
'aria-disabled': isDisabled,
})}
/>
{caption && (
<p className={cn('text-gray-500 text-sm', className?.caption)}>
{caption}
</p>
)}
</div>
{!isError && bottomLabel && (
<p
className={cn('w-full text-sm opacity-60', className?.bottomLabel)}
>
{bottomLabel}
</p>
)}
{isError && (
<p
className={cn('w-full text-sm text-error', className?.errorMessage)}
>
{errorMessage}
</p>
)}
</div>
{values && values.length > 0 && (
<div
className={cn(
'w-full mt-1.5 flex flex-col gap-1.5',
className?.fileItemContainer
)}
>
{values.map((file, idx) => (
<div
key={idx}
className={cn('w-full flex flex-row items-center gap-2')}
>
<div className='p-2 rounded-full bg-primary/10'>
<Icon
icon='basil:file-solid'
width={24}
height={24}
className='text-blue-500'
/>
</div>
<div className='w-full text-sm'>
<p>{file.name}</p>
</div>
<Button
variant='ghost'
color='error'
onClick={() => {
onDelete?.(idx);
}}
className='rounded-full text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon icon='fluent:delete-12-regular' width={24} height={24} />
</Button>
</div>
))}
</div>
)}
</div>
);
};
export default DropFileInput;