diff --git a/src/lib/components/admin/AlbumMetadataPopover.svelte b/src/lib/components/admin/AlbumMetadataPopover.svelte
new file mode 100644
index 0000000..772edcc
--- /dev/null
+++ b/src/lib/components/admin/AlbumMetadataPopover.svelte
@@ -0,0 +1,101 @@
+
+
+
\ No newline at end of file
diff --git a/src/lib/components/admin/Button.svelte b/src/lib/components/admin/Button.svelte
index 2a9fde4..ce346a4 100644
--- a/src/lib/components/admin/Button.svelte
+++ b/src/lib/components/admin/Button.svelte
@@ -2,7 +2,7 @@
import type { HTMLButtonAttributes } from 'svelte/elements'
interface Props extends HTMLButtonAttributes {
- variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'text' | 'overlay'
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'text' | 'overlay' | 'danger-text'
buttonSize?: 'small' | 'medium' | 'large' | 'icon'
iconOnly?: boolean
iconPosition?: 'left' | 'right'
@@ -343,6 +343,23 @@
}
}
+ .btn-danger-text {
+ background: none;
+ color: #dc2626;
+ padding: $unit;
+ font-weight: 600;
+
+ &:hover:not(:disabled) {
+ background-color: $grey-90;
+ color: #dc2626;
+ }
+
+ &:active:not(:disabled) {
+ background-color: $grey-80;
+ color: #dc2626;
+ }
+ }
+
.btn-overlay {
background-color: white;
color: $grey-20;
diff --git a/src/lib/components/admin/GalleryUploader.svelte b/src/lib/components/admin/GalleryUploader.svelte
index 6f320ee..136706f 100644
--- a/src/lib/components/admin/GalleryUploader.svelte
+++ b/src/lib/components/admin/GalleryUploader.svelte
@@ -5,7 +5,6 @@
import SmartImage from '../SmartImage.svelte'
import MediaLibraryModal from './MediaLibraryModal.svelte'
import { authenticatedFetch } from '$lib/admin-auth'
- import RefreshIcon from '$icons/refresh.svg?component'
interface Props {
label: string
@@ -103,7 +102,7 @@
for (let i = 0; i < files.length; i++) {
const file = files[i]
const validationError = validateFile(file)
-
+
if (validationError) {
errors.push(`${file.name}: ${validationError}`)
} else if (filesToUpload.length < remainingSlots) {
@@ -126,10 +125,10 @@
try {
// Initialize progress tracking
const progressKeys = filesToUpload.map((file, index) => `${file.name}-${index}`)
- uploadProgress = Object.fromEntries(progressKeys.map(key => [key, 0]))
+ uploadProgress = Object.fromEntries(progressKeys.map((key) => [key, 0]))
// Simulate progress for user feedback
- const progressIntervals = progressKeys.map(key => {
+ const progressIntervals = progressKeys.map((key) => {
return setInterval(() => {
if (uploadProgress[key] < 90) {
uploadProgress[key] += Math.random() * 10
@@ -139,16 +138,16 @@
})
const uploadedMedia = await uploadFiles(filesToUpload)
-
+
// Clear progress intervals
- progressIntervals.forEach(interval => clearInterval(interval))
-
+ progressIntervals.forEach((interval) => clearInterval(interval))
+
// Complete progress
- progressKeys.forEach(key => {
+ progressKeys.forEach((key) => {
uploadProgress[key] = 100
})
uploadProgress = { ...uploadProgress }
-
+
// Brief delay to show completion
setTimeout(() => {
const newValue = [...(value || []), ...uploadedMedia]
@@ -158,7 +157,6 @@
isUploading = false
uploadProgress = {}
}, 500)
-
} catch (err) {
isUploading = false
uploadProgress = {}
@@ -180,7 +178,7 @@
function handleDrop(event: DragEvent) {
event.preventDefault()
isDragOver = false
-
+
const files = event.dataTransfer?.files
if (files) {
handleFiles(files)
@@ -202,7 +200,7 @@
// Remove individual image - now passes the item to be removed instead of doing it locally
function handleRemoveImage(index: number) {
if (!value || !value[index]) return
-
+
const itemToRemove = value[index]
// Call the onRemove callback if provided, otherwise fall back to onUpload
if (onRemove) {
@@ -219,7 +217,7 @@
// Update alt text on server
async function handleAltTextChange(item: any, newAltText: string) {
if (!item) return
-
+
try {
// For album photos, use mediaId; for direct media objects, use id
const mediaId = item.mediaId || item.id
@@ -227,7 +225,7 @@
console.error('No media ID found for alt text update')
return
}
-
+
const response = await authenticatedFetch(`/api/media/${mediaId}/metadata`, {
method: 'PATCH',
headers: {
@@ -241,9 +239,13 @@
if (response.ok) {
const updatedData = await response.json()
if (value) {
- const index = value.findIndex(v => (v.mediaId || v.id) === mediaId)
+ const index = value.findIndex((v) => (v.mediaId || v.id) === mediaId)
if (index !== -1) {
- value[index] = { ...value[index], altText: updatedData.altText, updatedAt: updatedData.updatedAt }
+ value[index] = {
+ ...value[index],
+ altText: updatedData.altText,
+ updatedAt: updatedData.updatedAt
+ }
value = [...value]
}
}
@@ -275,25 +277,25 @@
function handleImageDrop(event: DragEvent, dropIndex: number) {
event.preventDefault()
-
+
if (draggedIndex === null || !value) return
-
+
const newValue = [...value]
const draggedItem = newValue[draggedIndex]
-
+
// Remove from old position
newValue.splice(draggedIndex, 1)
-
+
// Insert at new position (adjust index if dragging to later position)
const adjustedDropIndex = draggedIndex < dropIndex ? dropIndex - 1 : dropIndex
newValue.splice(adjustedDropIndex, 0, draggedItem)
-
+
value = newValue
onUpload(newValue)
if (onReorder) {
onReorder(newValue)
}
-
+
draggedIndex = null
draggedOverIndex = null
}
@@ -311,12 +313,12 @@
function handleMediaSelect(selectedMedia: any | any[]) {
// For gallery mode, selectedMedia will be an array
const mediaArray = Array.isArray(selectedMedia) ? selectedMedia : [selectedMedia]
-
+
// Add selected media to existing gallery (avoid duplicates)
// Check both id and mediaId to handle different object types
- const currentIds = value?.map(m => m.mediaId || m.id) || []
- const newMedia = mediaArray.filter(media => !currentIds.includes(media.id))
-
+ const currentIds = value?.map((m) => m.mediaId || m.id) || []
+ const newMedia = mediaArray.filter((media) => !currentIds.includes(media.id))
+
if (newMedia.length > 0) {
const updatedGallery = [...(value || []), ...newMedia]
value = updatedGallery
@@ -331,21 +333,9 @@
-
-
-
- {#if helpText}
-
{helpText}
- {/if}
-
{#if !hasImages || (hasImages && canAddMore)}
-
Uploading images...
-
+
{#each Object.entries(uploadProgress) as [fileName, progress]}
@@ -398,12 +388,53 @@
{:else}
-
{/if}
@@ -439,7 +468,7 @@
{#if hasImages}
{#each value as media, index (media.id)}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
-
@@ -861,4 +920,4 @@
align-items: stretch;
}
}
-
\ No newline at end of file
+
diff --git a/src/lib/components/admin/GenericMetadataPopover.svelte b/src/lib/components/admin/GenericMetadataPopover.svelte
new file mode 100644
index 0000000..8e94ec7
--- /dev/null
+++ b/src/lib/components/admin/GenericMetadataPopover.svelte
@@ -0,0 +1,450 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/lib/components/admin/MetadataPopover.svelte b/src/lib/components/admin/MetadataPopover.svelte
index 1860bc3..8eb014d 100644
--- a/src/lib/components/admin/MetadataPopover.svelte
+++ b/src/lib/components/admin/MetadataPopover.svelte
@@ -64,9 +64,27 @@
left = viewportWidth - popoverRect.width - 16
}
- // Adjust if would go off-screen vertically
+ // Check if popover would go off-screen vertically (both top and bottom)
if (top + popoverRect.height > viewportHeight - 16) {
- top = triggerRect.top - popoverRect.height - 8
+ // Try positioning above the trigger
+ const topAbove = triggerRect.top - popoverRect.height - 8
+ if (topAbove >= 16) {
+ top = topAbove
+ } else {
+ // If neither above nor below works, position with maximum available space
+ if (triggerRect.top > viewportHeight - triggerRect.bottom) {
+ // More space above - position at top of viewport with margin
+ top = 16
+ } else {
+ // More space below - position at bottom of viewport with margin
+ top = viewportHeight - popoverRect.height - 16
+ }
+ }
+ }
+
+ // Also check if positioning below would place us off the top (shouldn't happen but be safe)
+ if (top < 16) {
+ top = 16
}
popoverElement.style.position = 'fixed'
diff --git a/src/lib/components/admin/PostMetadataPopover.svelte b/src/lib/components/admin/PostMetadataPopover.svelte
new file mode 100644
index 0000000..55c12e8
--- /dev/null
+++ b/src/lib/components/admin/PostMetadataPopover.svelte
@@ -0,0 +1,105 @@
+
+
+
\ No newline at end of file
diff --git a/src/routes/admin/albums/[id]/edit/+page.svelte b/src/routes/admin/albums/[id]/edit/+page.svelte
index b9e68c2..0e76306 100644
--- a/src/routes/admin/albums/[id]/edit/+page.svelte
+++ b/src/routes/admin/albums/[id]/edit/+page.svelte
@@ -7,11 +7,11 @@
import Input from '$lib/components/admin/Input.svelte'
import FormFieldWrapper from '$lib/components/admin/FormFieldWrapper.svelte'
import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte'
- import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
import MediaLibraryModal from '$lib/components/admin/MediaLibraryModal.svelte'
import MediaDetailsModal from '$lib/components/admin/MediaDetailsModal.svelte'
import GalleryUploader from '$lib/components/admin/GalleryUploader.svelte'
import SaveActionsGroup from '$lib/components/admin/SaveActionsGroup.svelte'
+ import AlbumMetadataPopover from '$lib/components/admin/AlbumMetadataPopover.svelte'
// Form state
let album = $state
(null)
@@ -28,7 +28,6 @@
let isLoading = $state(true)
let isSaving = $state(false)
let error = $state('')
- let isDeleteModalOpen = $state(false)
// Photo management state
let isMediaLibraryOpen = $state(false)
@@ -39,6 +38,10 @@
let isMediaDetailsOpen = $state(false)
let selectedMedia = $state(null)
+ // Metadata popover state
+ let isMetadataOpen = $state(false)
+ let metadataButtonElement: HTMLButtonElement
+
onMount(async () => {
await loadAlbum()
})
@@ -443,14 +446,14 @@
try {
if (newPhotos.length > 0) {
// Check if these are new uploads (have File objects) or library selections (have media IDs)
- const uploadsToAdd = newPhotos.filter(photo => photo instanceof File || !photo.id)
- const libraryPhotosToAdd = newPhotos.filter(photo => photo.id && !(photo instanceof File))
-
+ const uploadsToAdd = newPhotos.filter((photo) => photo instanceof File || !photo.id)
+ const libraryPhotosToAdd = newPhotos.filter((photo) => photo.id && !(photo instanceof File))
+
// Handle new uploads
if (uploadsToAdd.length > 0) {
await handleAddPhotosFromUpload(uploadsToAdd)
}
-
+
// Handle library selections
if (libraryPhotosToAdd.length > 0) {
await handleAddPhotos(libraryPhotosToAdd)
@@ -471,7 +474,7 @@
error = 'Cannot remove photo: no photo ID found'
return
}
-
+
// Call the existing remove photo function
const success = await handleRemovePhoto(photoId, true) // Skip confirmation since user clicked remove
if (!success) {
@@ -487,7 +490,6 @@
}
}
-
function generateSlug(text: string): string {
return text
.toLowerCase()
@@ -503,6 +505,34 @@
})
const canSave = $derived(title.trim().length > 0 && slug.trim().length > 0)
+
+ // Metadata popover handlers
+ function handleMetadataUpdate(key: string, value: any) {
+ if (key === 'date') {
+ date = value ? new Date(value).toISOString().split('T')[0] : ''
+ } else {
+ // Update the form state variable
+ switch (key) {
+ case 'slug':
+ slug = value
+ break
+ case 'location':
+ location = value
+ break
+ case 'isPhotography':
+ isPhotography = value
+ break
+ case 'showInUniverse':
+ showInUniverse = value
+ break
+ }
+ }
+ }
+
+ function handleMetadataDelete() {
+ isMetadataOpen = false
+ handleDelete()
+ }
@@ -522,23 +552,34 @@
-
-
-
-
-
-
-
-
-
-
-
@@ -695,16 +667,6 @@
{/if}
-
-
(isDeleteModalOpen = false)}
-/>
-
([])
+ let isManagingPhotos = $state(false)
+
+ // Media details modal state
+ let isMediaDetailsOpen = $state(false)
+ let selectedMedia = $state(null)
+
+ // Metadata popover state
+ let isMetadataOpen = $state(false)
+ let metadataButtonElement: HTMLButtonElement
+
// Auto-generate slug from title
$effect(() => {
if (title && !slug) {
@@ -96,6 +113,107 @@
goto('/admin/albums')
}
+ // Photo management functions (simplified for new album - no API calls yet)
+ function handleMediaLibraryClose() {
+ isMediaLibraryOpen = false
+ }
+
+ function handlePhotoClick(photo: any) {
+ // Convert album photo to media format for MediaDetailsModal
+ selectedMedia = {
+ id: photo.mediaId || photo.id,
+ filename: photo.filename,
+ originalName: photo.filename,
+ mimeType: photo.mimeType || 'image/jpeg',
+ size: photo.size || 0,
+ url: photo.url,
+ thumbnailUrl: photo.thumbnailUrl,
+ width: photo.width,
+ height: photo.height,
+ altText: photo.altText || '',
+ description: photo.description || '',
+ isPhotography: photo.isPhotography || false,
+ createdAt: photo.createdAt,
+ updatedAt: photo.updatedAt
+ }
+ isMediaDetailsOpen = true
+ }
+
+ function handleMediaDetailsClose() {
+ isMediaDetailsOpen = false
+ selectedMedia = null
+ }
+
+ function handleMediaUpdate(updatedMedia: any) {
+ // Update the photo in the album photos list
+ const photoIndex = albumPhotos.findIndex(
+ (photo) => (photo.mediaId || photo.id) === updatedMedia.id
+ )
+ if (photoIndex !== -1) {
+ albumPhotos[photoIndex] = {
+ ...albumPhotos[photoIndex],
+ filename: updatedMedia.filename,
+ altText: updatedMedia.altText,
+ description: updatedMedia.description,
+ isPhotography: updatedMedia.isPhotography
+ }
+ albumPhotos = [...albumPhotos] // Trigger reactivity
+ }
+ selectedMedia = updatedMedia
+ }
+
+ function handlePhotoReorder(reorderedPhotos: any[]) {
+ albumPhotos = reorderedPhotos
+ }
+
+ function handleGalleryAdd(newPhotos: any[]) {
+ if (newPhotos.length > 0) {
+ albumPhotos = [...albumPhotos, ...newPhotos]
+ }
+ }
+
+ function handleGalleryRemove(itemToRemove: any, index: number) {
+ albumPhotos = albumPhotos.filter((_, i) => i !== index)
+ }
+
+ // Metadata popover handlers
+ function handleMetadataUpdate(key: string, value: any) {
+ if (key === 'date') {
+ date = value ? new Date(value).toISOString().split('T')[0] : ''
+ } else {
+ // Update the form state variable
+ switch (key) {
+ case 'slug':
+ slug = value
+ break
+ case 'location':
+ location = value
+ break
+ case 'isPhotography':
+ isPhotography = value
+ break
+ case 'showInUniverse':
+ showInUniverse = value
+ break
+ }
+ }
+ }
+
+ // Mock album object for metadata popover
+ const mockAlbum = $derived({
+ id: null,
+ title,
+ slug,
+ description,
+ date: date ? new Date(date).toISOString() : null,
+ location,
+ isPhotography,
+ showInUniverse,
+ status,
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString()
+ })
+
const canSave = $derived(title.trim().length > 0 && slug.trim().length > 0)
@@ -115,11 +233,40 @@
@@ -141,15 +288,6 @@
fullWidth
/>
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/routes/admin/posts/[id]/edit/+page.svelte b/src/routes/admin/posts/[id]/edit/+page.svelte
index ba6077b..cb6e93f 100644
--- a/src/routes/admin/posts/[id]/edit/+page.svelte
+++ b/src/routes/admin/posts/[id]/edit/+page.svelte
@@ -5,7 +5,7 @@
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import Editor from '$lib/components/admin/Editor.svelte'
import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte'
- import MetadataPopover from '$lib/components/admin/MetadataPopover.svelte'
+ import PostMetadataPopover from '$lib/components/admin/PostMetadataPopover.svelte'
import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
import Button from '$lib/components/admin/Button.svelte'
import SaveActionsGroup from '$lib/components/admin/SaveActionsGroup.svelte'
@@ -229,7 +229,7 @@
{#if showMetadata && metadataButtonRef}
-
{#if showMetadata && metadataButtonRef}
-