feat(ui): add wedding theme styles and layout components

* Introduced wedding-themed CSS variables for styling.
* Created layout component for the wedding site with navigation and countdown.
* Added RSVP and photo upload pages with form handling.
* Implemented QR code page for wedding site access.
* Configured TypeScript and Vite for the project.
* Added robots.txt for search engine crawling.
This commit is contained in:
2026-08-11 23:31:35 +02:00
commit 39f5b8d5cc
89 changed files with 6829 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
<script lang="ts">
import { createWeddingInfo, formatFullDate } from '$lib/wedding-info.svelte';
import { uploadPhotos, ApiError } from '$lib/api';
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('');
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,
email: email || undefined,
phone: phone || undefined,
files
});
status = 'done';
} 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">
{#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-lg">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">
<h1 class="font-serif text-4xl">Share your photos</h1>
<p class="text-surface-600-400 mt-2 mb-8 text-lg">
Upload photos from the day — 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">Name *</span>
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700" type="text" required bind:value={name} disabled={status === 'submitting'} />
</label>
<label class="label">
<span class="label-text">Message to the couple (optional)</span>
<textarea class="textarea border border-surface-300 focus:border-primary-500 dark:border-surface-700" rows="3" bind:value={message} disabled={status === 'submitting'}
></textarea>
</label>
<label class="label">
<span class="label-text">Email (optional)</span>
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700" type="email" bind:value={email} disabled={status === 'submitting'} />
</label>
<label class="label">
<span class="label-text">Phone (optional)</span>
<input class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700" type="tel" bind:value={phone} disabled={status === 'submitting'} />
</label>
<label class="label">
<span class="label-text">Photos *</span>
<input
class="input border border-surface-300 focus:border-primary-500 dark:border-surface-700"
type="file"
accept="image/*"
multiple
required
bind:files
disabled={status === 'submitting'}
/>
</label>
{#if status === 'error'}
<p class="text-error-500 text-base">{errorMessage}</p>
{/if}
<button class="btn preset-filled-secondary-500 mt-2" type="submit" disabled={status === 'submitting'}>
{status === 'submitting' ? 'Uploading…' : 'Upload photos'}
</button>
</form>
</div>
{/if}
</section>