Updates to MediaUploadModal
This commit is contained in:
parent
824e44a1ef
commit
3096c0ff51
6 changed files with 420 additions and 241 deletions
|
|
@ -58,13 +58,15 @@ $mention-padding: $unit-3x;
|
||||||
|
|
||||||
$font-stack: 'Circular Std', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
$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-extra-small: 0.75rem; // 12
|
||||||
$font-size: 1rem; // 14
|
$font-size-small: 0.875rem; // 14
|
||||||
$font-size-med: 1.25rem; // 16
|
$font-size: 1rem; // 18
|
||||||
$font-size-large: 1.4rem; // 18
|
$font-size-med: 1.25rem; // 20
|
||||||
$font-size-xlarge: 1.65rem; // 22
|
$font-size-large: 1.4rem; // 22
|
||||||
|
$font-size-xlarge: 1.65rem; // 26
|
||||||
|
|
||||||
$font-weight: 400;
|
$font-weight: 400;
|
||||||
$font-weight-med: 500;
|
$font-weight-med: 500;
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.artist-name {
|
.artist-name {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-extra-small;
|
||||||
font-weight: $font-weight-med;
|
font-weight: $font-weight-med;
|
||||||
color: $grey-40;
|
color: $grey-40;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-playtime {
|
.game-playtime {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-extra-small;
|
||||||
font-weight: $font-weight-med;
|
font-weight: $font-weight-med;
|
||||||
color: $grey-40;
|
color: $grey-40;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Modal from './Modal.svelte'
|
import Modal from './Modal.svelte'
|
||||||
import Button from './Button.svelte'
|
import Button from './Button.svelte'
|
||||||
import LoadingSpinner from './LoadingSpinner.svelte'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
|
@ -158,12 +157,76 @@
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2>Upload Media</h2>
|
<h2>Upload Media</h2>
|
||||||
</div>
|
</div>
|
||||||
<!-- Drop Zone -->
|
|
||||||
<div class="modal-inner-content">
|
<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
|
<div
|
||||||
class="drop-zone"
|
class="drop-zone"
|
||||||
class:active={dragActive}
|
class:active={dragActive}
|
||||||
class:has-files={files.length > 0}
|
class:has-files={files.length > 0}
|
||||||
|
class:compact={files.length > 0}
|
||||||
ondragover={handleDragOver}
|
ondragover={handleDragOver}
|
||||||
ondragleave={handleDragLeave}
|
ondragleave={handleDragLeave}
|
||||||
ondrop={handleDrop}
|
ondrop={handleDrop}
|
||||||
|
|
@ -225,9 +288,35 @@
|
||||||
<p>or click to browse and select files</p>
|
<p>or click to browse and select files</p>
|
||||||
<p class="upload-hint">Supports JPG, PNG, GIF, WebP, and SVG files</p>
|
<p class="upload-hint">Supports JPG, PNG, GIF, WebP, and SVG files</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="file-count">
|
<div class="compact-content">
|
||||||
<strong>{files.length} file{files.length !== 1 ? 's' : ''} selected</strong>
|
<svg
|
||||||
<p>Drop more files to add them, or click to browse</p>
|
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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -250,108 +339,55 @@
|
||||||
{dragActive ? 'Drop files' : 'Click to browse'}
|
{dragActive ? 'Drop files' : 'Click to browse'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- File List -->
|
<!-- Upload Results -->
|
||||||
{#if files.length > 0}
|
{#if successCount > 0 || uploadErrors.length > 0}
|
||||||
<div class="file-list">
|
<div class="upload-results">
|
||||||
<div class="file-list-header">
|
{#if successCount > 0}
|
||||||
<h3>Files to Upload</h3>
|
<div class="success-message">
|
||||||
<div class="file-actions">
|
✅ Successfully uploaded {successCount} file{successCount !== 1 ? 's' : ''}
|
||||||
<Button
|
{#if successCount === files.length && uploadErrors.length === 0}
|
||||||
variant="secondary"
|
<br /><small>Closing modal...</small>
|
||||||
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}
|
{/if}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
<!-- Upload Results -->
|
<!-- Modal Footer with actions -->
|
||||||
{#if successCount > 0 || uploadErrors.length > 0}
|
<div class="modal-footer">
|
||||||
<div class="upload-results">
|
<Button
|
||||||
{#if successCount > 0}
|
variant="secondary"
|
||||||
<div class="success-message">
|
buttonSize="medium"
|
||||||
✅ Successfully uploaded {successCount} file{successCount !== 1 ? 's' : ''}
|
onclick={clearAll}
|
||||||
{#if successCount === files.length && uploadErrors.length === 0}
|
disabled={isUploading || files.length === 0}
|
||||||
<br /><small>Closing modal...</small>
|
>
|
||||||
{/if}
|
Clear all
|
||||||
</div>
|
</Button>
|
||||||
{/if}
|
<Button
|
||||||
|
variant="primary"
|
||||||
{#if uploadErrors.length > 0}
|
buttonSize="medium"
|
||||||
<div class="error-messages">
|
onclick={uploadFiles}
|
||||||
<h4>Upload Errors:</h4>
|
disabled={isUploading || files.length === 0}
|
||||||
{#each uploadErrors as error}
|
loading={isUploading}
|
||||||
<div class="error-item">❌ {error}</div>
|
>
|
||||||
{/each}
|
{isUploading
|
||||||
</div>
|
? 'Uploading...'
|
||||||
{/if}
|
: files.length > 0
|
||||||
</div>
|
? `Upload ${files.length} file${files.length !== 1 ? 's' : ''}`
|
||||||
{/if}
|
: 'Upload files'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|
@ -359,8 +395,8 @@
|
||||||
.upload-modal-content {
|
.upload-modal-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
// height: 70vh;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
|
|
@ -378,6 +414,17 @@
|
||||||
|
|
||||||
.modal-inner-content {
|
.modal-inner-content {
|
||||||
padding: $unit $unit-3x $unit-3x;
|
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 {
|
.drop-zone {
|
||||||
|
|
@ -398,10 +445,37 @@
|
||||||
padding: $unit-4x;
|
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 {
|
&:hover {
|
||||||
border-color: $grey-60;
|
border-color: $grey-60;
|
||||||
background: $grey-90;
|
background: $grey-90;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.uploading {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
border-style: solid;
|
||||||
|
background: rgba(59, 130, 246, 0.02);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-zone-content {
|
.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 {
|
.files {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $unit-2x;
|
gap: $unit;
|
||||||
|
margin-bottom: $unit-3x;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-item {
|
.file-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $unit-3x;
|
gap: $unit-2x;
|
||||||
padding: $unit-2x;
|
padding: $unit;
|
||||||
background: $grey-95;
|
background: $grey-95;
|
||||||
border-radius: $unit;
|
border-radius: $image-corner-radius;
|
||||||
border: 1px solid $grey-85;
|
border: 1px solid $grey-85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -537,15 +586,59 @@
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 4px;
|
height: 6px;
|
||||||
background: $grey-85;
|
background: $grey-90;
|
||||||
border-radius: 2px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin-bottom: $unit-half;
|
||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #3b82f6;
|
background: #3b82f6;
|
||||||
transition: width 0.3s ease;
|
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;
|
align-items: flex-start;
|
||||||
gap: $unit-2x;
|
gap: $unit-2x;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: $unit-2x;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||||
import Button from '$lib/components/admin/Button.svelte'
|
import Button from '$lib/components/admin/Button.svelte'
|
||||||
import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte'
|
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
let files = $state<File[]>([])
|
let files = $state<File[]>([])
|
||||||
|
|
@ -147,11 +146,102 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="upload-container">
|
<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 -->
|
<!-- Drop Zone -->
|
||||||
<div
|
<div
|
||||||
class="drop-zone"
|
class="drop-zone"
|
||||||
class:active={dragActive}
|
class:active={dragActive}
|
||||||
class:has-files={files.length > 0}
|
class:has-files={files.length > 0}
|
||||||
|
class:compact={files.length > 0}
|
||||||
|
class:uploading={isUploading}
|
||||||
ondragover={handleDragOver}
|
ondragover={handleDragOver}
|
||||||
ondragleave={handleDragLeave}
|
ondragleave={handleDragLeave}
|
||||||
ondrop={handleDrop}
|
ondrop={handleDrop}
|
||||||
|
|
@ -213,9 +303,19 @@
|
||||||
<p>or click to browse and select files</p>
|
<p>or click to browse and select files</p>
|
||||||
<p class="upload-hint">Supports JPG, PNG, GIF, WebP, and SVG files</p>
|
<p class="upload-hint">Supports JPG, PNG, GIF, WebP, and SVG files</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="file-count">
|
<div class="compact-content">
|
||||||
<strong>{files.length} file{files.length !== 1 ? 's' : ''} selected</strong>
|
<svg
|
||||||
<p>Drop more files to add them, or click to browse</p>
|
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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -239,84 +339,6 @@
|
||||||
</button>
|
</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>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Upload Results -->
|
<!-- Upload Results -->
|
||||||
{#if successCount > 0 || uploadErrors.length > 0}
|
{#if successCount > 0 || uploadErrors.length > 0}
|
||||||
<div class="upload-results">
|
<div class="upload-results">
|
||||||
|
|
@ -373,10 +395,37 @@
|
||||||
padding: $unit-4x;
|
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 {
|
&:hover {
|
||||||
border-color: $grey-60;
|
border-color: $grey-60;
|
||||||
background: $grey-90;
|
background: $grey-90;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.uploading {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
border-style: solid;
|
||||||
|
background: rgba(59, 130, 246, 0.02);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.drop-zone-content {
|
.drop-zone-content {
|
||||||
|
|
@ -435,7 +484,7 @@
|
||||||
border: 1px solid $grey-85;
|
border: 1px solid $grey-85;
|
||||||
border-radius: $unit-2x;
|
border-radius: $unit-2x;
|
||||||
padding: $unit-3x;
|
padding: $unit-3x;
|
||||||
margin-bottom: $unit-4x;
|
margin-bottom: $unit-3x;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-header {
|
.file-list-header {
|
||||||
|
|
@ -454,6 +503,7 @@
|
||||||
.file-actions {
|
.file-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $unit-2x;
|
gap: $unit-2x;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -513,15 +563,59 @@
|
||||||
|
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 4px;
|
height: 6px;
|
||||||
background: $grey-85;
|
background: $grey-90;
|
||||||
border-radius: 2px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
margin-bottom: $unit-half;
|
||||||
|
|
||||||
.progress-fill {
|
.progress-fill {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #3b82f6;
|
background: #3b82f6;
|
||||||
transition: width 0.3s ease;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,6 @@ export const POST: RequestHandler = async (event) => {
|
||||||
const formData = await event.request.formData()
|
const formData = await event.request.formData()
|
||||||
const file = formData.get('file') as File
|
const file = formData.get('file') as File
|
||||||
const context = (formData.get('context') as string) || 'media'
|
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 description = (formData.get('description') as string) || null
|
||||||
const isPhotography = formData.get('isPhotography') === 'true'
|
const isPhotography = formData.get('isPhotography') === 'true'
|
||||||
|
|
||||||
|
|
@ -163,10 +162,8 @@ export const POST: RequestHandler = async (event) => {
|
||||||
width: uploadResult.width,
|
width: uploadResult.width,
|
||||||
height: uploadResult.height,
|
height: uploadResult.height,
|
||||||
exifData: exifData,
|
exifData: exifData,
|
||||||
altText: altText?.trim() || null,
|
|
||||||
description: description?.trim() || null,
|
description: description?.trim() || null,
|
||||||
isPhotography: isPhotography,
|
isPhotography: isPhotography
|
||||||
usedIn: []
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -187,7 +184,6 @@ export const POST: RequestHandler = async (event) => {
|
||||||
originalName: media.originalName,
|
originalName: media.originalName,
|
||||||
mimeType: media.mimeType,
|
mimeType: media.mimeType,
|
||||||
size: media.size,
|
size: media.size,
|
||||||
altText: media.altText,
|
|
||||||
description: media.description,
|
description: media.description,
|
||||||
createdAt: media.createdAt,
|
createdAt: media.createdAt,
|
||||||
updatedAt: media.updatedAt
|
updatedAt: media.updatedAt
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue