Meet & Greet nudge

This commit is contained in:
2026-05-02 09:43:32 +12:00
parent 8f31a3fea4
commit cd8d581f7a
11 changed files with 845 additions and 35 deletions
+53 -22
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import Icon from '$lib/components/Icon.svelte';
import SuccessModal from '$lib/components/SuccessModal.svelte';
import ErrorModal from '$lib/components/ErrorModal.svelte';
import { reveal } from '$lib/actions/reveal';
import type { BookingContent } from '$lib/types';
@@ -26,7 +27,32 @@
let errors: Record<string, string> = {};
let submitting = false;
let submitted = false;
let submitError = '';
let showErrorModal = false;
let submitErrorDetail = '';
function validateEmail(raw: string): string {
const value = raw.trim();
if (!value) return 'Please enter your email address';
if (!value.includes('@')) return 'Email is missing the @ sign';
const [local, ...domainParts] = value.split('@');
const domain = domainParts.join('@');
if (domainParts.length > 1) return 'Email can only contain one @ sign';
if (!local) return 'Please add the part before the @';
if (!domain) return 'Please add a domain after the @, like @gmail.com';
if (!domain.includes('.')) return 'Please include a domain ending, like @gmail.com';
const tld = domain.split('.').pop() ?? '';
if (tld.length < 2) return 'That domain ending looks too short';
if (/\s/.test(value)) return 'Email cannot contain spaces';
const re = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
if (!re.test(value)) return 'That email doesnt look quite right';
return '';
}
const defaultDogIntro =
'Tell us about your dog and where you are based so we can plan the right Meet & Greet.';
@@ -70,7 +96,10 @@
const next: Record<string, string> = {};
if (!fullName.trim()) next.fullName = 'Please enter your full name';
if (!email.trim() || !emailInput?.checkValidity()) next.email = 'Please enter a valid email address';
const emailError = validateEmail(email);
if (emailError) next.email = emailError;
if (!phone.trim()) next.phone = 'Please enter your contact number';
errors = next;
@@ -108,7 +137,8 @@
}
submitting = true;
submitError = '';
submitErrorDetail = '';
showErrorModal = false;
try {
const res = await fetch('/api/submit', {
@@ -124,19 +154,23 @@
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.detail ?? 'Something went wrong. Please try again.');
const detail = typeof body?.detail === 'string'
? body.detail
: body?.detail?.message ?? body?.message ?? `Server responded with ${res.status}`;
throw new Error(detail);
}
submitted = true;
} catch (err: unknown) {
submitError = err instanceof Error ? err.message : 'Something went wrong. Please try again.';
submitErrorDetail = err instanceof Error ? err.message : String(err);
showErrorModal = true;
} finally {
submitting = false;
}
}
</script>
<section id="reservation" use:reveal={{ delay: 70 }} class="reveal-block">
<section id="newlead" use:reveal={{ delay: 70 }} class="reveal-block">
<div class="form-inner">
{#if submitted}
@@ -148,6 +182,14 @@
/>
{/if}
{#if showErrorModal}
<ErrorModal
detail={submitErrorDetail}
onClose={() => (showErrorModal = false)}
onRetry={() => (showErrorModal = false)}
/>
{/if}
<div class="booking-header">
<h2 class="booking-title">
<span class="booking-title-plain">{headingParts.plain}</span>{' '}<span class="booking-title-highlight">{headingParts.highlight}</span>
@@ -228,6 +270,11 @@
placeholder="Email"
class:input-invalid={errors.email}
on:input={() => clearError('email')}
on:blur={() => {
if (!email.trim()) return;
const msg = validateEmail(email);
errors = { ...errors, email: msg };
}}
/>
{#if errors.email}
<p class="field-error">
@@ -377,13 +424,6 @@
{#if submitting}Sending…{:else}Send <Icon name="fas fa-arrow-right" />{/if}
</button>
</div>
{#if submitError}
<p class="booking-submit-error">
<Icon name="fas fa-circle-exclamation" />
{submitError}
</p>
{/if}
{/if}
</form>
</div>
@@ -403,13 +443,4 @@
opacity: 1;
transform: translate3d(0, 0, 0);
}
.booking-submit-error {
margin: 16px 0 0;
display: flex;
align-items: center;
gap: 8px;
color: #c0392b;
font-size: 14px;
}
</style>