General enquries feature
This commit is contained in:
@@ -7,10 +7,13 @@
|
||||
import type { BookingContent } from '$lib/types';
|
||||
|
||||
export let booking: BookingContent;
|
||||
export let allowGeneralEnquiry = false;
|
||||
type EnquiryType = 'booking' | 'general';
|
||||
|
||||
let step = 1;
|
||||
$: headingParts = splitBookingTitle(booking.title);
|
||||
|
||||
let enquiryType: EnquiryType = 'booking';
|
||||
let fullName = '';
|
||||
let email = '';
|
||||
let phone = '';
|
||||
@@ -59,12 +62,26 @@
|
||||
|
||||
const defaultDogIntro =
|
||||
'Tell us about your dog and where you are based so we can plan the right Meet & Greet.';
|
||||
const defaultGeneralIntro =
|
||||
'Need to send feedback, make a complaint, or ask a business question? Choose general enquiry and tell us what you need.';
|
||||
const defaultGeneralSubtitle =
|
||||
'Almost there — just your contact details so we can reply properly to your message.';
|
||||
|
||||
$: dogIntro = booking.dogIntro?.trim() || defaultDogIntro;
|
||||
$: hasBanner = Boolean(booking.subtitle?.trim());
|
||||
$: generalIntro = booking.generalIntro?.trim() || defaultGeneralIntro;
|
||||
$: hasServices = booking.serviceOptions.length > 0;
|
||||
$: if (!allowGeneralEnquiry && enquiryType === 'general') {
|
||||
enquiryType = 'booking';
|
||||
}
|
||||
$: isGeneralEnquiry = allowGeneralEnquiry && enquiryType === 'general';
|
||||
$: ownerSubtitle = isGeneralEnquiry
|
||||
? booking.generalSubtitle?.trim() || defaultGeneralSubtitle
|
||||
: booking.subtitle;
|
||||
$: ownerStepLabel = booking.ownerStepLabel?.trim() || 'Owner Details';
|
||||
$: dogStepLabel = booking.dogStepLabel?.trim() || 'Your dog';
|
||||
$: firstStepLabel = isGeneralEnquiry ? 'Your enquiry' : dogStepLabel;
|
||||
$: firstStepIntro = isGeneralEnquiry ? generalIntro : dogIntro;
|
||||
$: successPetName = petName.trim() || 'your dog';
|
||||
|
||||
onMount(() => {
|
||||
formStartedAt = Date.now();
|
||||
@@ -99,22 +116,47 @@
|
||||
selectedServices = selectedServices.filter((item) => item !== service);
|
||||
}
|
||||
|
||||
function validateDogStep(): boolean {
|
||||
function setEnquiryType(nextType: EnquiryType) {
|
||||
enquiryType = nextType;
|
||||
if (nextType === 'general') {
|
||||
petName = '';
|
||||
location = '';
|
||||
selectedServices = [];
|
||||
}
|
||||
errors = {};
|
||||
}
|
||||
|
||||
function validateFirstStep(): boolean {
|
||||
const next: Record<string, string> = {};
|
||||
|
||||
if (!petName.trim()) next.petName = "Please enter your dog's name";
|
||||
if (!location.trim()) next.location = 'Please enter your location';
|
||||
if (isGeneralEnquiry) {
|
||||
if (!message.trim()) next.message = 'Please tell us how we can help';
|
||||
} else {
|
||||
if (!petName.trim()) next.petName = "Please enter your dog's name";
|
||||
if (!location.trim()) next.location = 'Please enter your location';
|
||||
}
|
||||
|
||||
errors = next;
|
||||
|
||||
if (next.petName) { petNameInput?.focus(); return false; }
|
||||
if (next.location) { locationInput?.focus(); return false; }
|
||||
if (next.petName) {
|
||||
petNameInput?.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (next.location) {
|
||||
locationInput?.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (next.message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function goToOwnerStep() {
|
||||
if (!validateDogStep()) return;
|
||||
if (!validateFirstStep()) return;
|
||||
errors = {};
|
||||
step = 2;
|
||||
}
|
||||
@@ -152,13 +194,19 @@
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
fullName, email, phone, petName, location, message,
|
||||
services: selectedServices,
|
||||
enquiryType,
|
||||
fullName,
|
||||
email,
|
||||
phone,
|
||||
petName: isGeneralEnquiry ? '' : petName,
|
||||
location: isGeneralEnquiry ? '' : location,
|
||||
message,
|
||||
services: isGeneralEnquiry ? [] : selectedServices,
|
||||
website,
|
||||
formStartedAt,
|
||||
referrer: document.referrer,
|
||||
page: window.location.href,
|
||||
}),
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -185,8 +233,9 @@
|
||||
{#if submitted}
|
||||
<SuccessModal
|
||||
firstName={fullName.split(' ')[0]}
|
||||
{petName}
|
||||
petName={successPetName}
|
||||
{email}
|
||||
{enquiryType}
|
||||
onClose={() => (submitted = false)}
|
||||
/>
|
||||
{/if}
|
||||
@@ -194,6 +243,7 @@
|
||||
{#if showErrorModal}
|
||||
<ErrorModal
|
||||
detail={submitErrorDetail}
|
||||
{enquiryType}
|
||||
onClose={() => (showErrorModal = false)}
|
||||
onRetry={() => (showErrorModal = false)}
|
||||
/>
|
||||
@@ -212,7 +262,7 @@
|
||||
on:click={() => (step = 1)}
|
||||
>
|
||||
<span class="booking-step-number">1</span>
|
||||
<span class="booking-step-label">{dogStepLabel}</span>
|
||||
<span class="booking-step-label">{firstStepLabel}</span>
|
||||
</button>
|
||||
<span class="booking-step-divider" aria-hidden="true"></span>
|
||||
<button
|
||||
@@ -247,70 +297,120 @@
|
||||
|
||||
{#if step === 1}
|
||||
<div class="booking-panel">
|
||||
{#if dogIntro}
|
||||
<div class="booking-panel-banner">{dogIntro}</div>
|
||||
{#if firstStepIntro}
|
||||
<div class="booking-panel-banner">{firstStepIntro}</div>
|
||||
{/if}
|
||||
|
||||
<div class:booking-card-grid-with-banner={Boolean(dogIntro)} class="booking-card-grid booking-card-grid-dog">
|
||||
<div class="booking-field-card" class:booking-field-card-invalid={errors.petName}>
|
||||
<label for="petName">
|
||||
<Icon name="fas fa-dog" /> Pet'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}
|
||||
<div class:booking-card-grid-with-banner={Boolean(firstStepIntro)} class="booking-card-grid booking-card-grid-dog">
|
||||
{#if allowGeneralEnquiry}
|
||||
<div class="booking-field-card booking-field-card-full">
|
||||
<label>
|
||||
<Icon name="fas fa-comments" /> Enquiry type
|
||||
</label>
|
||||
<div class="booking-toggle-group" role="radiogroup" aria-label="Enquiry type">
|
||||
<label class="booking-toggle-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="enquiryType"
|
||||
value="booking"
|
||||
checked={enquiryType === 'booking'}
|
||||
on:change={() => setEnquiryType('booking')}
|
||||
/>
|
||||
<span class="booking-toggle-indicator" aria-hidden="true"></span>
|
||||
<span>Book a Meet & Greet</span>
|
||||
</label>
|
||||
<label class="booking-toggle-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="enquiryType"
|
||||
value="general"
|
||||
checked={enquiryType === 'general'}
|
||||
on:change={() => setEnquiryType('general')}
|
||||
/>
|
||||
<span class="booking-toggle-indicator" aria-hidden="true"></span>
|
||||
<span>General enquiry</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="booking-help-text">
|
||||
General enquiries cover feedback, complaints, business enquiries, and other non-booking messages.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="booking-field-card booking-field-card-wide" class:booking-field-card-invalid={errors.location}>
|
||||
<label for="location">
|
||||
<Icon name="fas fa-location-dot" /> Location <span class="booking-required">*</span>
|
||||
{#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-wide" class:booking-field-card-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="Neighborhood, 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>
|
||||
{/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>
|
||||
<input
|
||||
bind:this={locationInput}
|
||||
bind:value={location}
|
||||
type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
required
|
||||
placeholder="Neighborhood, 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-card booking-field-card-full">
|
||||
<label for="message"><Icon name="fas fa-comment" /> About Your Dog</label>
|
||||
<textarea
|
||||
bind:value={message}
|
||||
id="message"
|
||||
name="message"
|
||||
rows="4"
|
||||
placeholder="Describe your pet, any special needs, or anything we should know."
|
||||
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}
|
||||
{#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">
|
||||
@@ -340,19 +440,24 @@
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<input type="hidden" name="petName" value={petName} />
|
||||
<input type="hidden" name="location" value={location} />
|
||||
<input type="hidden" name="enquiryType" value={enquiryType} />
|
||||
{#if !isGeneralEnquiry}
|
||||
<input type="hidden" name="petName" value={petName} />
|
||||
<input type="hidden" name="location" value={location} />
|
||||
{/if}
|
||||
<input type="hidden" name="message" value={message} />
|
||||
{#each selectedServices as service}
|
||||
<input type="hidden" name="services" value={service} />
|
||||
{/each}
|
||||
{#if !isGeneralEnquiry}
|
||||
{#each selectedServices as service}
|
||||
<input type="hidden" name="services" value={service} />
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="booking-panel">
|
||||
{#if hasBanner}
|
||||
<div class="booking-panel-banner">{booking.subtitle}</div>
|
||||
{#if ownerSubtitle}
|
||||
<div class="booking-panel-banner">{ownerSubtitle}</div>
|
||||
{/if}
|
||||
|
||||
<div class:booking-card-grid-with-banner={hasBanner} class="booking-card-grid booking-card-grid-owner">
|
||||
<div class:booking-card-grid-with-banner={Boolean(ownerSubtitle)} class="booking-card-grid booking-card-grid-owner">
|
||||
<div class="booking-field-card booking-field-card-group booking-field-card-full">
|
||||
<div class="booking-field-group booking-field-group-owner">
|
||||
<div class="booking-field-stack" class:booking-field-stack-invalid={errors.fullName}>
|
||||
|
||||
Reference in New Issue
Block a user