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:
@@ -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é & 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>
|
||||
Reference in New Issue
Block a user