Fix adding/removing to albums
This commit is contained in:
parent
b2ad9efd9c
commit
9f7b408bc7
7 changed files with 267 additions and 262 deletions
|
|
@ -9,9 +9,10 @@
|
|||
|
||||
interface Props {
|
||||
label: string
|
||||
value?: Media[]
|
||||
onUpload: (media: Media[]) => void
|
||||
onReorder?: (media: Media[]) => void
|
||||
value?: any[] // Changed from Media[] to any[] to be more flexible
|
||||
onUpload: (media: any[]) => void
|
||||
onReorder?: (media: any[]) => void
|
||||
onRemove?: (item: any, index: number) => void // New callback for removals
|
||||
maxItems?: number
|
||||
allowAltText?: boolean
|
||||
required?: boolean
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
value = $bindable([]),
|
||||
onUpload,
|
||||
onReorder,
|
||||
onRemove,
|
||||
maxItems = 20,
|
||||
allowAltText = true,
|
||||
required = false,
|
||||
|
|
@ -151,7 +153,8 @@
|
|||
setTimeout(() => {
|
||||
const newValue = [...(value || []), ...uploadedMedia]
|
||||
value = newValue
|
||||
onUpload(newValue)
|
||||
// Only pass the newly uploaded media, not the entire gallery
|
||||
onUpload(uploadedMedia)
|
||||
isUploading = false
|
||||
uploadProgress = {}
|
||||
}, 500)
|
||||
|
|
@ -196,21 +199,36 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Remove individual image
|
||||
// Remove individual image - now passes the item to be removed instead of doing it locally
|
||||
function handleRemoveImage(index: number) {
|
||||
if (!value) return
|
||||
const newValue = value.filter((_, i) => i !== index)
|
||||
value = newValue
|
||||
onUpload(newValue)
|
||||
if (!value || !value[index]) return
|
||||
|
||||
const itemToRemove = value[index]
|
||||
// Call the onRemove callback if provided, otherwise fall back to onUpload
|
||||
if (onRemove) {
|
||||
onRemove(itemToRemove, index)
|
||||
} else {
|
||||
// Fallback: remove locally and call onUpload
|
||||
const newValue = value.filter((_, i) => i !== index)
|
||||
value = newValue
|
||||
onUpload(newValue)
|
||||
}
|
||||
uploadError = null
|
||||
}
|
||||
|
||||
// Update alt text on server
|
||||
async function handleAltTextChange(media: Media, newAltText: string) {
|
||||
if (!media) return
|
||||
async function handleAltTextChange(item: any, newAltText: string) {
|
||||
if (!item) return
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(`/api/media/${media.id}/metadata`, {
|
||||
// For album photos, use mediaId; for direct media objects, use id
|
||||
const mediaId = item.mediaId || item.id
|
||||
if (!mediaId) {
|
||||
console.error('No media ID found for alt text update')
|
||||
return
|
||||
}
|
||||
|
||||
const response = await authenticatedFetch(`/api/media/${mediaId}/metadata`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
|
@ -223,7 +241,7 @@
|
|||
if (response.ok) {
|
||||
const updatedData = await response.json()
|
||||
if (value) {
|
||||
const index = value.findIndex(m => m.id === media.id)
|
||||
const index = value.findIndex(v => (v.mediaId || v.id) === mediaId)
|
||||
if (index !== -1) {
|
||||
value[index] = { ...value[index], altText: updatedData.altText, updatedAt: updatedData.updatedAt }
|
||||
value = [...value]
|
||||
|
|
@ -290,18 +308,20 @@
|
|||
isMediaLibraryOpen = true
|
||||
}
|
||||
|
||||
function handleMediaSelect(selectedMedia: Media | Media[]) {
|
||||
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)
|
||||
const currentIds = value?.map(m => m.id) || []
|
||||
// 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))
|
||||
|
||||
if (newMedia.length > 0) {
|
||||
const updatedGallery = [...(value || []), ...newMedia]
|
||||
value = updatedGallery
|
||||
onUpload(updatedGallery)
|
||||
// Only pass the newly selected media, not the entire gallery
|
||||
onUpload(newMedia)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -445,7 +465,22 @@
|
|||
<!-- Image Preview -->
|
||||
<div class="image-preview">
|
||||
<SmartImage
|
||||
{media}
|
||||
media={{
|
||||
id: media.mediaId || media.id,
|
||||
filename: media.filename,
|
||||
originalName: media.originalName || media.filename,
|
||||
mimeType: media.mimeType || 'image/jpeg',
|
||||
size: media.size || 0,
|
||||
url: media.url,
|
||||
thumbnailUrl: media.thumbnailUrl,
|
||||
width: media.width,
|
||||
height: media.height,
|
||||
altText: media.altText,
|
||||
description: media.description,
|
||||
isPhotography: media.isPhotography || false,
|
||||
createdAt: media.createdAt,
|
||||
updatedAt: media.updatedAt
|
||||
}}
|
||||
alt={media.altText || media.filename || 'Gallery image'}
|
||||
containerWidth={300}
|
||||
loading="lazy"
|
||||
|
|
|
|||
|
|
@ -34,10 +34,6 @@
|
|||
let isMediaLibraryOpen = $state(false)
|
||||
let albumPhotos = $state<any[]>([])
|
||||
let isManagingPhotos = $state(false)
|
||||
let isUploading = $state(false)
|
||||
let uploadProgress = $state<Record<string, number>>({})
|
||||
let uploadErrors = $state<string[]>([])
|
||||
let fileInput: HTMLInputElement
|
||||
|
||||
// Media details modal state
|
||||
let isMediaDetailsOpen = $state(false)
|
||||
|
|
@ -189,14 +185,32 @@
|
|||
|
||||
try {
|
||||
isManagingPhotos = true
|
||||
error = '' // Clear any previous errors
|
||||
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) {
|
||||
goto('/admin/login')
|
||||
return
|
||||
}
|
||||
|
||||
// Check for duplicates before adding
|
||||
const existingMediaIds = albumPhotos.map((p) => p.mediaId).filter(Boolean)
|
||||
const newMedia = mediaArray.filter((media) => !existingMediaIds.includes(media.id))
|
||||
|
||||
if (newMedia.length === 0) {
|
||||
error = 'All selected photos are already in this album'
|
||||
return
|
||||
}
|
||||
|
||||
if (newMedia.length < mediaArray.length) {
|
||||
console.log(
|
||||
`Skipping ${mediaArray.length - newMedia.length} photos that are already in the album`
|
||||
)
|
||||
}
|
||||
|
||||
// Add photos to album via API
|
||||
for (const media of mediaArray) {
|
||||
const addedPhotos = []
|
||||
for (const media of newMedia) {
|
||||
const response = await fetch(`/api/albums/${album.id}/photos`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -205,22 +219,28 @@
|
|||
},
|
||||
body: JSON.stringify({
|
||||
mediaId: media.id,
|
||||
displayOrder: albumPhotos.length
|
||||
displayOrder: albumPhotos.length + addedPhotos.length
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to add photo ${media.filename}`)
|
||||
const errorData = await response.text()
|
||||
throw new Error(`Failed to add photo ${media.filename}: ${response.status} ${errorData}`)
|
||||
}
|
||||
|
||||
const photo = await response.json()
|
||||
albumPhotos = [...albumPhotos, photo]
|
||||
addedPhotos.push(photo)
|
||||
}
|
||||
|
||||
// Update local state with all added photos
|
||||
albumPhotos = [...albumPhotos, ...addedPhotos]
|
||||
|
||||
// Update album photo count
|
||||
if (album._count) {
|
||||
album._count.photos = albumPhotos.length
|
||||
}
|
||||
|
||||
console.log(`Successfully added ${addedPhotos.length} photos to album`)
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to add photos'
|
||||
console.error('Failed to add photos:', err)
|
||||
|
|
@ -230,38 +250,62 @@
|
|||
}
|
||||
}
|
||||
|
||||
async function handleRemovePhoto(photoId: number) {
|
||||
if (!confirm('Are you sure you want to remove this photo from the album?')) {
|
||||
return
|
||||
async function handleRemovePhoto(photoId: number, skipConfirmation = false) {
|
||||
const photoToRemove = albumPhotos.find((p) => p.id === photoId)
|
||||
if (!photoToRemove) {
|
||||
error = 'Photo not found in album'
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
!skipConfirmation &&
|
||||
!confirm(
|
||||
`Remove "${photoToRemove.filename || 'this photo'}" from this album?\n\nNote: This will only remove it from the album. The original photo will remain in your media library.`
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
isManagingPhotos = true
|
||||
error = '' // Clear any previous errors
|
||||
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) {
|
||||
goto('/admin/login')
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/photos/${photoId}`, {
|
||||
console.log(`Attempting to remove photo with ID: ${photoId} from album ${album.id}`)
|
||||
console.log('Photo to remove:', photoToRemove)
|
||||
|
||||
const response = await fetch(`/api/albums/${album.id}/photos?photoId=${photoId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Basic ${auth}` }
|
||||
})
|
||||
|
||||
console.log(`DELETE response status: ${response.status}`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to remove photo from album')
|
||||
const errorData = await response.text()
|
||||
console.error(`Delete failed: ${response.status} ${errorData}`)
|
||||
throw new Error(`Failed to remove photo: ${response.status} ${errorData}`)
|
||||
}
|
||||
|
||||
// Remove from local state
|
||||
// Remove from local state only after successful API call
|
||||
albumPhotos = albumPhotos.filter((photo) => photo.id !== photoId)
|
||||
|
||||
// Update album photo count
|
||||
if (album._count) {
|
||||
album._count.photos = albumPhotos.length
|
||||
}
|
||||
|
||||
console.log(`Successfully removed photo ${photoId} from album`)
|
||||
return true
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to remove photo'
|
||||
error = err instanceof Error ? err.message : 'Failed to remove photo from album'
|
||||
console.error('Failed to remove photo:', err)
|
||||
return false
|
||||
} finally {
|
||||
isManagingPhotos = false
|
||||
}
|
||||
|
|
@ -394,110 +438,56 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Direct upload functions
|
||||
function handleFileSelect(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const files = Array.from(target.files || [])
|
||||
if (files.length > 0) {
|
||||
uploadFilesToAlbum(files)
|
||||
}
|
||||
// Reset input so same files can be selected again
|
||||
target.value = ''
|
||||
}
|
||||
|
||||
async function uploadFilesToAlbum(files: File[]) {
|
||||
if (files.length === 0) return
|
||||
|
||||
isUploading = true
|
||||
uploadErrors = []
|
||||
uploadProgress = {}
|
||||
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) {
|
||||
goto('/admin/login')
|
||||
return
|
||||
}
|
||||
|
||||
// Filter for image files
|
||||
const imageFiles = files.filter((file) => file.type.startsWith('image/'))
|
||||
|
||||
if (imageFiles.length !== files.length) {
|
||||
uploadErrors = [
|
||||
...uploadErrors,
|
||||
`${files.length - imageFiles.length} non-image files were skipped`
|
||||
]
|
||||
}
|
||||
|
||||
// Handle new photos added through GalleryUploader (uploads or library selections)
|
||||
async function handleGalleryAdd(newPhotos: any[]) {
|
||||
try {
|
||||
// Upload each file and add to album
|
||||
for (const file of imageFiles) {
|
||||
try {
|
||||
// First upload the file to media library
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
// If this is a photography album, mark the uploaded media as photography
|
||||
if (isPhotography) {
|
||||
formData.append('isPhotography', 'true')
|
||||
}
|
||||
|
||||
const uploadResponse = await fetch('/api/media/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
const error = await uploadResponse.json()
|
||||
uploadErrors = [...uploadErrors, `${file.name}: ${error.message || 'Upload failed'}`]
|
||||
continue
|
||||
}
|
||||
|
||||
const media = await uploadResponse.json()
|
||||
uploadProgress = { ...uploadProgress, [file.name]: 50 }
|
||||
|
||||
// Then add the uploaded media to the album
|
||||
const addResponse = await fetch(`/api/albums/${album.id}/photos`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mediaId: media.id,
|
||||
displayOrder: albumPhotos.length
|
||||
})
|
||||
})
|
||||
|
||||
if (!addResponse.ok) {
|
||||
uploadErrors = [...uploadErrors, `${file.name}: Failed to add to album`]
|
||||
continue
|
||||
}
|
||||
|
||||
const photo = await addResponse.json()
|
||||
albumPhotos = [...albumPhotos, photo]
|
||||
uploadProgress = { ...uploadProgress, [file.name]: 100 }
|
||||
} catch (err) {
|
||||
uploadErrors = [...uploadErrors, `${file.name}: Network error`]
|
||||
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))
|
||||
|
||||
// Handle new uploads
|
||||
if (uploadsToAdd.length > 0) {
|
||||
await handleAddPhotosFromUpload(uploadsToAdd)
|
||||
}
|
||||
|
||||
// Handle library selections
|
||||
if (libraryPhotosToAdd.length > 0) {
|
||||
await handleAddPhotos(libraryPhotosToAdd)
|
||||
}
|
||||
}
|
||||
|
||||
// Update album photo count
|
||||
if (album._count) {
|
||||
album._count.photos = albumPhotos.length
|
||||
}
|
||||
} finally {
|
||||
isUploading = false
|
||||
// Clear progress after a delay
|
||||
setTimeout(() => {
|
||||
uploadProgress = {}
|
||||
uploadErrors = []
|
||||
}, 3000)
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to add photos'
|
||||
console.error('Failed to add photos:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle photo removal from GalleryUploader
|
||||
async function handleGalleryRemove(itemToRemove: any, index: number) {
|
||||
try {
|
||||
// Find the photo ID to remove
|
||||
const photoId = itemToRemove.id
|
||||
if (!photoId) {
|
||||
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) {
|
||||
// If removal failed, we need to reset the gallery state
|
||||
// Force a reactivity update
|
||||
albumPhotos = [...albumPhotos]
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to remove photo'
|
||||
console.error('Failed to remove photo:', err)
|
||||
// Reset gallery state on error
|
||||
albumPhotos = [...albumPhotos]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function generateSlug(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
|
|
@ -512,7 +502,6 @@
|
|||
}
|
||||
})
|
||||
|
||||
|
||||
const canSave = $derived(title.trim().length > 0 && slug.trim().length > 0)
|
||||
</script>
|
||||
|
||||
|
|
@ -670,70 +659,14 @@
|
|||
|
||||
<!-- Photo Management -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h2>Photos ({albumPhotos.length})</h2>
|
||||
<div class="photo-actions">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => fileInput.click()}
|
||||
disabled={isManagingPhotos || isUploading}
|
||||
>
|
||||
<svg
|
||||
slot="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21 15V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V15M17 8L12 3M12 3L7 8M12 3V15"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
{isUploading ? 'Uploading...' : 'Upload from Computer'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => (isMediaLibraryOpen = true)}
|
||||
disabled={isManagingPhotos || isUploading}
|
||||
>
|
||||
<svg
|
||||
slot="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21 21L15 15L21 21ZM3 9C3 8.17157 3.67157 7.5 4.5 7.5H19.5C20.3284 7.5 21 8.17157 21 9V18C21 18.8284 20.3284 19.5 19.5 19.5H4.5C3.67157 19.5 3 18.8284 3 18V9Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9 13.5L12 10.5L15 13.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Add from Library
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Photos ({albumPhotos.length})</h2>
|
||||
|
||||
<GalleryUploader
|
||||
label="Album Photos"
|
||||
bind:value={albumPhotos}
|
||||
onUpload={handleAddPhotosFromUpload}
|
||||
onUpload={handleGalleryAdd}
|
||||
onReorder={handlePhotoReorder}
|
||||
onRemove={handleGalleryRemove}
|
||||
showBrowseLibrary={true}
|
||||
placeholder="Add photos to this album by uploading or selecting from your media library"
|
||||
helpText="Drag photos to reorder them. Click on photos to edit metadata."
|
||||
|
|
@ -811,7 +744,6 @@
|
|||
gap: $unit-2x;
|
||||
}
|
||||
|
||||
|
||||
.btn-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
|
@ -872,7 +804,6 @@
|
|||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: $grey-10;
|
||||
border-bottom: 1px solid $grey-85;
|
||||
padding-bottom: $unit-2x;
|
||||
}
|
||||
|
||||
|
|
@ -1141,18 +1072,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Photo actions styles
|
||||
.photo-actions {
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
justify-content: center;
|
||||
margin-top: $unit-3x;
|
||||
}
|
||||
|
||||
// Upload status styles
|
||||
.upload-status {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
const response = await fetch('/api/albums', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Basic ${auth}`,
|
||||
Authorization: `Basic ${auth}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(albumData)
|
||||
|
|
@ -81,10 +81,9 @@
|
|||
}
|
||||
|
||||
const album = await response.json()
|
||||
|
||||
|
||||
// Redirect to album edit page or albums list
|
||||
goto(`/admin/albums/${album.id}/edit`)
|
||||
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to create album'
|
||||
console.error('Failed to create album:', err)
|
||||
|
|
@ -97,7 +96,6 @@
|
|||
goto('/admin/albums')
|
||||
}
|
||||
|
||||
|
||||
const canSave = $derived(title.trim().length > 0 && slug.trim().length > 0)
|
||||
</script>
|
||||
|
||||
|
|
@ -115,46 +113,14 @@
|
|||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<h1>New Album</h1>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="publish-dropdown">
|
||||
<Button
|
||||
variant="primary"
|
||||
buttonSize="large"
|
||||
onclick={() => handleSave('published')}
|
||||
disabled={!canSave || isSaving}
|
||||
>
|
||||
{isSaving ? 'Publishing...' : 'Publish'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
iconOnly
|
||||
buttonSize="large"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation()
|
||||
isPublishDropdownOpen = !isPublishDropdownOpen
|
||||
}}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<svg slot="icon" width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M3 4.5L6 7.5L9 4.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
{#if isPublishDropdownOpen}
|
||||
<DropdownMenuContainer>
|
||||
<DropdownItem onclick={() => {handleSave('draft'); isPublishDropdownOpen = false}}>
|
||||
Save as Draft
|
||||
</DropdownItem>
|
||||
</DropdownMenuContainer>
|
||||
{/if}
|
||||
</div>
|
||||
<PublishDropdown
|
||||
onPublish={() => handleSave('published')}
|
||||
onSaveDraft={() => handleSave('draft')}
|
||||
disabled={!canSave || isSaving}
|
||||
isLoading={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -165,7 +131,7 @@
|
|||
|
||||
<div class="form-section">
|
||||
<h2>Album Details</h2>
|
||||
|
||||
|
||||
<Input
|
||||
label="Title"
|
||||
bind:value={title}
|
||||
|
|
@ -329,7 +295,6 @@
|
|||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: $grey-10;
|
||||
border-bottom: 1px solid $grey-85;
|
||||
padding-bottom: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
|
@ -414,4 +379,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -47,19 +47,20 @@ export const GET: RequestHandler = async (event) => {
|
|||
}
|
||||
})
|
||||
|
||||
// Enrich photos with media information
|
||||
// Enrich photos with media information using proper media usage tracking
|
||||
const photosWithMedia = album.photos.map(photo => {
|
||||
// Try to find matching media by filename since we don't have direct relationship
|
||||
const media = Array.from(mediaMap.values()).find(m => m.filename === photo.filename)
|
||||
// Find the corresponding media usage record for this photo
|
||||
const usage = mediaUsages.find(u => u.media && u.media.filename === photo.filename)
|
||||
const media = usage?.media
|
||||
|
||||
return {
|
||||
...photo,
|
||||
mediaId: media?.id,
|
||||
altText: media?.altText,
|
||||
description: media?.description,
|
||||
isPhotography: media?.isPhotography,
|
||||
mimeType: media?.mimeType,
|
||||
size: media?.size
|
||||
mediaId: media?.id || null,
|
||||
altText: media?.altText || '',
|
||||
description: media?.description || photo.caption || '',
|
||||
isPhotography: media?.isPhotography || false,
|
||||
mimeType: media?.mimeType || 'image/jpeg',
|
||||
size: media?.size || 0
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -161,4 +161,89 @@ export const PUT: RequestHandler = async (event) => {
|
|||
logger.error('Failed to update photo order', error as Error)
|
||||
return errorResponse('Failed to update photo order', 500)
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/albums/[id]/photos - Remove a photo from an album (without deleting the media)
|
||||
export const DELETE: RequestHandler = async (event) => {
|
||||
// Check authentication
|
||||
if (!checkAdminAuth(event)) {
|
||||
return errorResponse('Unauthorized', 401)
|
||||
}
|
||||
|
||||
const albumId = parseInt(event.params.id)
|
||||
if (isNaN(albumId)) {
|
||||
return errorResponse('Invalid album ID', 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(event.request.url)
|
||||
const photoId = url.searchParams.get('photoId')
|
||||
|
||||
logger.info('DELETE photo request', { albumId, photoId })
|
||||
|
||||
if (!photoId || isNaN(parseInt(photoId))) {
|
||||
return errorResponse('Photo ID is required as query parameter', 400)
|
||||
}
|
||||
|
||||
const photoIdNum = parseInt(photoId)
|
||||
|
||||
// Check if album exists
|
||||
const album = await prisma.album.findUnique({
|
||||
where: { id: albumId }
|
||||
})
|
||||
|
||||
if (!album) {
|
||||
logger.error('Album not found', { albumId })
|
||||
return errorResponse('Album not found', 404)
|
||||
}
|
||||
|
||||
// Check if photo exists in this album
|
||||
const photo = await prisma.photo.findFirst({
|
||||
where: {
|
||||
id: photoIdNum,
|
||||
albumId: albumId // Ensure photo belongs to this album
|
||||
}
|
||||
})
|
||||
|
||||
logger.info('Photo lookup result', { photoIdNum, albumId, found: !!photo })
|
||||
|
||||
if (!photo) {
|
||||
logger.error('Photo not found in album', { photoIdNum, albumId })
|
||||
return errorResponse('Photo not found in this album', 404)
|
||||
}
|
||||
|
||||
// Find and remove the specific media usage record for this photo
|
||||
// We need to find the media ID associated with this photo to remove the correct usage record
|
||||
const mediaUsage = await prisma.mediaUsage.findFirst({
|
||||
where: {
|
||||
contentType: 'album',
|
||||
contentId: albumId,
|
||||
fieldName: 'photos',
|
||||
media: {
|
||||
filename: photo.filename // Match by filename since that's how they're linked
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (mediaUsage) {
|
||||
await prisma.mediaUsage.delete({
|
||||
where: { id: mediaUsage.id }
|
||||
})
|
||||
}
|
||||
|
||||
// Delete the photo record (this removes it from the album but keeps the media)
|
||||
await prisma.photo.delete({
|
||||
where: { id: photoIdNum }
|
||||
})
|
||||
|
||||
logger.info('Photo removed from album', {
|
||||
photoId: photoIdNum,
|
||||
albumId: albumId
|
||||
})
|
||||
|
||||
return new Response(null, { status: 204 })
|
||||
} catch (error) {
|
||||
logger.error('Failed to remove photo from album', error as Error)
|
||||
return errorResponse('Failed to remove photo from album', 500)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,8 +20,7 @@ export const GET: RequestHandler = async (event) => {
|
|||
include: {
|
||||
photos: {
|
||||
where: {
|
||||
status: 'published',
|
||||
showInPhotos: true
|
||||
status: 'published'
|
||||
},
|
||||
orderBy: { displayOrder: 'asc' },
|
||||
select: {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ export const GET: RequestHandler = async (event) => {
|
|||
}
|
||||
}
|
||||
|
||||
// DELETE /api/photos/[id] - Delete a photo (remove from album)
|
||||
// DELETE /api/photos/[id] - Delete a photo completely (removes photo record and media usage)
|
||||
// NOTE: This deletes the photo entirely. Use DELETE /api/albums/[id]/photos to remove from album only.
|
||||
export const DELETE: RequestHandler = async (event) => {
|
||||
// Check authentication
|
||||
if (!checkAdminAuth(event)) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue