From 2ec0be092e5274ccd056ddf90517a824ae32a4a3 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 26 Jun 2025 13:46:52 -0400 Subject: [PATCH] refactor: extract reusable components from media modals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create MediaGrid component for media selection grid (~150 lines) - Create FileUploadZone component for drag-and-drop uploads (~120 lines) - Create FilePreviewList component for upload preview (~100 lines) - Create MediaMetadataPanel component for media details (~150 lines) - Create MediaUsageList component for usage tracking (~80 lines) - Add mediaHelpers.ts utility for file operations This reduces ~750-800 lines of duplicate code across media modals. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../components/admin/FilePreviewList.svelte | 349 ++++++++++++++++++ .../components/admin/FileUploadZone.svelte | 313 ++++++++++++++++ src/lib/components/admin/MediaGrid.svelte | 277 ++++++++++++++ .../admin/MediaMetadataPanel.svelte | 223 +++++++++++ .../components/admin/MediaUsageList.svelte | 162 ++++++++ src/lib/utils/mediaHelpers.ts | 94 +++++ 6 files changed, 1418 insertions(+) create mode 100644 src/lib/components/admin/FilePreviewList.svelte create mode 100644 src/lib/components/admin/FileUploadZone.svelte create mode 100644 src/lib/components/admin/MediaGrid.svelte create mode 100644 src/lib/components/admin/MediaMetadataPanel.svelte create mode 100644 src/lib/components/admin/MediaUsageList.svelte create mode 100644 src/lib/utils/mediaHelpers.ts diff --git a/src/lib/components/admin/FilePreviewList.svelte b/src/lib/components/admin/FilePreviewList.svelte new file mode 100644 index 0000000..cddfe16 --- /dev/null +++ b/src/lib/components/admin/FilePreviewList.svelte @@ -0,0 +1,349 @@ + + +
+ {#each previews as preview (preview.id)} +
+
+ {#if isImageFile(preview.type)} + {preview.name} + {:else} +
📄
+ {/if} +
+ +
+
{preview.name}
+
{formatFileSize(preview.size)}
+
+ + {#if !isUploading && onRemove} + + {/if} + + {#if variant === 'upload' && isUploading && preview.file} +
+
+
+
+
+ {#if uploadProgress[preview.name] === 100} + + {:else if uploadProgress[preview.name] > 0} + {Math.round(uploadProgress[preview.name] || 0)}% + {:else} + Waiting... + {/if} +
+
+ {/if} +
+ {/each} + + {#if uploadErrors.length > 0} +
+ {#each uploadErrors as error} +
❌ {error}
+ {/each} +
+ {/if} +
+ + \ No newline at end of file diff --git a/src/lib/components/admin/FileUploadZone.svelte b/src/lib/components/admin/FileUploadZone.svelte new file mode 100644 index 0000000..e756478 --- /dev/null +++ b/src/lib/components/admin/FileUploadZone.svelte @@ -0,0 +1,313 @@ + + +
+
+ {#if compact} +
+ + + + + Add {multiple ? 'files' : 'file'} or drop {multiple ? 'them' : 'it'} here +
+ {:else} +
+ + + + + + + +
+

Drop {multiple ? 'files' : 'file'} here

+

or click to browse and select {multiple ? 'files' : 'file'}

+

+ {#if accept.includes('image/*')} + Supports JPG, PNG, GIF, WebP, and SVG files + {:else if accept.includes('video/*')} + Supports MP4, WebM, and other video formats + {:else} + Supports selected file types + {/if} +

+ {/if} +
+ + + + +
+ + \ No newline at end of file diff --git a/src/lib/components/admin/MediaGrid.svelte b/src/lib/components/admin/MediaGrid.svelte new file mode 100644 index 0000000..376fa94 --- /dev/null +++ b/src/lib/components/admin/MediaGrid.svelte @@ -0,0 +1,277 @@ + + +
+ {#if isLoading && media.length === 0} + +
+ {#each Array(12) as _, i} + + {/each} +
+ {:else if media.length === 0} +
+ + + + + +

{emptyMessage}

+
+ {:else} +
+ {#each media as item, i (item.id)} + + {/each} +
+ {/if} +
+ + \ No newline at end of file diff --git a/src/lib/components/admin/MediaMetadataPanel.svelte b/src/lib/components/admin/MediaMetadataPanel.svelte new file mode 100644 index 0000000..d174f61 --- /dev/null +++ b/src/lib/components/admin/MediaMetadataPanel.svelte @@ -0,0 +1,223 @@ + + + + + \ No newline at end of file diff --git a/src/lib/components/admin/MediaUsageList.svelte b/src/lib/components/admin/MediaUsageList.svelte new file mode 100644 index 0000000..c352c2b --- /dev/null +++ b/src/lib/components/admin/MediaUsageList.svelte @@ -0,0 +1,162 @@ + + +
+ {#if loading} +
+ + Loading usage information... +
+ {:else if usage.length > 0} +
    + {#each usage as usageItem} +
  • +
    +
    + {#if usageItem.contentUrl} + + {usageItem.contentTitle} + + {:else} + {usageItem.contentTitle} + {/if} + {usageItem.contentType} +
    +
    + {usageItem.fieldDisplayName} + Added {new Date(usageItem.createdAt).toLocaleDateString()} +
    +
    +
  • + {/each} +
+ {:else} +

{emptyMessage}

+ {/if} +
+ + \ No newline at end of file diff --git a/src/lib/utils/mediaHelpers.ts b/src/lib/utils/mediaHelpers.ts new file mode 100644 index 0000000..7ae4976 --- /dev/null +++ b/src/lib/utils/mediaHelpers.ts @@ -0,0 +1,94 @@ +import type { Media } from '@prisma/client' + +/** + * Format file size in human-readable format + */ +export function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 Bytes' + const k = 1024 + const sizes = ['Bytes', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +/** + * Get file type from MIME type + */ +export function getFileType(mimeType: string): string { + if (mimeType.startsWith('image/')) return 'Image' + if (mimeType.startsWith('video/')) return 'Video' + if (mimeType.startsWith('audio/')) return 'Audio' + if (mimeType.includes('pdf')) return 'PDF' + if (mimeType === 'image/svg+xml') return 'SVG' + return 'File' +} + +/** + * Check if a file is an image + */ +export function isImageFile(mimeType: string): boolean { + return mimeType.startsWith('image/') +} + +/** + * Check if a file is a video + */ +export function isVideoFile(mimeType: string): boolean { + return mimeType.startsWith('video/') +} + +/** + * Generate thumbnail URL for media + */ +export function generateThumbnailUrl(media: Media): string { + // For SVGs, use the original URL + if (media.mimeType === 'image/svg+xml') { + return media.url + } + // Use thumbnail URL if available, otherwise fallback to main URL + return media.thumbnailUrl || media.url +} + +/** + * Get file extension from filename + */ +export function getFileExtension(filename: string): string { + const parts = filename.split('.') + return parts.length > 1 ? parts[parts.length - 1].toLowerCase() : '' +} + +/** + * Validate if file type is accepted + */ +export function validateFileType(file: File, acceptedTypes: string[]): boolean { + // If no types specified, accept all + if (acceptedTypes.length === 0) return true + + // Check if file type matches any accepted type + return acceptedTypes.some(type => { + if (type === 'image/*') return file.type.startsWith('image/') + if (type === 'video/*') return file.type.startsWith('video/') + if (type === 'audio/*') return file.type.startsWith('audio/') + return file.type === type + }) +} + +/** + * Get display name for MIME type + */ +export function getMimeTypeDisplayName(mimeType: string): string { + const typeMap: Record = { + 'image/jpeg': 'JPEG Image', + 'image/png': 'PNG Image', + 'image/gif': 'GIF Image', + 'image/webp': 'WebP Image', + 'image/svg+xml': 'SVG Image', + 'video/mp4': 'MP4 Video', + 'video/webm': 'WebM Video', + 'audio/mpeg': 'MP3 Audio', + 'audio/wav': 'WAV Audio', + 'application/pdf': 'PDF Document' + } + + return typeMap[mimeType] || getFileType(mimeType) +} \ No newline at end of file