import { FileImage, Upload, X } from 'lucide-react'; import { useEffect, useRef, useState, useId } from 'react'; import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle, AttachmentTrigger, } from '@/components/ui/attachment'; import { uploadFile, UploadError } from '@/lib/upload'; type FileUploadProps = { value: string | null; onChange: (key: string | null) => void; folder?: string; accept?: string; maxSize?: number; onUploadingChange?: (uploading: boolean) => void; existingUrl?: string | null; onFileMeta?: (meta: { size: number; type: string } | null) => void; }; function formatFileSize(bytes: number): string { if (bytes < 1024) { return `${bytes} B`; } if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(1)} KB`; } return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function acceptToLabels(accept: string): string[] { const mimeMap: Record = { 'image/jpeg': 'JPG', 'image/png': 'PNG', 'image/webp': 'WebP', 'image/gif': 'GIF', 'image/svg+xml': 'SVG', 'application/pdf': 'PDF', 'video/mp4': 'MP4', 'application/zip': 'ZIP', }; return accept .split(',') .map( (mime) => mimeMap[mime.trim()] || mime.trim().split('/').pop()?.toUpperCase() || 'File', ) .filter((v, i, a) => a.indexOf(v) === i); } function formatMaxSize(bytes: number): string { if (bytes < 1024 * 1024) { return `${(bytes / 1024).toFixed(0)}KB`; } return `${(bytes / (1024 * 1024)).toFixed(0)}MB`; } export function FileUpload({ value, onChange, folder, accept = 'image/jpeg,image/png,image/webp,image/gif', maxSize = 10 * 1024 * 1024, onUploadingChange, existingUrl, onFileMeta, }: FileUploadProps) { const inputRef = useRef(null); const uploadId = useId(); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [fileName, setFileName] = useState(null); const [fileSize, setFileSize] = useState(null); const [preview, setPreview] = useState(null); const onUploadingChangeRef = useRef(onUploadingChange); onUploadingChangeRef.current = onUploadingChange; useEffect(() => { onUploadingChangeRef.current?.(uploading); }, [uploading]); async function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) { return; } setError(null); setUploading(true); setFileName(file.name); setFileSize(file.size); const objectUrl = URL.createObjectURL(file); setPreview(objectUrl); try { const key = await uploadFile(file, folder); onChange(key); onFileMeta?.({ size: file.size, type: file.type }); } catch (err) { const message = err instanceof UploadError ? err.message : 'Gagal mengunggah file.'; setError(message); setPreview(null); setFileName(null); setFileSize(null); onFileMeta?.(null); } finally { setUploading(false); if (inputRef.current) { inputRef.current.value = ''; } } } function handleRemove(e: React.MouseEvent) { e.stopPropagation(); onChange(null); setFileName(null); setFileSize(null); setPreview(null); setError(null); onFileMeta?.(null); if (inputRef.current) { inputRef.current.value = ''; } } const state = uploading ? 'uploading' : error ? 'error' : value ? 'done' : 'idle'; return ( <> inputRef.current?.click()} aria-label={ value ? 'Ganti file' : 'Pilih file untuk diunggah' } /> {preview ? ( {fileName ) : existingUrl && value ? ( Bukti ) : uploading ? ( ) : ( )} {value ? ( <> {fileName} {fileSize ? formatFileSize(fileSize) : 'Terupload'} ) : uploading ? ( <> Mengunggah... Memproses file ) : error ? ( <> Gagal {error} ) : ( <> Unggah File {`${acceptToLabels(accept).join(', ')} ยท Maks ${formatMaxSize(maxSize)}`} )} {value && ( )} ); }