Updates to MediaUploadModal

This commit is contained in:
Justin Edmund 2025-06-13 12:43:56 -04:00
parent 824e44a1ef
commit 3096c0ff51
6 changed files with 420 additions and 241 deletions

View file

@ -58,13 +58,15 @@ $mention-padding: $unit-3x;
$font-stack: 'Circular Std', 'Helvetica Neue', Helvetica, Arial, sans-serif;
$font-unit: 14px;
$font-unit: 18px;
$font-unit-mobile: 16px;
$font-size-small: 0.7rem; // 10
$font-size: 1rem; // 14
$font-size-med: 1.25rem; // 16
$font-size-large: 1.4rem; // 18
$font-size-xlarge: 1.65rem; // 22
$font-size-extra-small: 0.75rem; // 12
$font-size-small: 0.875rem; // 14
$font-size: 1rem; // 18
$font-size-med: 1.25rem; // 20
$font-size-large: 1.4rem; // 22
$font-size-xlarge: 1.65rem; // 26
$font-weight: 400;
$font-weight-med: 500;

View file

@ -97,7 +97,7 @@
}
.artist-name {
font-size: $font-size-small;
font-size: $font-size-extra-small;
font-weight: $font-weight-med;
color: $grey-40;
}

View file

@ -104,7 +104,7 @@
}
.game-playtime {
font-size: $font-size-small;
font-size: $font-size-extra-small;
font-weight: $font-weight-med;
color: $grey-40;
}

View file

@ -1,7 +1,6 @@
<script lang="ts">
import Modal from './Modal.svelte'
import Button from './Button.svelte'
import LoadingSpinner from './LoadingSpinner.svelte'
interface Props {
isOpen: boolean
@ -158,12 +157,76 @@
<div class="modal-header">
<h2>Upload Media</h2>
</div>
<!-- Drop Zone -->
<div class="modal-inner-content">
<!-- File List (shown above drop zone when files are selected) -->
{#if files.length > 0}
<div class="files">
{#each files as file, index}
<div class="file-item">
<div class="file-preview">
{#if file.type.startsWith('image/')}
<img src={URL.createObjectURL(file)} alt={file.name} />
{:else}
<div class="file-icon">📄</div>
{/if}
</div>
<div class="file-info">
<div class="file-name">{file.name}</div>
<div class="file-size">{formatFileSize(file.size)}</div>
{#if isUploading}
<div class="progress-bar">
<div
class="progress-fill"
style="width: {uploadProgress[file.name] || 0}%"
></div>
</div>
<div class="upload-status">
{#if uploadProgress[file.name] === 100}
<span class="status-complete">✓ Complete</span>
{:else if uploadProgress[file.name] > 0}
<span class="status-uploading"
>{Math.round(uploadProgress[file.name] || 0)}%</span
>
{:else}
<span class="status-waiting">Waiting...</span>
{/if}
</div>
{/if}
</div>
{#if !isUploading}
<button
type="button"
class="remove-button"
onclick={() => removeFile(index)}
title="Remove file"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
{/if}
</div>
{/each}
</div>
{/if}
<!-- Drop Zone (compact when files are selected) -->
<div
class="drop-zone"
class:active={dragActive}
class:has-files={files.length > 0}
class:compact={files.length > 0}
ondragover={handleDragOver}
ondragleave={handleDragLeave}
ondrop={handleDrop}
@ -225,9 +288,35 @@
<p>or click to browse and select files</p>
<p class="upload-hint">Supports JPG, PNG, GIF, WebP, and SVG files</p>
{:else}
<div class="file-count">
<strong>{files.length} file{files.length !== 1 ? 's' : ''} selected</strong>
<p>Drop more files to add them, or click to browse</p>
<div class="compact-content">
<svg
class="add-icon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12"
y1="5"
x2="12"
y2="19"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
<line
x1="5"
y1="12"
x2="19"
y2="12"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
<span>Add more files or drop them here</span>
</div>
{/if}
</div>
@ -250,108 +339,55 @@
{dragActive ? 'Drop files' : 'Click to browse'}
</button>
</div>
</div>
<!-- File List -->
{#if files.length > 0}
<div class="file-list">
<div class="file-list-header">
<h3>Files to Upload</h3>
<div class="file-actions">
<Button
variant="secondary"
buttonSize="small"
onclick={clearAll}
disabled={isUploading}
>
Clear All
</Button>
<Button
variant="primary"
buttonSize="small"
onclick={uploadFiles}
disabled={isUploading || files.length === 0}
>
{#if isUploading}
<LoadingSpinner buttonSize="small" />
Uploading...
{:else}
Upload {files.length} File{files.length !== 1 ? 's' : ''}
{/if}
</Button>
</div>
</div>
<div class="files">
{#each files as file, index}
<div class="file-item">
<div class="file-preview">
{#if file.type.startsWith('image/')}
<img src={URL.createObjectURL(file)} alt={file.name} />
{:else}
<div class="file-icon">📄</div>
{/if}
</div>
<div class="file-info">
<div class="file-name">{file.name}</div>
<div class="file-size">{formatFileSize(file.size)}</div>
{#if uploadProgress[file.name]}
<div class="progress-bar">
<div class="progress-fill" style="width: {uploadProgress[file.name]}%"></div>
</div>
{/if}
</div>
{#if !isUploading}
<button
type="button"
class="remove-button"
onclick={() => removeFile(index)}
title="Remove file"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<!-- Upload Results -->
{#if successCount > 0 || uploadErrors.length > 0}
<div class="upload-results">
{#if successCount > 0}
<div class="success-message">
✅ Successfully uploaded {successCount} file{successCount !== 1 ? 's' : ''}
{#if successCount === files.length && uploadErrors.length === 0}
<br /><small>Closing modal...</small>
{/if}
</div>
{/each}
{/if}
{#if uploadErrors.length > 0}
<div class="error-messages">
<h4>Upload Errors:</h4>
{#each uploadErrors as error}
<div class="error-item">{error}</div>
{/each}
</div>
{/if}
</div>
</div>
{/if}
{/if}
</div>
<!-- Upload Results -->
{#if successCount > 0 || uploadErrors.length > 0}
<div class="upload-results">
{#if successCount > 0}
<div class="success-message">
✅ Successfully uploaded {successCount} file{successCount !== 1 ? 's' : ''}
{#if successCount === files.length && uploadErrors.length === 0}
<br /><small>Closing modal...</small>
{/if}
</div>
{/if}
{#if uploadErrors.length > 0}
<div class="error-messages">
<h4>Upload Errors:</h4>
{#each uploadErrors as error}
<div class="error-item">{error}</div>
{/each}
</div>
{/if}
</div>
{/if}
<!-- Modal Footer with actions -->
<div class="modal-footer">
<Button
variant="secondary"
buttonSize="medium"
onclick={clearAll}
disabled={isUploading || files.length === 0}
>
Clear all
</Button>
<Button
variant="primary"
buttonSize="medium"
onclick={uploadFiles}
disabled={isUploading || files.length === 0}
loading={isUploading}
>
{isUploading
? 'Uploading...'
: files.length > 0
? `Upload ${files.length} file${files.length !== 1 ? 's' : ''}`
: 'Upload files'}
</Button>
</div>
</div>
</Modal>
@ -359,8 +395,8 @@
.upload-modal-content {
display: flex;
flex-direction: column;
// height: 70vh;
max-height: 70vh;
overflow-y: auto;
}
.modal-header {
@ -378,6 +414,17 @@
.modal-inner-content {
padding: $unit $unit-3x $unit-3x;
flex: 1;
overflow-y: auto;
}
.modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: $unit-3x;
border-top: 1px solid $grey-85;
background: $grey-95;
}
.drop-zone {
@ -398,10 +445,37 @@
padding: $unit-4x;
}
&.compact {
padding: $unit-3x;
min-height: auto;
.drop-zone-content {
.compact-content {
display: flex;
align-items: center;
justify-content: center;
gap: $unit-2x;
color: $grey-40;
font-size: 0.875rem;
.add-icon {
color: $grey-50;
}
}
}
}
&:hover {
border-color: $grey-60;
background: $grey-90;
}
&.uploading {
border-color: #3b82f6;
border-style: solid;
background: rgba(59, 130, 246, 0.02);
pointer-events: none;
}
}
.drop-zone-content {
@ -455,45 +529,20 @@
}
}
.file-list {
background: white;
border: 1px solid $grey-85;
border-radius: $unit-2x;
padding: $unit-3x;
}
.file-list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: $unit-3x;
padding-bottom: $unit-2x;
border-bottom: 1px solid $grey-85;
h3 {
margin: 0;
color: $grey-20;
}
.file-actions {
display: flex;
gap: $unit-2x;
}
}
.files {
display: flex;
flex-direction: column;
gap: $unit-2x;
gap: $unit;
margin-bottom: $unit-3x;
}
.file-item {
display: flex;
align-items: center;
gap: $unit-3x;
padding: $unit-2x;
gap: $unit-2x;
padding: $unit;
background: $grey-95;
border-radius: $unit;
border-radius: $image-corner-radius;
border: 1px solid $grey-85;
}
@ -537,15 +586,59 @@
.progress-bar {
width: 100%;
height: 4px;
background: $grey-85;
border-radius: 2px;
height: 6px;
background: $grey-90;
border-radius: 3px;
overflow: hidden;
margin-bottom: $unit-half;
.progress-fill {
height: 100%;
background: #3b82f6;
transition: width 0.3s ease;
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(
90deg,
transparent 30%,
rgba(255, 255, 255, 0.2) 50%,
transparent 70%
);
animation: shimmer 1.5s infinite;
}
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.upload-status {
font-size: 0.75rem;
font-weight: 500;
.status-complete {
color: #16a34a;
}
.status-uploading {
color: #3b82f6;
}
.status-waiting {
color: $grey-50;
}
}
@ -608,11 +701,5 @@
align-items: flex-start;
gap: $unit-2x;
}
.file-list-header {
flex-direction: column;
align-items: flex-start;
gap: $unit-2x;
}
}
</style>

View file

@ -2,7 +2,6 @@
import { goto } from '$app/navigation'
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import Button from '$lib/components/admin/Button.svelte'
import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte'
import { onMount } from 'svelte'
let files = $state<File[]>([])
@ -147,11 +146,102 @@
</header>
<div class="upload-container">
<!-- File List -->
{#if files.length > 0}
<div class="file-list">
<div class="file-list-header">
<h3>Files to Upload</h3>
<div class="file-actions">
<Button
variant="primary"
buttonSize="small"
onclick={uploadFiles}
disabled={isUploading || files.length === 0}
loading={isUploading}
>
{isUploading ? 'Uploading...' : `Upload ${files.length} File${files.length !== 1 ? 's' : ''}`}
</Button>
<Button
variant="ghost"
buttonSize="icon"
onclick={clearAll}
disabled={isUploading}
title="Clear all files"
>
<svg slot="icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="8" y1="8" x2="16" y2="16"></line>
<line x1="16" y1="8" x2="8" y2="16"></line>
</svg>
</Button>
</div>
</div>
<div class="files">
{#each files as file, index}
<div class="file-item">
<div class="file-preview">
{#if file.type.startsWith('image/')}
<img src={URL.createObjectURL(file)} alt={file.name} />
{:else}
<div class="file-icon">📄</div>
{/if}
</div>
<div class="file-info">
<div class="file-name">{file.name}</div>
<div class="file-size">{formatFileSize(file.size)}</div>
{#if isUploading}
<div class="progress-bar">
<div class="progress-fill" style="width: {uploadProgress[file.name] || 0}%"></div>
</div>
<div class="upload-status">
{#if uploadProgress[file.name] === 100}
<span class="status-complete">✓ Complete</span>
{:else if uploadProgress[file.name] > 0}
<span class="status-uploading">{Math.round(uploadProgress[file.name] || 0)}%</span>
{:else}
<span class="status-waiting">Waiting...</span>
{/if}
</div>
{/if}
</div>
{#if !isUploading}
<button
type="button"
class="remove-button"
onclick={() => removeFile(index)}
title="Remove file"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
{/if}
</div>
{/each}
</div>
</div>
{/if}
<!-- Drop Zone -->
<div
class="drop-zone"
class:active={dragActive}
class:has-files={files.length > 0}
class:compact={files.length > 0}
class:uploading={isUploading}
ondragover={handleDragOver}
ondragleave={handleDragLeave}
ondrop={handleDrop}
@ -213,9 +303,19 @@
<p>or click to browse and select files</p>
<p class="upload-hint">Supports JPG, PNG, GIF, WebP, and SVG files</p>
{:else}
<div class="file-count">
<strong>{files.length} file{files.length !== 1 ? 's' : ''} selected</strong>
<p>Drop more files to add them, or click to browse</p>
<div class="compact-content">
<svg
class="add-icon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line x1="12" y1="5" x2="12" y2="19" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
<span>Add more files or drop them here</span>
</div>
{/if}
</div>
@ -239,84 +339,6 @@
</button>
</div>
<!-- File List -->
{#if files.length > 0}
<div class="file-list">
<div class="file-list-header">
<h3>Files to Upload</h3>
<div class="file-actions">
<Button
variant="secondary"
buttonSize="small"
onclick={clearAll}
disabled={isUploading}
>
Clear All
</Button>
<Button
variant="primary"
buttonSize="small"
onclick={uploadFiles}
disabled={isUploading || files.length === 0}
>
{#if isUploading}
<LoadingSpinner buttonSize="small" />
Uploading...
{:else}
Upload {files.length} File{files.length !== 1 ? 's' : ''}
{/if}
</Button>
</div>
</div>
<div class="files">
{#each files as file, index}
<div class="file-item">
<div class="file-preview">
{#if file.type.startsWith('image/')}
<img src={URL.createObjectURL(file)} alt={file.name} />
{:else}
<div class="file-icon">📄</div>
{/if}
</div>
<div class="file-info">
<div class="file-name">{file.name}</div>
<div class="file-size">{formatFileSize(file.size)}</div>
{#if uploadProgress[file.name]}
<div class="progress-bar">
<div class="progress-fill" style="width: {uploadProgress[file.name]}%"></div>
</div>
{/if}
</div>
{#if !isUploading}
<button
type="button"
class="remove-button"
onclick={() => removeFile(index)}
title="Remove file"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
{/if}
</div>
{/each}
</div>
</div>
{/if}
<!-- Upload Results -->
{#if successCount > 0 || uploadErrors.length > 0}
<div class="upload-results">
@ -372,11 +394,38 @@
&.has-files {
padding: $unit-4x;
}
&.compact {
padding: $unit-3x;
min-height: auto;
.drop-zone-content {
.compact-content {
display: flex;
align-items: center;
justify-content: center;
gap: $unit-2x;
color: $grey-40;
font-size: 0.875rem;
.add-icon {
color: $grey-50;
}
}
}
}
&:hover {
border-color: $grey-60;
background: $grey-90;
}
&.uploading {
border-color: #3b82f6;
border-style: solid;
background: rgba(59, 130, 246, 0.02);
pointer-events: none;
}
}
.drop-zone-content {
@ -435,7 +484,7 @@
border: 1px solid $grey-85;
border-radius: $unit-2x;
padding: $unit-3x;
margin-bottom: $unit-4x;
margin-bottom: $unit-3x;
}
.file-list-header {
@ -454,6 +503,7 @@
.file-actions {
display: flex;
gap: $unit-2x;
align-items: center;
}
}
@ -513,15 +563,59 @@
.progress-bar {
width: 100%;
height: 4px;
background: $grey-85;
border-radius: 2px;
height: 6px;
background: $grey-90;
border-radius: 3px;
overflow: hidden;
margin-bottom: $unit-half;
.progress-fill {
height: 100%;
background: #3b82f6;
transition: width 0.3s ease;
position: relative;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background: linear-gradient(
90deg,
transparent 30%,
rgba(255, 255, 255, 0.2) 50%,
transparent 70%
);
animation: shimmer 1.5s infinite;
}
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.upload-status {
font-size: 0.75rem;
font-weight: 500;
.status-complete {
color: #16a34a;
}
.status-uploading {
color: #3b82f6;
}
.status-waiting {
color: $grey-50;
}
}

View file

@ -111,7 +111,6 @@ export const POST: RequestHandler = async (event) => {
const formData = await event.request.formData()
const file = formData.get('file') as File
const context = (formData.get('context') as string) || 'media'
const altText = (formData.get('altText') as string) || null
const description = (formData.get('description') as string) || null
const isPhotography = formData.get('isPhotography') === 'true'
@ -163,10 +162,8 @@ export const POST: RequestHandler = async (event) => {
width: uploadResult.width,
height: uploadResult.height,
exifData: exifData,
altText: altText?.trim() || null,
description: description?.trim() || null,
isPhotography: isPhotography,
usedIn: []
isPhotography: isPhotography
}
})
@ -187,7 +184,6 @@ export const POST: RequestHandler = async (event) => {
originalName: media.originalName,
mimeType: media.mimeType,
size: media.size,
altText: media.altText,
description: media.description,
createdAt: media.createdAt,
updatedAt: media.updatedAt