([])
- 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)
@@ -670,70 +659,14 @@
-
+
Photos ({albumPhotos.length})
0 && slug.trim().length > 0)
@@ -115,46 +113,14 @@
/>
- New Album
@@ -165,7 +131,7 @@
Album Details
-
+
\ No newline at end of file
+
diff --git a/src/routes/api/albums/[id]/+server.ts b/src/routes/api/albums/[id]/+server.ts
index 551915c..91e75e3 100644
--- a/src/routes/api/albums/[id]/+server.ts
+++ b/src/routes/api/albums/[id]/+server.ts
@@ -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
}
})
diff --git a/src/routes/api/albums/[id]/photos/+server.ts b/src/routes/api/albums/[id]/photos/+server.ts
index e78e12d..f971627 100644
--- a/src/routes/api/albums/[id]/photos/+server.ts
+++ b/src/routes/api/albums/[id]/photos/+server.ts
@@ -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)
+ }
}
\ No newline at end of file
diff --git a/src/routes/api/photos/+server.ts b/src/routes/api/photos/+server.ts
index ff62950..92b7334 100644
--- a/src/routes/api/photos/+server.ts
+++ b/src/routes/api/photos/+server.ts
@@ -20,8 +20,7 @@ export const GET: RequestHandler = async (event) => {
include: {
photos: {
where: {
- status: 'published',
- showInPhotos: true
+ status: 'published'
},
orderBy: { displayOrder: 'asc' },
select: {
diff --git a/src/routes/api/photos/[id]/+server.ts b/src/routes/api/photos/[id]/+server.ts
index 45ebf4a..23a6980 100644
--- a/src/routes/api/photos/[id]/+server.ts
+++ b/src/routes/api/photos/[id]/+server.ts
@@ -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)) {