Files
zoe-shaldon/ui/src/routes/photos/+page.svelte
T
warkanum f16d280f9a 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
2026-08-23 10:33:50 +02:00

146 lines
4.5 KiB
Svelte

<script lang="ts">
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;
let name = $state('');
let message = $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) {
status = 'error';
errorMessage = 'Please choose at least one photo.';
return;
}
if (files.length > MAX_FILES) {
status = 'error';
errorMessage = `Please upload at most ${MAX_FILES} photos at a time.`;
return;
}
for (const file of files) {
if (file.size > MAX_SIZE_MB * 1024 * 1024) {
status = 'error';
errorMessage = `"${file.name}" is larger than ${MAX_SIZE_MB}MB.`;
return;
}
}
status = 'submitting';
errorMessage = '';
try {
await uploadPhotos({
name,
message: message || undefined,
files
});
status = 'done';
loadGuestPhotos();
} catch (err) {
status = 'error';
errorMessage = err instanceof ApiError ? err.message : 'Something went wrong. Please try again.';
}
}
</script>
<section class="flex flex-1 items-center justify-center px-4 py-16">
<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 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}>
<label class="label">
<span class="label-text text-2xl">Name *</span>
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700 text-2xl" type="text" required bind:value={name} disabled={status === 'submitting'} />
</label>
<label class="label">
<span class="label-text text-2xl">Message to the couple (optional)</span>
<textarea class="textarea border border-surface-300 focus:border-primary-500 dark:border-surface-700 text-2xl" rows="3" bind:value={message} disabled={status === 'submitting'}
></textarea>
</label>
<label class="label">
<span class="label-text text-2xl">Photos *</span>
<input
class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700 text-2xl"
type="file"
accept="image/*"
multiple
required
bind:files
disabled={status === 'submitting'}
/>
</label>
{#if status === 'error'}
<p class="text-error-500 text-2xl">{errorMessage}</p>
{/if}
<button class="btn preset-filled-secondary-500 mt-2 text-2xl" type="submit" disabled={status === 'submitting'}>
{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>
</div>
</section>