feat(api): add endpoint to list guest photos

* implement handleListPhotos to retrieve uploaded photos
* create PhotoFileRecord struct for photo data
* add fetchGuestPhotos function in the UI
* display guest photos in the photo upload page
* remove index.html as it is no longer needed
* add calendar functionality to download wedding event
This commit is contained in:
2026-08-23 10:33:50 +02:00
parent 4eddecea8e
commit f16d280f9a
9 changed files with 198 additions and 46 deletions
+57 -38
View File
@@ -1,28 +1,37 @@
<script lang="ts">
import { createWeddingInfo, formatFullDate } from '$lib/wedding-info.svelte';
import { uploadPhotos, ApiError } from '$lib/api';
import { browser } from '$app/environment';
import { uploadPhotos, fetchGuestPhotos, ApiError, type GuestPhoto } from '$lib/api';
import PhotoGallery from '$lib/components/PhotoGallery.svelte';
const MAX_FILES = 10;
const MAX_SIZE_MB = 15;
const wedding = createWeddingInfo();
let now = $state(Date.now());
$effect(() => {
const id = setInterval(() => (now = Date.now()), 30_000);
return () => clearInterval(id);
});
const isOpen = $derived(now >= wedding.date.getTime());
let name = $state('');
let message = $state('');
let email = $state('');
let phone = $state('');
let files: FileList | undefined = $state();
let status = $state<'idle' | 'submitting' | 'done' | 'error'>('idle');
let errorMessage = $state('');
let guestPhotos = $state<GuestPhoto[]>([]);
let guestPhotosStatus = $state<'loading' | 'done' | 'error'>('loading');
function loadGuestPhotos() {
fetchGuestPhotos()
.then((photos) => {
guestPhotos = photos;
guestPhotosStatus = 'done';
})
.catch(() => {
guestPhotosStatus = 'error';
});
}
$effect(() => {
if (!browser) return;
loadGuestPhotos();
});
async function onSubmit(event: SubmitEvent) {
event.preventDefault();
if (!files || files.length === 0) {
@@ -49,11 +58,10 @@
await uploadPhotos({
name,
message: message || undefined,
email: email || undefined,
phone: phone || undefined,
files
});
status = 'done';
loadGuestPhotos();
} catch (err) {
status = 'error';
errorMessage = err instanceof ApiError ? err.message : 'Something went wrong. Please try again.';
@@ -62,20 +70,17 @@
</script>
<section class="flex flex-1 items-center justify-center px-4 py-16">
{#if !isOpen}
<div class="flex flex-col items-center gap-6 text-center">
<h1 class="font-serif text-4xl">Photo uploads open on the wedding day</h1>
<p class="text-surface-600-400 text-2xl">Come back on {formatFullDate(wedding.date)} to share your photos with Zoé &amp; Shaldon.</p>
</div>
{:else if status === 'done'}
<div class="preset-filled-primary-500 rounded-base max-w-md p-4 text-center">
Thank you — your photos have been uploaded!
</div>
{:else}
<div class="w-full max-w-md">
<div class="w-full max-w-md">
<PhotoGallery />
{#if status === 'done'}
<div class="preset-filled-primary-500 rounded-base p-4 text-center">
Thank you — your photos have been uploaded!
</div>
{:else}
<h1 class="font-serif text-4xl">Share your photos</h1>
<p class="text-surface-600-400 mt-2 mb-8 text-2xl">
Upload photos from the day — up to {MAX_FILES} at a time, {MAX_SIZE_MB}MB each.
Upload your photos of Zoé &amp; Shaldon — up to {MAX_FILES} at a time, {MAX_SIZE_MB}MB each.
</p>
<form class="flex flex-col gap-4" onsubmit={onSubmit}>
@@ -90,16 +95,6 @@
></textarea>
</label>
<label class="label">
<span class="label-text text-2xl">Email (optional)</span>
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700 text-2xl" type="email" bind:value={email} disabled={status === 'submitting'} />
</label>
<label class="label">
<span class="label-text text-2xl">Phone (optional)</span>
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700 text-2xl" type="tel" bind:value={phone} disabled={status === 'submitting'} />
</label>
<label class="label">
<span class="label-text text-2xl">Photos *</span>
<input
@@ -121,6 +116,30 @@
{status === 'submitting' ? 'Uploading…' : 'Upload photos'}
</button>
</form>
{/if}
<div class="border-surface-300 dark:border-surface-700 mt-10 border-t pt-6">
<h2 class="font-serif text-center text-3xl">Photos From Our Guests</h2>
{#if guestPhotosStatus === 'loading'}
<p class="text-surface-600-400 mt-4 text-center text-2xl">Loading…</p>
{:else if guestPhotosStatus === 'error'}
<p class="text-error-500 mt-4 text-center text-2xl">Could not load guest photos.</p>
{:else if guestPhotos.length === 0}
<p class="text-surface-600-400 mt-4 text-center text-2xl">No photos yet — be the first to share!</p>
{:else}
<div class="mt-4 grid grid-cols-3 gap-2">
{#each guestPhotos as photo (photo.url)}
<a href={photo.url} target="_blank" rel="noopener noreferrer">
<img
src={photo.url}
alt="Shared by {photo.uploaderName}"
class="border-surface-300 dark:border-surface-700 aspect-square w-full rounded-base border object-cover"
loading="lazy"
/>
</a>
{/each}
</div>
{/if}
</div>
{/if}
</div>
</section>