Content Rewrite
This commit is contained in:
@@ -11,7 +11,38 @@
|
||||
type EnquiryType = 'booking' | 'general';
|
||||
const visitStartedStorageKey = 'goodwalk_visit_started_at';
|
||||
const journeyStorageKey = 'goodwalk_journey';
|
||||
const requestedServiceStorageKey = 'goodwalk_requested_service';
|
||||
const maxJourneyEntries = 8;
|
||||
const servicePrompts: Record<
|
||||
string,
|
||||
{
|
||||
intro: string;
|
||||
messageLabel: string;
|
||||
messagePlaceholder: string;
|
||||
}
|
||||
> = {
|
||||
'Pack Walks': {
|
||||
intro:
|
||||
'Tell us about your dog, your area, and how they feel around other dogs so we can see if Pack Walks are the right fit.',
|
||||
messageLabel: 'Pack Walks fit',
|
||||
messagePlaceholder:
|
||||
'How old is your dog, how do they feel in groups, and is there anything about confidence, recall, or social behaviour we should know?'
|
||||
},
|
||||
'1:1 Walks': {
|
||||
intro:
|
||||
'Tell us about your dog, your area, and what you want from one-on-one walks so we can plan the right routine.',
|
||||
messageLabel: '1:1 walk needs',
|
||||
messagePlaceholder:
|
||||
'Tell us about your dog’s size, pace, leash manners, confidence, and anything else that would help us tailor a one-on-one walk.'
|
||||
},
|
||||
'Puppy Visits': {
|
||||
intro:
|
||||
'Tell us about your puppy, your area, and the kind of support you need at home so we can plan the right visit.',
|
||||
messageLabel: 'Puppy visit details',
|
||||
messagePlaceholder:
|
||||
'Tell us your puppy’s age, routine, toilet needs, feeding schedule, and anything important we should know before visiting.'
|
||||
}
|
||||
};
|
||||
|
||||
let step = 1;
|
||||
$: headingParts = splitBookingTitle(booking.title);
|
||||
@@ -76,7 +107,9 @@
|
||||
const defaultGeneralSubtitle =
|
||||
'Almost there — just your contact details so we can reply properly to your message.';
|
||||
|
||||
$: dogIntro = booking.dogIntro?.trim() || defaultDogIntro;
|
||||
$: primarySelectedService = selectedServices[0] ?? '';
|
||||
$: activeServicePrompt = servicePrompts[primarySelectedService];
|
||||
$: dogIntro = activeServicePrompt?.intro || booking.dogIntro?.trim() || defaultDogIntro;
|
||||
$: generalIntro = booking.generalIntro?.trim() || defaultGeneralIntro;
|
||||
$: hasServices = booking.serviceOptions.length > 0;
|
||||
$: if (!allowGeneralEnquiry && enquiryType === 'general') {
|
||||
@@ -90,6 +123,16 @@
|
||||
$: dogStepLabel = booking.dogStepLabel?.trim() || 'Your dog';
|
||||
$: detailsStepLabel = isGeneralEnquiry ? 'Your enquiry' : dogStepLabel;
|
||||
$: detailsStepIntro = isGeneralEnquiry ? generalIntro : dogIntro;
|
||||
$: bookingEyebrow = isGeneralEnquiry ? 'Friendly contact' : primarySelectedService || 'Free Meet & Greet';
|
||||
$: bookingIntro = isGeneralEnquiry
|
||||
? 'Send us the details and we’ll point you in the right direction.'
|
||||
: 'Tell us a little about your dog first. We’ll come back within 24 hours with the right next step.';
|
||||
$: detailsMessageLabel = isGeneralEnquiry
|
||||
? 'Your Message'
|
||||
: activeServicePrompt?.messageLabel || 'About Your Dog';
|
||||
$: detailsMessagePlaceholder = isGeneralEnquiry
|
||||
? 'Tell us if this is feedback, a complaint, a business enquiry, or anything else we should know.'
|
||||
: activeServicePrompt?.messagePlaceholder || 'Describe your pet, any special needs, or anything we should know.';
|
||||
$: successPetName = petName.trim() || 'your dog';
|
||||
|
||||
onMount(() => {
|
||||
@@ -98,6 +141,19 @@
|
||||
pageEnteredAt = now;
|
||||
visitStartedAt = readOrCreateVisitStartedAt(now);
|
||||
journey = updateJourneySnapshot(window.location.pathname, window.location.search);
|
||||
|
||||
applyRequestedService();
|
||||
|
||||
const handleRequestedService = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ service?: string }>;
|
||||
applyRequestedService(customEvent.detail?.service?.trim());
|
||||
};
|
||||
|
||||
window.addEventListener('goodwalk:service-selected', handleRequestedService as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('goodwalk:service-selected', handleRequestedService as EventListener);
|
||||
};
|
||||
});
|
||||
|
||||
function splitBookingTitle(title: string) {
|
||||
@@ -169,11 +225,35 @@
|
||||
errors = {};
|
||||
}
|
||||
|
||||
function applyRequestedService(service?: string) {
|
||||
const requestedService =
|
||||
service ||
|
||||
(() => {
|
||||
try {
|
||||
return window.sessionStorage.getItem(requestedServiceStorageKey)?.trim() || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
|
||||
if (!requestedService || !booking.serviceOptions.includes(requestedService)) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedServices = [requestedService];
|
||||
|
||||
try {
|
||||
window.sessionStorage.removeItem(requestedServiceStorageKey);
|
||||
} catch {
|
||||
// Ignore storage cleanup failures.
|
||||
}
|
||||
}
|
||||
|
||||
function toggleService(service: string, checked: boolean) {
|
||||
noteInteraction();
|
||||
|
||||
if (checked) {
|
||||
selectedServices = [...selectedServices, service];
|
||||
selectedServices = [service, ...selectedServices.filter((item) => item !== service)];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -342,10 +422,22 @@
|
||||
{/if}
|
||||
|
||||
<div class="booking-header">
|
||||
<span class="booking-eyebrow">{bookingEyebrow}</span>
|
||||
<h2 class="booking-title">
|
||||
<span class="booking-title-plain">{headingParts.plain}</span>{' '}
|
||||
<span class="booking-title-highlight">{headingParts.highlight}</span>
|
||||
</h2>
|
||||
<p class="booking-intro">{bookingIntro}</p>
|
||||
<div class="booking-trust-row" aria-label="Booking highlights">
|
||||
<span class="booking-trust-chip">
|
||||
<Icon name="fas fa-comment-dots" />
|
||||
Reply within 24 hours
|
||||
</span>
|
||||
<span class="booking-trust-chip">
|
||||
<Icon name="fas fa-paw" />
|
||||
Free, no-obligation Meet & Greet
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="booking-stepper" aria-label="Booking form steps">
|
||||
<button
|
||||
@@ -438,113 +530,138 @@
|
||||
{/if}
|
||||
|
||||
{#if !isGeneralEnquiry}
|
||||
<div class="booking-field-card" class:booking-field-card-invalid={errors.petName}>
|
||||
<label for="petName">
|
||||
<Icon name="fas fa-dog" /> Dog's Name <span class="booking-required">*</span>
|
||||
</label>
|
||||
<input
|
||||
bind:this={petNameInput}
|
||||
bind:value={petName}
|
||||
type="text"
|
||||
id="petName"
|
||||
name="petName"
|
||||
required
|
||||
placeholder="Your dog's name"
|
||||
class:input-invalid={errors.petName}
|
||||
on:input={() => clearError('petName')}
|
||||
/>
|
||||
{#if errors.petName}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.petName}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="booking-field-card booking-field-card-group booking-field-card-full">
|
||||
<div class="booking-field-group booking-field-group-dog">
|
||||
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.petName}>
|
||||
<label for="petName">
|
||||
<Icon name="fas fa-dog" /> Dog's Name <span class="booking-required">*</span>
|
||||
</label>
|
||||
<input
|
||||
bind:this={petNameInput}
|
||||
bind:value={petName}
|
||||
type="text"
|
||||
id="petName"
|
||||
name="petName"
|
||||
required
|
||||
placeholder="Your dog's name"
|
||||
class:input-invalid={errors.petName}
|
||||
on:input={() => clearError('petName')}
|
||||
/>
|
||||
{#if errors.petName}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.petName}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.location}>
|
||||
<label for="location">
|
||||
<Icon name="fas fa-location-dot" /> Location <span class="booking-required">*</span>
|
||||
</label>
|
||||
<input
|
||||
bind:this={locationInput}
|
||||
bind:value={location}
|
||||
type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
required
|
||||
placeholder="Suburb, street..."
|
||||
class:input-invalid={errors.location}
|
||||
on:input={() => clearError('location')}
|
||||
/>
|
||||
{#if errors.location}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.location}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="booking-field-stack booking-field-stack-full"
|
||||
class:booking-field-stack-invalid={errors.message}
|
||||
>
|
||||
<label for="message">
|
||||
<Icon name="fas fa-comment" /> {detailsMessageLabel}
|
||||
</label>
|
||||
<textarea
|
||||
bind:value={message}
|
||||
id="message"
|
||||
name="message"
|
||||
rows="4"
|
||||
placeholder={detailsMessagePlaceholder}
|
||||
class:input-invalid={errors.message}
|
||||
on:input={() => clearError('message')}
|
||||
></textarea>
|
||||
{#if errors.message}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.message}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if hasServices}
|
||||
<div class="booking-field-stack booking-field-stack-full">
|
||||
<span class="booking-service-label"><Icon name="fas fa-paw" /> Services</span>
|
||||
<div class="booking-service-options">
|
||||
{#each booking.serviceOptions as service}
|
||||
<label class="booking-check-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="services"
|
||||
value={service}
|
||||
checked={selectedServices.includes(service)}
|
||||
on:change={(event) =>
|
||||
toggleService(service, (event.currentTarget as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span class="booking-check-box" aria-hidden="true"></span>
|
||||
<span>{service}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isGeneralEnquiry}
|
||||
<div
|
||||
class="booking-field-card booking-field-card-wide"
|
||||
class:booking-field-card-invalid={errors.location}
|
||||
class="booking-field-card booking-field-card-full"
|
||||
class:booking-field-card-invalid={errors.message}
|
||||
>
|
||||
<label for="location">
|
||||
<Icon name="fas fa-location-dot" /> Location <span class="booking-required">*</span>
|
||||
<label for="message">
|
||||
<Icon name="fas fa-comment" /> {detailsMessageLabel}
|
||||
<span class="booking-required">*</span>
|
||||
</label>
|
||||
<input
|
||||
bind:this={locationInput}
|
||||
bind:value={location}
|
||||
type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
required
|
||||
placeholder="Suburb, street..."
|
||||
class:input-invalid={errors.location}
|
||||
on:input={() => clearError('location')}
|
||||
/>
|
||||
{#if errors.location}
|
||||
<textarea
|
||||
bind:value={message}
|
||||
id="message"
|
||||
name="message"
|
||||
rows="4"
|
||||
placeholder={detailsMessagePlaceholder}
|
||||
class:input-invalid={errors.message}
|
||||
on:input={() => clearError('message')}
|
||||
></textarea>
|
||||
{#if errors.message}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.location}
|
||||
{errors.message}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="booking-field-card booking-field-card-full"
|
||||
class:booking-field-card-invalid={errors.message}
|
||||
>
|
||||
<label for="message">
|
||||
<Icon name="fas fa-comment" /> {isGeneralEnquiry ? 'Your Message' : 'About Your Dog'}
|
||||
{#if isGeneralEnquiry}<span class="booking-required">*</span>{/if}
|
||||
</label>
|
||||
<textarea
|
||||
bind:value={message}
|
||||
id="message"
|
||||
name="message"
|
||||
rows="4"
|
||||
placeholder={isGeneralEnquiry
|
||||
? 'Tell us if this is feedback, a complaint, a business enquiry, or anything else we should know.'
|
||||
: 'Describe your pet, any special needs, or anything we should know.'}
|
||||
class:input-invalid={errors.message}
|
||||
on:input={() => clearError('message')}
|
||||
></textarea>
|
||||
{#if errors.message}
|
||||
<p class="field-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.message}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if hasServices && !isGeneralEnquiry}
|
||||
<div class="booking-service-row">
|
||||
<span class="booking-service-label"><Icon name="fas fa-paw" /> Services</span>
|
||||
<div class="booking-service-options">
|
||||
{#each booking.serviceOptions as service}
|
||||
<label class="booking-check-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="services"
|
||||
value={service}
|
||||
checked={selectedServices.includes(service)}
|
||||
on:change={(event) =>
|
||||
toggleService(service, (event.currentTarget as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span class="booking-check-box" aria-hidden="true"></span>
|
||||
<span>{service}</span>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="booking-actions booking-actions-next">
|
||||
<button type="button" class="btn btn-yellow booking-next-button" on:click={goToOwnerStep}>
|
||||
{ownerStepLabel}
|
||||
Next: {ownerStepLabel.toLowerCase()}
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</button>
|
||||
<p class="booking-next-note">Response from us within 24 hours</p>
|
||||
<p class="booking-next-note">No payment, no pressure, just the right starting point for your dog.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<input type="hidden" name="fullName" value={fullName} />
|
||||
@@ -652,7 +769,13 @@
|
||||
Back
|
||||
</button>
|
||||
<button type="submit" class="btn btn-yellow booking-submit-button" disabled={submitting}>
|
||||
{#if submitting}Sending…{:else}Send <Icon name="fas fa-arrow-right" />{/if}
|
||||
{#if submitting}
|
||||
Sending…
|
||||
{:else if isGeneralEnquiry}
|
||||
Send enquiry <Icon name="fas fa-arrow-right" />
|
||||
{:else}
|
||||
Request Meet & Greet <Icon name="fas fa-arrow-right" />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user