From 6132c17a9bd4e15ec5913134e638b3612f3e41eb Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 16 Jun 2025 09:58:07 +0100 Subject: [PATCH] Add better masonry and infinite scrolling --- package-lock.json | 18 +++ package.json | 2 + src/lib/components/MasonryPhotoGrid.svelte | 71 +++++++++ src/routes/api/photos/+server.ts | 80 ++++++---- src/routes/photos/+page.svelte | 163 ++++++++++++++++++++- src/routes/photos/+page.ts | 2 +- 6 files changed, 304 insertions(+), 32 deletions(-) create mode 100644 src/lib/components/MasonryPhotoGrid.svelte diff --git a/package-lock.json b/package-lock.json index 65cb842..bc0c4aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,8 @@ "redis": "^4.7.0", "sharp": "^0.34.2", "steamapi": "^3.0.11", + "svelte-bricks": "^0.3.2", + "svelte-infinite": "^0.5.0", "svelte-medium-image-zoom": "^0.2.6", "svelte-portal": "^2.2.1", "svelte-tiptap": "^2.1.0", @@ -7809,6 +7811,14 @@ "@types/estree": "^1.0.1" } }, + "node_modules/svelte-bricks": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/svelte-bricks/-/svelte-bricks-0.3.2.tgz", + "integrity": "sha512-VKQdeXj0+iRV8U0BWl+5r3W/IzZiA51Z6Zjctj/AYd5PU9MwNraHzj63wzUataKxDXB2AX2vc/bb4LuqywY0/w==", + "dependencies": { + "svelte": "^5.27.3" + } + }, "node_modules/svelte-check": { "version": "3.8.4", "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.4.tgz", @@ -7901,6 +7911,14 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/svelte-infinite": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/svelte-infinite/-/svelte-infinite-0.5.0.tgz", + "integrity": "sha512-3ZomRQcQzg8VWtnqO4MvPC0Jt3hvh1wmC47t64BcI+8UXTl0FJYVfB7ky4d1NJ3mf/KZZa+hcIZJPnV9cOt8gQ==", + "peerDependencies": { + "svelte": "^5.0.0-0" + } + }, "node_modules/svelte-medium-image-zoom": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/svelte-medium-image-zoom/-/svelte-medium-image-zoom-0.2.6.tgz", diff --git a/package.json b/package.json index bcf5473..85aa070 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,8 @@ "redis": "^4.7.0", "sharp": "^0.34.2", "steamapi": "^3.0.11", + "svelte-bricks": "^0.3.2", + "svelte-infinite": "^0.5.0", "svelte-medium-image-zoom": "^0.2.6", "svelte-portal": "^2.2.1", "svelte-tiptap": "^2.1.0", diff --git a/src/lib/components/MasonryPhotoGrid.svelte b/src/lib/components/MasonryPhotoGrid.svelte new file mode 100644 index 0000000..44f3278 --- /dev/null +++ b/src/lib/components/MasonryPhotoGrid.svelte @@ -0,0 +1,71 @@ + + + + +
+ + {#snippet children({ item })} + + {/snippet} + +
+ + \ No newline at end of file diff --git a/src/routes/api/photos/+server.ts b/src/routes/api/photos/+server.ts index 5a5b4e1..df68920 100644 --- a/src/routes/api/photos/+server.ts +++ b/src/routes/api/photos/+server.ts @@ -35,10 +35,8 @@ export const GET: RequestHandler = async (event) => { } } } - }, - orderBy: { createdAt: 'desc' }, - skip: offset, - take: limit + } + // Remove orderBy to sort everything together later }) // Fetch individual photos (marked for photography, not in any album) @@ -63,17 +61,50 @@ export const GET: RequestHandler = async (event) => { createdAt: true, photoPublishedAt: true, exifData: true - }, - orderBy: { photoPublishedAt: 'desc' }, - skip: offset, - take: limit + } + // Remove orderBy to sort everything together later }) + // Helper function to extract date from EXIF data + const getPhotoDate = (media: any): Date => { + // Try to get date from EXIF data + if (media.exifData && typeof media.exifData === 'object') { + // Check for common EXIF date fields + const exif = media.exifData as any + const dateTaken = exif.DateTimeOriginal || exif.DateTime || exif.dateTaken + if (dateTaken) { + // Parse EXIF date format (typically "YYYY:MM:DD HH:MM:SS") + const parsedDate = new Date(dateTaken.replace(/^(\d{4}):(\d{2}):(\d{2})/, '$1-$2-$3')) + if (!isNaN(parsedDate.getTime())) { + return parsedDate + } + } + } + + // Fallback to photoPublishedAt + if (media.photoPublishedAt) { + return new Date(media.photoPublishedAt) + } + + // Final fallback to createdAt + return new Date(media.createdAt) + } + // Transform albums to PhotoAlbum format const photoAlbums: PhotoAlbum[] = albums .filter((album) => album.media.length > 0) // Only include albums with media .map((album) => { const firstMedia = album.media[0].media + + // Find the most recent EXIF date from all photos in the album + let albumDate = new Date(album.createdAt) + for (const albumMedia of album.media) { + const mediaDate = getPhotoDate(albumMedia.media) + if (mediaDate > albumDate) { + albumDate = mediaDate + } + } + return { id: `album-${album.id}`, slug: album.slug, @@ -95,24 +126,14 @@ export const GET: RequestHandler = async (event) => { width: albumMedia.media.width || 400, height: albumMedia.media.height || 400 })), - createdAt: album.createdAt.toISOString() + createdAt: albumDate.toISOString() } }) // Transform individual media to Photo format const photos: Photo[] = individualMedia.map((media) => { - // Extract date from EXIF data if available - let photoDate: string - if (media.exifData && typeof media.exifData === 'object' && 'dateTaken' in media.exifData) { - // Use EXIF date if available - photoDate = media.exifData.dateTaken as string - } else if (media.photoPublishedAt) { - // Fall back to published date - photoDate = media.photoPublishedAt.toISOString() - } else { - // Fall back to created date - photoDate = media.createdAt.toISOString() - } + // Use the same helper function to get the photo date + const photoDate = getPhotoDate(media) return { id: `media-${media.id}`, @@ -121,27 +142,32 @@ export const GET: RequestHandler = async (event) => { caption: media.photoCaption || undefined, width: media.width || 400, height: media.height || 400, - createdAt: photoDate + createdAt: photoDate.toISOString() } }) // Combine albums and individual photos - const photoItems: PhotoItem[] = [...photoAlbums, ...photos] + let allPhotoItems: PhotoItem[] = [...photoAlbums, ...photos] // Sort by creation date (both albums and photos now have createdAt) - photoItems.sort((a, b) => { + // Newest first (reverse chronological) + allPhotoItems.sort((a, b) => { const dateA = a.createdAt ? new Date(a.createdAt) : new Date() const dateB = b.createdAt ? new Date(b.createdAt) : new Date() return dateB.getTime() - dateA.getTime() }) + // Apply pagination after sorting + const totalItems = allPhotoItems.length + const paginatedItems = allPhotoItems.slice(offset, offset + limit) + const response = { - photoItems, + photoItems: paginatedItems, pagination: { - total: photoItems.length, + total: totalItems, limit, offset, - hasMore: photoItems.length === limit // Simple check, could be more sophisticated + hasMore: offset + limit < totalItems } } diff --git a/src/routes/photos/+page.svelte b/src/routes/photos/+page.svelte index f8425f1..4e918d9 100644 --- a/src/routes/photos/+page.svelte +++ b/src/routes/photos/+page.svelte @@ -1,14 +1,76 @@