jedmund-svelte/src/lib/components/admin/MediaUploadModal.svelte
Justin Edmund aa3622d606 feat: add video playback UI and thumbnail display
- Add video player with controls in MediaDetailsModal
- Display video metadata (duration, codecs, bitrate) in metadata panel
- Show video thumbnails with play icon overlay in MediaGrid
- Support video preview in upload components
- Replace emoji icons with SVG play icon
- Maintain natural video aspect ratio in all views
- Add proper styling for video thumbnails and placeholders

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-23 03:14:06 -04:00

287 lines
6.1 KiB
Svelte

<script lang="ts">
import Modal from './Modal.svelte'
import Button from './Button.svelte'
import FileUploadZone from './FileUploadZone.svelte'
import FilePreviewList from './FilePreviewList.svelte'
import { formatFileSize } from '$lib/utils/mediaHelpers'
interface Props {
isOpen: boolean
onClose: () => void
onUploadComplete: () => void
}
let { isOpen = $bindable(), onClose, onUploadComplete }: Props = $props()
let files = $state<File[]>([])
let dragActive = $state(false)
let isUploading = $state(false)
let uploadProgress = $state<Record<string, number>>({})
let uploadErrors = $state<string[]>([])
let successCount = $state(0)
// Reset state when modal opens/closes
$effect(() => {
if (!isOpen) {
files = []
dragActive = false
isUploading = false
uploadProgress = {}
uploadErrors = []
successCount = 0
}
})
function handleFilesAdded(newFiles: File[]) {
addFiles(newFiles)
}
function addFiles(newFiles: File[]) {
// Filter for supported file types (images and videos)
const supportedFiles = newFiles.filter(
(file) => file.type.startsWith('image/') || file.type.startsWith('video/')
)
if (supportedFiles.length !== newFiles.length) {
uploadErrors = [
...uploadErrors,
`${newFiles.length - supportedFiles.length} unsupported files were skipped`
]
}
files = [...files, ...supportedFiles]
}
function removeFile(id: string | number) {
// For files, the id is the filename
const fileToRemove = files.find((f) => f.name === id)
if (fileToRemove) {
files = files.filter((f) => f.name !== id)
// Clear any related upload progress
if (uploadProgress[fileToRemove.name]) {
const { [fileToRemove.name]: removed, ...rest } = uploadProgress
uploadProgress = rest
}
}
}
async function uploadFiles() {
if (files.length === 0) return
isUploading = true
uploadErrors = []
successCount = 0
uploadProgress = {}
const auth = localStorage.getItem('admin_auth')
if (!auth) {
uploadErrors = ['Authentication required']
isUploading = false
return
}
// Upload files individually to show progress
for (const file of files) {
try {
const formData = new FormData()
formData.append('file', file)
const response = await fetch('/api/media/upload', {
method: 'POST',
headers: {
Authorization: `Basic ${auth}`
},
body: formData
})
if (!response.ok) {
const error = await response.json()
uploadErrors = [...uploadErrors, `${file.name}: ${error.message || 'Upload failed'}`]
} else {
successCount++
uploadProgress = { ...uploadProgress, [file.name]: 100 }
}
} catch (error) {
uploadErrors = [...uploadErrors, `${file.name}: Network error`]
}
}
isUploading = false
// If all uploads succeeded, close modal and refresh media list
if (successCount === files.length && uploadErrors.length === 0) {
setTimeout(() => {
onUploadComplete()
onClose()
}, 1500)
}
}
function clearAll() {
files = []
uploadProgress = {}
uploadErrors = []
successCount = 0
}
function handleClose() {
if (!isUploading) {
onClose()
}
}
</script>
<Modal bind:isOpen on:close={handleClose} size="large">
<div class="upload-modal-content">
<div class="modal-header">
<h2>Upload Media</h2>
</div>
<div class="modal-inner-content">
<!-- File List (shown above drop zone when files are selected) -->
{#if files.length > 0}
<FilePreviewList
{files}
onRemove={removeFile}
{uploadProgress}
{isUploading}
variant="upload"
/>
{/if}
<!-- Drop Zone (compact when files are selected) -->
<FileUploadZone
onFilesAdded={handleFilesAdded}
accept={['image/*', 'video/*']}
multiple={true}
compact={files.length > 0}
disabled={isUploading}
{dragActive}
/>
<!-- Upload Results -->
{#if successCount > 0}
<div class="upload-results">
<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>
</div>
{/if}
<!-- Error messages are now handled in FilePreviewList -->
</div>
<!-- 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>
<style lang="scss">
.upload-modal-content {
display: flex;
flex-direction: column;
// height: 70vh;
max-height: 70vh;
}
.modal-header {
display: flex;
flex-direction: row;
padding: $unit-2x $unit-3x $unit $unit-3x;
h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: $gray-10;
}
}
.modal-inner-content {
padding: $unit $unit-3x $unit-3x;
display: flex;
flex-direction: column;
gap: $unit-2x;
flex: 1;
overflow-y: auto;
}
.modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: $unit-3x;
border-top: 1px solid $gray-85;
background: $gray-95;
}
.upload-results {
background: white;
border: 1px solid $gray-85;
border-radius: $unit-2x;
padding: $unit-3x;
.success-message {
color: #16a34a;
margin-bottom: $unit-2x;
small {
color: $gray-50;
}
}
.error-messages {
h4 {
color: $red-60;
margin-bottom: $unit-2x;
}
.error-item {
color: $red-60;
margin-bottom: $unit;
font-size: 0.925rem;
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.upload-modal-content {
max-height: 80vh;
}
.drop-zone {
padding: $unit-4x $unit-2x;
}
.file-item {
flex-direction: column;
align-items: flex-start;
gap: $unit-2x;
}
}
</style>