dstpabuaran.com/resources/js/components/file-upload.tsx
Yoga Pangestu a138ffe63c feat: add media library configuration and file upload components
- Added `media-library.php` configuration file for managing media uploads and conversions.
- Updated `filesystems.php` to set default visibility for AWS S3 storage.
- Implemented `FileUpload` component for handling file uploads with preview and error handling.
- Created `ImagePreviewModal` component for displaying image previews in a modal.
- Introduced `Attachment` UI components for better file attachment handling.
- Added `useFileUpload` hook to manage file upload state and logic.
- Implemented utility functions for uploading files to S3 with presigned URLs.
- Created API route for generating presigned URLs for file uploads.
2026-07-30 10:16:49 +07:00

167 lines
5.5 KiB
TypeScript

import { FileImage, Upload, X } from 'lucide-react';
import { useEffect, useRef, useState } 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;
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`;
}
export function FileUpload({ value, onChange, folder, accept = 'image/jpeg,image/png,image/webp,image/gif', onUploadingChange, existingUrl, onFileMeta }: FileUploadProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [fileName, setFileName] = useState<string | null>(null);
const [fileSize, setFileSize] = useState<number | null>(null);
const [preview, setPreview] = useState<string | null>(null);
useEffect(() => {
onUploadingChange?.(uploading);
}, [uploading, onUploadingChange]);
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
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 (
<>
<input
ref={inputRef}
type="file"
accept={accept}
onChange={handleFileChange}
className="hidden"
id="file-upload"
/>
<Attachment state={state} orientation="horizontal" className='w-full'>
<AttachmentTrigger
onClick={() => inputRef.current?.click()}
aria-label={value ? 'Ganti file' : 'Pilih file untuk diunggah'}
/>
<AttachmentMedia variant={preview || existingUrl ? 'image' : 'icon'}>
{preview ? (
<img src={preview} alt={fileName ?? 'Preview'} />
) : existingUrl && value ? (
<img src={existingUrl} alt="Bukti" />
) : uploading ? (
<Upload className="animate-pulse" />
) : (
<FileImage />
)}
</AttachmentMedia>
<AttachmentContent>
{value ? (
<>
<AttachmentTitle>{fileName}</AttachmentTitle>
<AttachmentDescription>
{fileSize ? formatFileSize(fileSize) : 'Terupload'}
</AttachmentDescription>
</>
) : uploading ? (
<>
<AttachmentTitle>Mengunggah...</AttachmentTitle>
<AttachmentDescription>Memproses file</AttachmentDescription>
</>
) : error ? (
<>
<AttachmentTitle>Gagal</AttachmentTitle>
<AttachmentDescription>{error}</AttachmentDescription>
</>
) : (
<>
<AttachmentTitle>Unggah Bukti</AttachmentTitle>
<AttachmentDescription>Opsional · JPG, PNG, WebP, GIF · Maks 10MB</AttachmentDescription>
</>
)}
</AttachmentContent>
{value && (
<AttachmentActions>
<AttachmentAction aria-label="Hapus file" onClick={handleRemove}>
<X />
</AttachmentAction>
</AttachmentActions>
)}
</Attachment>
</>
);
}