diff --git a/src/lib/components/admin/GalleryUploader.svelte b/src/lib/components/admin/GalleryUploader.svelte index 5a9f7c9..0534f70 100644 --- a/src/lib/components/admin/GalleryUploader.svelte +++ b/src/lib/components/admin/GalleryUploader.svelte @@ -4,6 +4,8 @@ import SmartImage from '../SmartImage.svelte' import UnifiedMediaModal from './UnifiedMediaModal.svelte' import MediaDetailsModal from './MediaDetailsModal.svelte' + import FileIcon from '$icons/FileIcon.svelte' + import { validateImageFile, uploadMediaFiles } from '$lib/utils/mediaHelpers' // Gallery items can be either Media objects or objects with a mediaId reference type GalleryItem = Media | (Partial & { mediaId?: number }) @@ -55,43 +57,9 @@ const canAddMore = $derived(!maxItems || !value || value.length < maxItems) const remainingSlots = $derived(maxItems ? maxItems - (value?.length || 0) : Infinity) - // File validation + // File validation using shared helper function validateFile(file: File): string | null { - // Check file type - if (!file.type.startsWith('image/')) { - return 'Please select image files only' - } - - // Check file size - const sizeMB = file.size / 1024 / 1024 - if (sizeMB > maxFileSize) { - return `File size must be less than ${maxFileSize}MB` - } - - return null - } - - // Upload multiple files to server - async function uploadFiles(files: File[]): Promise { - const uploadPromises = files.map(async (file) => { - const formData = new FormData() - formData.append('file', file) - - const response = await fetch('/api/media/upload', { - method: 'POST', - body: formData, - credentials: 'same-origin' - }) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || `Upload failed for ${file.name}`) - } - - return await response.json() - }) - - return Promise.all(uploadPromises) + return validateImageFile(file, maxFileSize) } // Handle file selection/drop @@ -140,7 +108,8 @@ }, 100) }) - const uploadedMedia = await uploadFiles(filesToUpload) + // Upload files using shared helper + const uploadedMedia = await uploadMediaFiles(filesToUpload) as Media[] // Clear progress intervals progressIntervals.forEach((interval) => clearInterval(interval)) @@ -459,54 +428,7 @@ {:else}
- - - - - - - +

{placeholder}

Supports JPG, PNG, GIF up to {maxFileSize}MB diff --git a/src/lib/components/admin/ImageUploader.svelte b/src/lib/components/admin/ImageUploader.svelte index 37b5e55..cfcd80f 100644 --- a/src/lib/components/admin/ImageUploader.svelte +++ b/src/lib/components/admin/ImageUploader.svelte @@ -5,6 +5,8 @@ import SmartImage from '../SmartImage.svelte' import UnifiedMediaModal from './UnifiedMediaModal.svelte' import RefreshIcon from '$icons/refresh.svg?component' + import FileIcon from '$icons/FileIcon.svelte' + import { validateImageFile, uploadMediaFiles } from '$lib/utils/mediaHelpers' interface Props { label: string @@ -56,45 +58,22 @@ return `aspect-ratio: ${w}/${h}; padding-bottom: ${ratio}%;` }) - // File validation + // File validation using shared helper function validateFile(file: File): string | null { - // Check file type - if (!file.type.startsWith('image/')) { - return 'Please select an image file' - } - - // Check file size - const sizeMB = file.size / 1024 / 1024 - if (sizeMB > maxFileSize) { - return `File size must be less than ${maxFileSize}MB` - } - - return null + return validateImageFile(file, maxFileSize) } - // Upload file to server + // Upload file to server using shared helper async function uploadFile(file: File): Promise { - const formData = new FormData() - formData.append('file', file) - - // Removed altText upload - description is handled separately - + const extraFields: Record = {} + + // Add description if provided if (descriptionValue.trim()) { - formData.append('description', descriptionValue.trim()) + extraFields.description = descriptionValue.trim() } - const response = await fetch('/api/media/upload', { - method: 'POST', - body: formData, - credentials: 'same-origin' - }) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || 'Upload failed') - } - - return await response.json() + const uploadedMedia = await uploadMediaFiles([file], { extraFields }) + return uploadedMedia[0] as Media } // Handle file selection/drop @@ -420,54 +399,7 @@ {:else}

- - - - - - - +

{placeholder}

Supports JPG, PNG, GIF up to {maxFileSize}MB diff --git a/src/lib/utils/mediaHelpers.ts b/src/lib/utils/mediaHelpers.ts index 254585a..3636ff2 100644 --- a/src/lib/utils/mediaHelpers.ts +++ b/src/lib/utils/mediaHelpers.ts @@ -73,6 +73,25 @@ export function validateFileType(file: File, acceptedTypes: string[]): boolean { }) } +/** + * Validate image file for upload (type and size) + * Returns null if valid, error message if invalid + */ +export function validateImageFile(file: File, maxSizeMB: number): string | null { + // Check file type + if (!file.type.startsWith('image/')) { + return 'Please select an image file' + } + + // Check file size + const sizeMB = file.size / 1024 / 1024 + if (sizeMB > maxSizeMB) { + return `File size must be less than ${maxSizeMB}MB` + } + + return null +} + /** * Get display name for MIME type */ @@ -115,3 +134,64 @@ export function formatBitrate(bitrate: number): string { if (bitrate < 1000000) return `${(bitrate / 1000).toFixed(0)} kbps` return `${(bitrate / 1000000).toFixed(1)} Mbps` } + +/** + * Serialized Media type - represents Media as returned from API (dates as strings) + */ +export interface SerializedMedia { + id: number + filename: string + originalName: string + mimeType: string + size: number + url: string + thumbnailUrl: string | null + width: number | null + height: number | null + description: string | null + isPhotography: boolean + createdAt: string + updatedAt: string + exifData: Record | null + usedIn: string[] +} + +/** + * Upload media files to the server + * Returns serialized media objects (with string dates from JSON) + */ +export async function uploadMediaFiles( + files: File[], + options?: { + onProgress?: (fileKey: string, percent: number) => void + extraFields?: Record + } +): Promise { + const uploadPromises = files.map(async (file) => { + const formData = new FormData() + formData.append('file', file) + + // Add any extra fields (e.g., description) + if (options?.extraFields) { + Object.entries(options.extraFields).forEach(([key, value]) => { + formData.append(key, value) + }) + } + + const response = await fetch('/api/media/upload', { + method: 'POST', + body: formData, + credentials: 'same-origin' + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.error || `Upload failed for ${file.name}`) + } + + const result = await response.json() + return result as SerializedMedia + }) + + return Promise.all(uploadPromises) +}