import { useCallback, useEffect, useRef, useState } from 'react'; import Dropzone from 'react-dropzone'; import { Spinner } from 'react-bootstrap'; import { TbCloudUpload } from 'react-icons/tb'; import clsx from 'clsx'; import FileExtensionWithPreview from '@/components/FileExtensionWithPreview'; import { FILE_EXTENSIONS, IMAGE_EXTENSIONS, IMPORT_EXTENSIONS, S3_MIMES, uploadFileToS3, } from '@/helpers/s3Upload'; const MAX_SIZE = 500 * 1024 * 1024; function extensionsFor(field) { if (Array.isArray(field?.allowedExtensions) && field.allowedExtensions.length) { return field.allowedExtensions; } if (field?.type === 'file' && field.accept === 'import') return IMPORT_EXTENSIONS; if (field?.type === 'image' || field?.type === 'logo') return IMAGE_EXTENSIONS; return FILE_EXTENSIONS; } function acceptFromExtensions(exts) { return exts.reduce((accept, ext) => { const mime = S3_MIMES[ext]; if (!mime) return accept; const dotted = `.${ext}`; if (!accept[mime]) accept[mime] = []; if (!accept[mime].includes(dotted)) accept[mime].push(dotted); return accept; }, {}); } function isImagePath(value, type) { if (type?.startsWith('image/')) return true; return /\.(png|jpe?g|gif|webp|svg)(\?|$)/i.test(value || ''); } function fileNameFromUrl(url) { try { return decodeURIComponent(new URL(url).pathname.split('/').pop() || 'file'); } catch { return String(url || '').split('/').pop() || 'file'; } } function rejectMessage(rejected, exts) { const code = rejected?.[0]?.errors?.[0]?.code; if (code === 'file-too-large') return 'The file size is too big (500MB max).'; if (code === 'file-invalid-type') return `The file is not allowed (${exts.join(', ')} only).`; return rejected?.[0]?.errors?.[0]?.message || 'File was rejected'; } export default function S3FileField({ field, value = '', onChange, moduleName, required = false, onUploadingChange, }) { const [uploading, setUploading] = useState(false); const [error, setError] = useState(''); const [localFile, setLocalFile] = useState(null); const previewUrlRef = useRef(''); const onChangeRef = useRef(onChange); const uploadingRef = useRef(onUploadingChange); onChangeRef.current = onChange; uploadingRef.current = onUploadingChange; const exts = extensionsFor(field); const accept = acceptFromExtensions(exts); const clearPreview = () => { if (previewUrlRef.current) { URL.revokeObjectURL(previewUrlRef.current); previewUrlRef.current = ''; } }; useEffect(() => () => clearPreview(), []); const onDrop = useCallback( async (accepted, rejected) => { if (rejected?.length) { setError(rejectMessage(rejected, exts)); return; } const file = accepted[0]; if (!file) return; clearPreview(); const preview = file.type.startsWith('image/') ? URL.createObjectURL(file) : ''; previewUrlRef.current = preview; setLocalFile({ name: file.name, size: file.size, type: file.type, preview }); setError(''); setUploading(true); uploadingRef.current?.(true); try { const url = await uploadFileToS3(file, { moduleName: field?.uploadModule || moduleName }); onChangeRef.current?.(url); } catch (err) { setError(err.message || 'Upload failed'); onChangeRef.current?.(''); setLocalFile(null); clearPreview(); } finally { setUploading(false); uploadingRef.current?.(false); } }, [exts, field?.uploadModule, moduleName], ); const onRemove = (event) => { event.preventDefault(); event.stopPropagation(); if (uploading) return; clearPreview(); setLocalFile(null); setError(''); onChange?.(''); }; const previewSrc = localFile?.preview || (isImagePath(value, localFile?.type) ? value : ''); const displayName = localFile?.name || (value ? fileNameFromUrl(value) : ''); const hasFile = Boolean(localFile || value); const extension = (displayName.split('.').pop() || '').toLowerCase(); return (
{({ getRootProps, getInputProps, isDragActive }) => (
{hasFile ? ( <>
{previewSrc ? ( ) : ( )}

{displayName}

{uploading ? 'Uploading...' : 'Drag and drop or click to replace'}

{uploading ? ( ) : ( )} ) : (

Drag and drop a file here or click

)}
)}

500MB Max File size

{error ?
{error}
: null}
); }