- 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.
80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
|
|
|
export type PresignedUrlResponse = {
|
|
upload_url: string;
|
|
key: string;
|
|
uuid: string;
|
|
};
|
|
|
|
export class UploadError extends Error {
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = 'UploadError';
|
|
}
|
|
}
|
|
|
|
function getSessionCookie(): string {
|
|
return document.cookie.split('; ').find((c) => c.startsWith('XSRF-TOKEN='))?.split('=')[1] ?? '';
|
|
}
|
|
|
|
export async function requestPresignedUrl(
|
|
fileName: string,
|
|
mimeType: string,
|
|
folder?: string,
|
|
): Promise<PresignedUrlResponse> {
|
|
const response = await fetch('/api/presigned-url', {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'X-XSRF-TOKEN': decodeURIComponent(getSessionCookie()),
|
|
},
|
|
body: JSON.stringify({ file_name: fileName, mime_type: mimeType, folder }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => null);
|
|
|
|
throw new UploadError(data?.message ?? 'Gagal membuat URL upload.');
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
export async function uploadToS3(uploadUrl: string, file: File): Promise<void> {
|
|
const response = await fetch(uploadUrl, {
|
|
method: 'PUT',
|
|
body: file,
|
|
headers: {
|
|
'Content-Type': file.type,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new UploadError('Gagal mengunggah file ke storage.');
|
|
}
|
|
}
|
|
|
|
export async function uploadFile(file: File, folder?: string): Promise<string> {
|
|
if (!ALLOWED_TYPES.includes(file.type)) {
|
|
throw new UploadError('Tipe file tidak didukung. Hanya JPG, PNG, WebP, dan GIF yang diizinkan.');
|
|
}
|
|
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
throw new UploadError('Ukuran file melebihi batas 10MB.');
|
|
}
|
|
|
|
const { upload_url, key } = await requestPresignedUrl(file.name, file.type, folder);
|
|
await uploadToS3(upload_url, file);
|
|
|
|
return key;
|
|
}
|
|
|
|
export function getTemporaryUrl(key: string, minutes = 60): string {
|
|
// This is a client-side helper - the actual presigned GET URL
|
|
// should be generated server-side via the Expense model accessor
|
|
return `/api/presigned-url/${encodeURIComponent(key)}?minutes=${minutes}`;
|
|
}
|