diff --git a/src/lib/components/admin/AlbumForm.svelte b/src/lib/components/admin/AlbumForm.svelte index 9af4d65..2296b26 100644 --- a/src/lib/components/admin/AlbumForm.svelte +++ b/src/lib/components/admin/AlbumForm.svelte @@ -1,197 +1,216 @@
- -

📸 {mode === 'create' ? 'New Album' : 'Edit Album'}

+
+
+ (activeTab = value)} + />
- {#if mode === 'create'} - - - - {:else} - - + {#if !isLoading} + {/if}
-
- {#if error} -
- {error} -
- {/if} +
+ {#if isLoading} +
Loading album...
+ {:else} + {#if error} +
{error}
+ {/if} -
-
- +
+ +
+ +
+ - -
+ -
- -
+
+ + +
+
-
-
- - +
+ +
+ + + {#if mode === 'edit'} +
+
+

+ Photos {albumMedia.length > 0 ? `(${albumMedia.length})` : ''} +

+ +
+ {#if albumMedia.length > 0} +
+ {#each albumMedia as item} +
+ +
+ {/each} +
+ {:else} +

+ No photos added yet. Click "Manage Photos" to add photos to this album. +

+ {/if} +
+ {/if} +
+ + +
+
- -
- -
-
+ {/if}
+ +{#if album && mode === 'edit'} + +{/if} + diff --git a/src/lib/components/admin/AlbumListItem.svelte b/src/lib/components/admin/AlbumListItem.svelte index a1ab5fa..a62050b 100644 --- a/src/lib/components/admin/AlbumListItem.svelte +++ b/src/lib/components/admin/AlbumListItem.svelte @@ -18,15 +18,15 @@ date: string | null location: string | null coverPhotoId: number | null - isPhotography: boolean status: string showInUniverse: boolean publishedAt: string | null createdAt: string updatedAt: string photos: Photo[] + content?: any _count: { - photos: number + media: number } } @@ -105,7 +105,7 @@ } function getPhotoCount(): number { - return album._count?.photos || 0 + return album._count?.media || 0 } @@ -135,9 +135,10 @@

{album.title}

+ import { onMount } from 'svelte' + import Button from './Button.svelte' + import Input from './Input.svelte' + import LoadingSpinner from './LoadingSpinner.svelte' + + interface Album { + id: number + title: string + slug: string + _count?: { + media: number + } + } + + interface Props { + mediaId: number + currentAlbums: Album[] + onUpdate: (albums: Album[]) => void + onClose: () => void + } + + let { mediaId, currentAlbums = [], onUpdate, onClose }: Props = $props() + + // State + let albums = $state([]) + let filteredAlbums = $state([]) + let selectedAlbumIds = $state>(new Set(currentAlbums.map((a) => a.id))) + let isLoading = $state(true) + let isSaving = $state(false) + let error = $state('') + let searchQuery = $state('') + let showCreateNew = $state(false) + let newAlbumTitle = $state('') + let newAlbumSlug = $state('') + + onMount(() => { + loadAlbums() + }) + + $effect(() => { + if (searchQuery) { + filteredAlbums = albums.filter((album) => + album.title.toLowerCase().includes(searchQuery.toLowerCase()) + ) + } else { + filteredAlbums = albums + } + }) + + $effect(() => { + if (newAlbumTitle) { + // Auto-generate slug from title + newAlbumSlug = newAlbumTitle + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + } + }) + + async function loadAlbums() { + try { + isLoading = true + const auth = localStorage.getItem('admin_auth') + if (!auth) return + + const response = await fetch('/api/albums', { + headers: { Authorization: `Basic ${auth}` } + }) + + if (!response.ok) { + throw new Error('Failed to load albums') + } + + const data = await response.json() + albums = data.albums || [] + filteredAlbums = albums + } catch (err) { + console.error('Failed to load albums:', err) + error = 'Failed to load albums' + } finally { + isLoading = false + } + } + + function toggleAlbum(albumId: number) { + if (selectedAlbumIds.has(albumId)) { + selectedAlbumIds.delete(albumId) + } else { + selectedAlbumIds.add(albumId) + } + selectedAlbumIds = new Set(selectedAlbumIds) + } + + async function createNewAlbum() { + if (!newAlbumTitle.trim() || !newAlbumSlug.trim()) return + + try { + isSaving = true + error = '' + const auth = localStorage.getItem('admin_auth') + if (!auth) return + + const response = await fetch('/api/albums', { + method: 'POST', + headers: { + Authorization: `Basic ${auth}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + title: newAlbumTitle.trim(), + slug: newAlbumSlug.trim(), + isPhotography: true, + status: 'draft' + }) + }) + + if (!response.ok) { + const errorData = await response.json() + throw new Error(errorData.message || 'Failed to create album') + } + + const newAlbum = await response.json() + + // Add to albums list and select it + albums = [newAlbum, ...albums] + selectedAlbumIds.add(newAlbum.id) + selectedAlbumIds = new Set(selectedAlbumIds) + + // Reset form + showCreateNew = false + newAlbumTitle = '' + newAlbumSlug = '' + searchQuery = '' + } catch (err) { + error = err instanceof Error ? err.message : 'Failed to create album' + } finally { + isSaving = false + } + } + + async function handleSave() { + try { + isSaving = true + error = '' + const auth = localStorage.getItem('admin_auth') + if (!auth) return + + // Get the list of albums to add/remove + const currentAlbumIds = new Set(currentAlbums.map((a) => a.id)) + const albumsToAdd = Array.from(selectedAlbumIds).filter((id) => !currentAlbumIds.has(id)) + const albumsToRemove = currentAlbums + .filter((a) => !selectedAlbumIds.has(a.id)) + .map((a) => a.id) + + // Add to new albums + for (const albumId of albumsToAdd) { + const response = await fetch(`/api/albums/${albumId}/media`, { + method: 'POST', + headers: { + Authorization: `Basic ${auth}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ mediaIds: [mediaId] }) + }) + + if (!response.ok) { + throw new Error('Failed to add to album') + } + } + + // Remove from albums + for (const albumId of albumsToRemove) { + const response = await fetch(`/api/albums/${albumId}/media`, { + method: 'DELETE', + headers: { + Authorization: `Basic ${auth}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ mediaIds: [mediaId] }) + }) + + if (!response.ok) { + throw new Error('Failed to remove from album') + } + } + + // Get updated album list + const updatedAlbums = albums.filter((a) => selectedAlbumIds.has(a.id)) + onUpdate(updatedAlbums) + onClose() + } catch (err) { + console.error('Failed to update albums:', err) + error = 'Failed to update albums' + } finally { + isSaving = false + } + } + + // Computed + const hasChanges = $derived(() => { + const currentIds = new Set(currentAlbums.map((a) => a.id)) + if (currentIds.size !== selectedAlbumIds.size) return true + for (const id of selectedAlbumIds) { + if (!currentIds.has(id)) return true + } + return false + }) + + +
+
+

Manage Albums

+
+ + {#if error} +
{error}
+ {/if} + +
+ {#if !showCreateNew} +
+ + +
+ + {#if isLoading} +
+ +

Loading albums...

+
+ {:else if filteredAlbums.length === 0} +
+

{searchQuery ? 'No albums found' : 'No albums available'}

+
+ {:else} +
+ {#each filteredAlbums as album} + + {/each} +
+ {/if} + {:else} +
+

Create New Album

+ + +
+ + +
+
+ {/if} +
+ + {#if !showCreateNew} + + {/if} +
+ + diff --git a/src/routes/admin/albums/+page.svelte b/src/routes/admin/albums/+page.svelte index 3875520..e6177f2 100644 --- a/src/routes/admin/albums/+page.svelte +++ b/src/routes/admin/albums/+page.svelte @@ -24,15 +24,15 @@ date: string | null location: string | null coverPhotoId: number | null - isPhotography: boolean status: string showInUniverse: boolean publishedAt: string | null createdAt: string updatedAt: string photos: Photo[] + content?: any _count: { - photos: number + media: number } } @@ -48,14 +48,14 @@ let activeDropdown = $state(null) // Filter state - let photographyFilter = $state('all') + let statusFilter = $state('all') let sortBy = $state('newest') // Filter options const filterOptions = [ { value: 'all', label: 'All albums' }, - { value: 'true', label: 'Photography albums' }, - { value: 'false', label: 'Regular albums' } + { value: 'published', label: 'Published' }, + { value: 'draft', label: 'Drafts' } ] const sortOptions = [ @@ -107,11 +107,11 @@ albums = data.albums || [] total = data.pagination?.total || albums.length - // Calculate album type counts + // Calculate album status counts const counts: Record = { all: albums.length, - photography: albums.filter((a) => a.isPhotography).length, - regular: albums.filter((a) => !a.isPhotography).length + published: albums.filter((a) => a.status === 'published').length, + draft: albums.filter((a) => a.status === 'draft').length } albumTypeCounts = counts @@ -129,10 +129,10 @@ let filtered = [...albums] // Apply filter - if (photographyFilter === 'true') { - filtered = filtered.filter((album) => album.isPhotography === true) - } else if (photographyFilter === 'false') { - filtered = filtered.filter((album) => album.isPhotography === false) + if (statusFilter === 'published') { + filtered = filtered.filter((album) => album.status === 'published') + } else if (statusFilter === 'draft') { + filtered = filtered.filter((album) => album.status === 'draft') } // Apply sorting @@ -289,7 +289,7 @@ {#snippet left()} - - -
- - -
-

Photos ({albumPhotos.length})

- - -
- - -
-

Album Statistics

-
-
- {album._count?.photos || 0} - Photos -
-
- {status === 'published' ? 'Published' : 'Draft'} - Status -
-
- {new Date(album.createdAt).toLocaleDateString()} - Created -
-
-
-
- {/if} - - - - - - - - - - +{#if isLoading} +
Loading album...
+{:else if error} +
{error}
+{:else if !album} +
Album not found
+{:else} + +{/if} diff --git a/src/routes/admin/albums/new/+page.svelte b/src/routes/admin/albums/new/+page.svelte index 423a76c..baf9358 100644 --- a/src/routes/admin/albums/new/+page.svelte +++ b/src/routes/admin/albums/new/+page.svelte @@ -1,498 +1,9 @@ New Album - Admin @jedmund - -
-
- -
-
- - -
-
- -
- {#if error} -
{error}
- {/if} - -
-

Album Details

- - - - -
- - -
-

Photos ({albumPhotos.length})

- - -
-
-
- - - - - - - - +