This commit is contained in:
2026-05-19 23:36:58 +12:00
parent 5172588488
commit a7f8a619b1
68 changed files with 4486 additions and 1430 deletions
+291 -55
View File
@@ -10,6 +10,7 @@
export let booking: BookingContent;
export let pagePath = '';
$: isCompactContactPage = pagePath === '/contact-us';
const defaultServices = ['Tiny Gang Pack Walks', 'Solo Walks', 'Puppy Visits', 'Other Services'];
$: serviceOptions = booking.serviceOptions && booking.serviceOptions.length > 0
@@ -204,6 +205,22 @@
return Object.keys(next).length === 0;
}
function validateCompactContactForm(): boolean {
const next: Record<string, string> = {};
if (!petName.trim()) next.petName = "Please tell us your dog's name.";
if (selectedServices.length === 0) next.services = 'Pick at least one service.';
if (message.trim().length < 10) {
next.message = 'Tell us a little about your dog so we can prepare properly.';
}
if (!fullName.trim()) next.fullName = 'Please enter your full name';
const emailErr = validateEmail(email);
if (emailErr) next.email = emailErr;
if (!phone.trim()) next.phone = 'Please enter your phone number';
if (!location.trim()) next.location = 'Please enter your suburb';
errors = next;
return Object.keys(next).length === 0;
}
function goNext() {
noteInteraction();
if (!validateStep(step)) return;
@@ -222,7 +239,9 @@
async function handleSubmit() {
noteInteraction();
if (!validateStep(2)) return;
if (isCompactContactPage) {
if (!validateCompactContactForm()) return;
} else if (!validateStep(2)) return;
submitting = true;
sendClickedAt = Date.now();
@@ -252,7 +271,7 @@
journey,
referrer: typeof document !== 'undefined' ? document.referrer : '',
page: typeof window !== 'undefined' ? window.location.href : '',
variant: 'booking-wizard'
variant: isCompactContactPage ? 'contact-compact' : 'booking-wizard'
})
});
@@ -276,7 +295,7 @@
</script>
<section id="newlead" use:reveal={{ delay: 70 }} class="wiz reveal-block">
<div class="wiz-inner">
<div class="wiz-inner" class:wiz-inner--compact={isCompactContactPage}>
{#if submitted && SuccessModalComponent}
<svelte:component
this={SuccessModalComponent}
@@ -298,43 +317,45 @@
/>
{/if}
<div class="wiz-header">
<span class="wiz-eyebrow">Free Meet &amp; Greet</span>
<h2 class="wiz-title">
<span class="wiz-title-plain">{headingParts.plain}</span>
{#if headingParts.highlight}
{' '}
<span class="wiz-title-highlight">{headingParts.highlight}</span>
{/if}
</h2>
<p class="wiz-lead">{leadCopy}</p>
</div>
<div class="wiz-trust" aria-label="Goodwalk dog families">
<div class="wiz-avatars" aria-hidden="true">
{#each avatarDogs as dog}
<span class="wiz-avatar">
<img src={dog.image} alt="" loading="lazy" />
</span>
{/each}
{#if !isCompactContactPage}
<div class="wiz-header">
<span class="wiz-eyebrow">Free Meet &amp; Greet</span>
<h2 class="wiz-title">
<span class="wiz-title-plain">{headingParts.plain}</span>
{#if headingParts.highlight}
{' '}
<span class="wiz-title-highlight">{headingParts.highlight}</span>
{/if}
</h2>
<p class="wiz-lead">{leadCopy}</p>
</div>
<div class="wiz-trust-copy">
<p>{trustTitle}</p>
<span>{trustNote}</span>
</div>
</div>
<div class="wiz-progress" role="progressbar" aria-valuemin="1" aria-valuemax="2" aria-valuenow={step}>
<span class="wiz-step-mark" class:active={step >= 1} class:done={step > 1}>
<span class="wiz-step-num">1</span>
<span class="wiz-step-label">{booking.dogStepLabel || 'Your dog'}</span>
</span>
<span class="wiz-step-line" class:done={step > 1} aria-hidden="true"></span>
<span class="wiz-step-mark" class:active={step === 2}>
<span class="wiz-step-num">2</span>
<span class="wiz-step-label">{booking.ownerStepLabel || 'Your details'}</span>
</span>
</div>
<div class="wiz-trust" aria-label="Goodwalk dog families">
<div class="wiz-avatars" aria-hidden="true">
{#each avatarDogs as dog}
<span class="wiz-avatar">
<img src={dog.image} alt="" loading="lazy" />
</span>
{/each}
</div>
<div class="wiz-trust-copy">
<p>{trustTitle}</p>
<span>{trustNote}</span>
</div>
</div>
<div class="wiz-progress" role="progressbar" aria-valuemin="1" aria-valuemax="2" aria-valuenow={step}>
<span class="wiz-step-mark" class:active={step >= 1} class:done={step > 1}>
<span class="wiz-step-num">1</span>
<span class="wiz-step-label">{booking.dogStepLabel || 'Your dog'}</span>
</span>
<span class="wiz-step-line" class:done={step > 1} aria-hidden="true"></span>
<span class="wiz-step-mark" class:active={step === 2}>
<span class="wiz-step-num">2</span>
<span class="wiz-step-label">{booking.ownerStepLabel || 'Your details'}</span>
</span>
</div>
{/if}
<form
class="wiz-form"
@@ -355,10 +376,189 @@
/>
</div>
<article class="wiz-card">
<article class="wiz-card" class:wiz-card--compact={isCompactContactPage}>
{#key step}
<div class="wiz-step" in:fade={{ duration: 200 }}>
{#if step === 1}
<div class="wiz-step" class:wiz-step--compact={isCompactContactPage} in:fade={{ duration: 200 }}>
{#if isCompactContactPage}
<span class="wiz-step-eyebrow">Start here</span>
<h3 class="wiz-step-heading">Tell us about your dog</h3>
<p class="wiz-step-helper">A few details now. We come back with the right next step.</p>
<label class="wiz-field">
<span class="wiz-label">
<Icon name="fas fa-dog" />&nbsp;Your dog's name
</span>
<input
bind:value={petName}
on:input={() => clearError('petName')}
type="text"
placeholder="For example, Teddy"
class:invalid={errors.petName}
autocomplete="off"
/>
{#if errors.petName}
<span class="wiz-error">
<Icon name="fas fa-circle-exclamation" />
{errors.petName}
</span>
{/if}
</label>
<fieldset class="wiz-fieldset">
<legend class="wiz-label">
<Icon name="fas fa-paw" />&nbsp;Which service are you interested in?
</legend>
<div
class="wiz-service-grid"
class:wiz-service-grid--compact={isCompactContactPage}
class:invalid={errors.services}
role="group"
aria-label="Service interest"
>
{#each serviceOptions as service}
{@const checked = selectedServices.includes(service)}
<button
type="button"
class="wiz-service"
class:wiz-service--compact={isCompactContactPage}
class:active={checked}
aria-pressed={checked}
on:click={() => toggleService(service)}
>
<span class="wiz-service-check" aria-hidden="true">
{#if checked}<Icon name="fas fa-check" />{/if}
</span>
<span class="wiz-service-text">
<span class="wiz-service-label">{service}</span>
{#if serviceDescriptions[service]}
<span class="wiz-service-desc">{serviceDescriptions[service]}</span>
{/if}
</span>
</button>
{/each}
</div>
{#if errors.services}
<span class="wiz-error">
<Icon name="fas fa-circle-exclamation" />
{errors.services}
</span>
{/if}
</fieldset>
<div class="wiz-grid-two">
<label class="wiz-field">
<span class="wiz-label">
<Icon name="fas fa-user" />&nbsp;Full name
</span>
<input
bind:value={fullName}
on:input={() => clearError('fullName')}
type="text"
placeholder="Your full name"
class:invalid={errors.fullName}
autocomplete="name"
/>
{#if errors.fullName}
<span class="wiz-error">
<Icon name="fas fa-circle-exclamation" />
{errors.fullName}
</span>
{/if}
</label>
<label class="wiz-field">
<span class="wiz-label">
<Icon name="fas fa-envelope" />&nbsp;Email
</span>
<input
bind:value={email}
on:input={() => clearError('email')}
type="email"
placeholder="you@example.com"
class:invalid={errors.email}
autocomplete="email"
inputmode="email"
/>
{#if errors.email}
<span class="wiz-error">
<Icon name="fas fa-circle-exclamation" />
{errors.email}
</span>
{/if}
</label>
<label class="wiz-field">
<span class="wiz-label">
<Icon name="fas fa-phone" />&nbsp;Phone
</span>
<input
bind:value={phone}
on:input={() => clearError('phone')}
type="tel"
placeholder="(021) 234 5678"
class:invalid={errors.phone}
autocomplete="tel"
inputmode="tel"
/>
{#if errors.phone}
<span class="wiz-error">
<Icon name="fas fa-circle-exclamation" />
{errors.phone}
</span>
{/if}
</label>
<label class="wiz-field">
<span class="wiz-label">
<Icon name="fas fa-location-dot" />&nbsp;Your suburb
</span>
<input
bind:value={location}
on:input={() => clearError('location')}
type="text"
placeholder="For example, Grey Lynn"
class:invalid={errors.location}
autocomplete="address-level2"
/>
{#if errors.location}
<span class="wiz-error">
<Icon name="fas fa-circle-exclamation" />
{errors.location}
</span>
{/if}
</label>
</div>
<label class="wiz-field">
<span class="wiz-label">
<Icon name="fas fa-comment" />&nbsp;What else should we know?
</span>
<textarea
bind:value={message}
on:input={() => clearError('message')}
rows="3"
placeholder="Age, breed, temperament around other dogs, any health quirks, anything that helps us prepare."
class:invalid={errors.message}
></textarea>
{#if errors.message}
<span class="wiz-error">
<Icon name="fas fa-circle-exclamation" />
{errors.message}
</span>
{/if}
</label>
<div class="wiz-actions">
<button type="submit" class="wiz-btn wiz-btn-primary wiz-btn-primary--wide" disabled={submitting}>
{#if submitting}
Sending
{:else}
Send my details
{/if}
<Icon name="fas fa-paper-plane" />
</button>
</div>
{:else if step === 1}
<span class="wiz-step-eyebrow">Step one of two</span>
<h3 class="wiz-step-heading">Tell us about your dog</h3>
<p class="wiz-step-helper">Just the basics. Pick everything you are open to.</p>
@@ -556,7 +756,7 @@
</article>
</form>
<p class="wiz-reassurance" aria-live="polite">
<p class="wiz-reassurance" class:wiz-reassurance--compact={isCompactContactPage} aria-live="polite">
<Icon name="fas fa-bolt" />
A real reply within 24 hours, usually sooner.
</p>
@@ -575,6 +775,11 @@
margin: 0 auto;
}
.wiz-inner--compact {
max-width: 60rem;
margin-top: 8px;
}
.wiz-header {
text-align: center;
margin-bottom: 28px;
@@ -780,12 +985,21 @@
box-shadow: 0 30px 60px rgba(var(--ink-rgb), 0.08);
}
.wiz-card--compact {
max-width: 52rem;
padding: clamp(22px, 3vw, 32px);
}
.wiz-step {
display: flex;
flex-direction: column;
gap: 18px;
}
.wiz-step--compact {
gap: 16px;
}
.wiz-step-eyebrow {
align-self: flex-start;
padding: 6px 12px;
@@ -831,19 +1045,6 @@
color: var(--text-heading);
}
.wiz-optional {
margin-left: 6px;
padding: 2px 8px;
border-radius: 999px;
background: rgba(var(--brand-rgb), 0.06);
color: var(--text-subtle);
font-family: var(--font-body);
font-weight: 500;
font-size: 11px;
letter-spacing: 0.01em;
text-transform: uppercase;
}
.wiz-field input,
.wiz-field textarea {
width: 100%;
@@ -942,6 +1143,10 @@
border-color: rgba(var(--brand-rgb), 0.32);
}
.wiz-service--compact {
padding: 12px 14px;
}
.wiz-service.active {
border-color: var(--gw-green);
background: rgba(var(--accent-rgb), 0.1);
@@ -1022,6 +1227,11 @@
background: oklch(0.88 0.18 95);
}
.wiz-btn-primary--wide {
width: 100%;
justify-content: center;
}
.wiz-btn-back {
background: transparent;
color: var(--text-subtle);
@@ -1041,6 +1251,10 @@
text-align: center;
}
.wiz-reassurance--compact {
margin-top: 14px;
}
.wiz-reassurance :global(.icon) {
color: var(--yellow);
font-size: 13px;
@@ -1056,6 +1270,10 @@
padding-left: var(--space-container-x-mobile);
padding-right: var(--space-container-x-mobile);
}
.wiz-inner--compact {
margin-top: 0;
}
}
@media (max-width: 640px) {
@@ -1085,6 +1303,24 @@
grid-template-columns: 1fr;
}
.wiz-service-grid--compact {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.wiz-service--compact {
min-height: 100%;
padding: 12px;
}
.wiz-service--compact .wiz-service-desc {
display: none;
}
.wiz-card--compact {
padding: 20px 18px;
border-radius: 22px;
}
.wiz-grid-two {
grid-template-columns: 1fr;
}