137 lines
4.7 KiB
TypeScript
137 lines
4.7 KiB
TypeScript
'use client';
|
|
|
|
import { Button } from '@/ui/components/shadcn/button';
|
|
import {
|
|
Field,
|
|
FieldDescription,
|
|
FieldError,
|
|
} from '@/ui/components/shadcn/field';
|
|
import {
|
|
FileUpload,
|
|
FileUploadDropzone,
|
|
FileUploadItem,
|
|
FileUploadItemDelete,
|
|
FileUploadItemMetadata,
|
|
FileUploadItemPreview,
|
|
FileUploadList,
|
|
FileUploadTrigger,
|
|
} from '@/ui/components/shadcn/file-upload';
|
|
import { Spinner } from '@/ui/components/shadcn/spinner';
|
|
import { X } from 'lucide-react';
|
|
import React, { useCallback } from 'react';
|
|
|
|
export interface FileUploadFieldProps {
|
|
file: File | null;
|
|
onFileChange: (file: File | null) => Promise<void>;
|
|
accept?: string;
|
|
validate?: (file: File) => string | null;
|
|
onFileReject?: (file: File, message: string) => void;
|
|
label?: string;
|
|
description?: string;
|
|
error?: string;
|
|
icon?: React.ReactNode;
|
|
previewUrl?: string;
|
|
isUploading?: boolean;
|
|
}
|
|
|
|
export const FileUploadField: React.FC<FileUploadFieldProps> = ({
|
|
file,
|
|
onFileChange,
|
|
accept,
|
|
validate,
|
|
onFileReject,
|
|
label = 'File',
|
|
description,
|
|
error,
|
|
icon,
|
|
previewUrl,
|
|
isUploading,
|
|
}) => {
|
|
const handleAccept = useCallback(
|
|
(files: File[]) => {
|
|
const accepted = files[0];
|
|
if (!accepted) return;
|
|
onFileChange(accepted).then(() => {});
|
|
},
|
|
[onFileChange]
|
|
);
|
|
|
|
const handleValueChange = useCallback(
|
|
(files: File[]) => {
|
|
if (files.length === 0) onFileChange(null);
|
|
},
|
|
[onFileChange]
|
|
);
|
|
|
|
return (
|
|
<Field data-invalid={!!error}>
|
|
<FileUpload
|
|
value={file ? [file] : []}
|
|
onValueChange={handleValueChange}
|
|
onAccept={handleAccept}
|
|
onFileReject={onFileReject}
|
|
onFileValidate={validate}
|
|
accept={accept}
|
|
maxFiles={1}
|
|
multiple={false}
|
|
label={label}
|
|
className='min-w-0'
|
|
>
|
|
<FileUploadDropzone className='p-3'>
|
|
{icon}
|
|
<div className='flex flex-col gap-0.5 text-center'>
|
|
<p className='text-xs font-medium text-foreground'>
|
|
{label}
|
|
</p>
|
|
<p className='text-xs text-muted-foreground'>
|
|
Drag & drop or{' '}
|
|
<FileUploadTrigger className='cursor-pointer font-medium text-primary underline-offset-4 hover:underline'>
|
|
browse
|
|
</FileUploadTrigger>
|
|
</p>
|
|
</div>
|
|
</FileUploadDropzone>
|
|
|
|
<FileUploadList>
|
|
{file && (
|
|
<FileUploadItem value={file}>
|
|
<FileUploadItemPreview
|
|
render={
|
|
isUploading
|
|
? () => (
|
|
<Spinner className='size-5 text-muted-foreground' />
|
|
)
|
|
: previewUrl
|
|
? () => (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={previewUrl}
|
|
alt='Uploaded image'
|
|
className='size-full object-cover'
|
|
/>
|
|
)
|
|
: undefined
|
|
}
|
|
/>
|
|
<FileUploadItemMetadata size='sm' />
|
|
<FileUploadItemDelete asChild>
|
|
<Button
|
|
type='button'
|
|
variant='ghost'
|
|
size='icon'
|
|
className='ml-auto size-7 shrink-0'
|
|
>
|
|
<X className='size-3.5' />
|
|
</Button>
|
|
</FileUploadItemDelete>
|
|
</FileUploadItem>
|
|
)}
|
|
</FileUploadList>
|
|
</FileUpload>
|
|
|
|
{description && <FieldDescription>{description}</FieldDescription>}
|
|
{error && <FieldError errors={[new Error(error)]} />}
|
|
</Field>
|
|
);
|
|
};
|