@brandlab-digital/site-next 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/contact/attachments-client.d.ts +3 -0
- package/dist/contact/attachments-client.d.ts.map +1 -1
- package/dist/contact/attachments-client.js +31 -8
- package/dist/contact/attachments-client.js.map +1 -1
- package/dist/contact/client-entry.d.ts +1 -1
- package/dist/contact/client-entry.d.ts.map +1 -1
- package/dist/contact/client-entry.js.map +1 -1
- package/dist/contact/contract.d.ts.map +1 -1
- package/dist/contact/contract.js +2 -2
- package/dist/contact/contract.js.map +1 -1
- package/dist/contact/email.d.ts.map +1 -1
- package/dist/contact/email.js +9 -0
- package/dist/contact/email.js.map +1 -1
- package/dist/contact/form-client.d.ts +4 -2
- package/dist/contact/form-client.d.ts.map +1 -1
- package/dist/contact/form-client.js +49 -13
- package/dist/contact/form-client.js.map +1 -1
- package/dist/contact/rate-limit.d.ts +59 -0
- package/dist/contact/rate-limit.d.ts.map +1 -0
- package/dist/contact/rate-limit.js +109 -0
- package/dist/contact/rate-limit.js.map +1 -0
- package/dist/contact/route-handler.d.ts +6 -0
- package/dist/contact/route-handler.d.ts.map +1 -1
- package/dist/contact/route-handler.js +4 -0
- package/dist/contact/route-handler.js.map +1 -1
- package/dist/contact/server-entry.d.ts +1 -1
- package/dist/contact/server-entry.d.ts.map +1 -1
- package/dist/contact/server-entry.js +1 -1
- package/dist/contact/server-entry.js.map +1 -1
- package/dist/contact/server.d.ts +9 -0
- package/dist/contact/server.d.ts.map +1 -1
- package/dist/contact/server.js +71 -7
- package/dist/contact/server.js.map +1 -1
- package/dist/contact/shared.d.ts +11 -0
- package/dist/contact/shared.d.ts.map +1 -1
- package/dist/contact/shared.js +11 -0
- package/dist/contact/shared.js.map +1 -1
- package/dist/contact/testing.d.ts +5 -0
- package/dist/contact/testing.d.ts.map +1 -1
- package/dist/contact/testing.js +1 -0
- package/dist/contact/testing.js.map +1 -1
- package/dist/lead-reporting.d.ts +6 -0
- package/dist/lead-reporting.d.ts.map +1 -1
- package/dist/lead-reporting.js +13 -1
- package/dist/lead-reporting.js.map +1 -1
- package/dist/ui-kit/brandlab-skin.d.ts +12 -0
- package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
- package/dist/ui-kit/brandlab-skin.js.map +1 -1
- package/dist/ui-kit/form/copy.d.ts +3 -0
- package/dist/ui-kit/form/copy.d.ts.map +1 -0
- package/dist/ui-kit/form/copy.js +5 -0
- package/dist/ui-kit/form/copy.js.map +1 -0
- package/dist/ui-kit/form/dialog.d.ts +24 -0
- package/dist/ui-kit/form/dialog.d.ts.map +1 -0
- package/dist/ui-kit/form/dialog.js +17 -0
- package/dist/ui-kit/form/dialog.js.map +1 -0
- package/dist/ui-kit/form/field.d.ts +30 -0
- package/dist/ui-kit/form/field.d.ts.map +1 -0
- package/dist/ui-kit/form/field.js +11 -0
- package/dist/ui-kit/form/field.js.map +1 -0
- package/dist/ui-kit/form/index.d.ts +7 -0
- package/dist/ui-kit/form/index.d.ts.map +1 -0
- package/dist/ui-kit/form/index.js +7 -0
- package/dist/ui-kit/form/index.js.map +1 -0
- package/dist/ui-kit/form/photo-upload.d.ts +37 -0
- package/dist/ui-kit/form/photo-upload.d.ts.map +1 -0
- package/dist/ui-kit/form/photo-upload.js +19 -0
- package/dist/ui-kit/form/photo-upload.js.map +1 -0
- package/dist/ui-kit/form/quote-dialog.d.ts +15 -0
- package/dist/ui-kit/form/quote-dialog.d.ts.map +1 -0
- package/dist/ui-kit/form/quote-dialog.js +68 -0
- package/dist/ui-kit/form/quote-dialog.js.map +1 -0
- package/dist/ui-kit/form/quote-form.d.ts +165 -0
- package/dist/ui-kit/form/quote-form.d.ts.map +1 -0
- package/dist/ui-kit/form/quote-form.js +235 -0
- package/dist/ui-kit/form/quote-form.js.map +1 -0
- package/dist/ui-kit/form/thank-you.d.ts +37 -0
- package/dist/ui-kit/form/thank-you.d.ts.map +1 -0
- package/dist/ui-kit/form/thank-you.js +36 -0
- package/dist/ui-kit/form/thank-you.js.map +1 -0
- package/dist/ui-kit/structure.css +197 -0
- package/dist/ui.d.ts +10 -1
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +3 -3
- package/dist/ui.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `QuoteForm`: the one enquiry form for the hero, the quote dialog, the contact page and cost guides, on the shared
|
|
3
|
+
* contact runtime (`useBrandlabContactForm`, `POST /api/contact`, photo preparation, idempotent retries).
|
|
4
|
+
*
|
|
5
|
+
* Locked layout (F8): Name · Suburb; Phone · Email, both required; the message ("How can we help?"), 5 rows; the
|
|
6
|
+
* "Add photos" drop box with its "0 of 5" counter; the button; then one consent line linking the privacy policy. One
|
|
7
|
+
* column below 640 px. No job select, no "(required)", no helper or reassurance lines: every label and message is
|
|
8
|
+
* the site's copy. Errors appear only after a submit attempt, as a summary plus a message under each field; every
|
|
9
|
+
* typed value and chosen photo is kept on any error. The button stays disabled until the page has hydrated and is
|
|
10
|
+
* locked with a busy label while photos are prepared or the enquiry sends. An accepted enquiry sends one
|
|
11
|
+
* `contact_form` (with `has_photos`) and opens the thank-you page.
|
|
12
|
+
*
|
|
13
|
+
* `contract` and `analytics` can't cross from a server component, so a site binds them once with `createQuoteForm`
|
|
14
|
+
* in its own `'use client'` file and renders the bound `QuoteForm` and `QuoteDialog` from its pages.
|
|
15
|
+
*/
|
|
16
|
+
import { type BrandlabContactContract, type ContactAttachmentPolicy } from '@brandlab-digital/site-core/contact';
|
|
17
|
+
import { type JSX } from 'react';
|
|
18
|
+
import type { BrandlabAnalytics } from '../../analytics.js';
|
|
19
|
+
import type { BrandlabContactAttachmentErrorReason, UseBrandlabContactAttachmentsOptions } from '../../contact/attachments-client.js';
|
|
20
|
+
import { type SkinEntry } from '../skin.js';
|
|
21
|
+
import { type QuoteDialogSkin } from './quote-dialog.js';
|
|
22
|
+
export declare const QUOTE_FORM_VARIANTS: readonly ["card", "plain", "inline"];
|
|
23
|
+
export type QuoteFormVariant = (typeof QUOTE_FORM_VARIANTS)[number];
|
|
24
|
+
/** The form's id and its lead diagnostics' `form_id`, one per placement. */
|
|
25
|
+
export declare const QUOTE_FORM_PLACEMENTS: readonly ["quote-hero", "quote-dialog", "contact", "guide"];
|
|
26
|
+
export type QuoteFormPlacement = (typeof QUOTE_FORM_PLACEMENTS)[number];
|
|
27
|
+
/** The variant each placement takes unless the skin names one: the hero aside, the dialog and contact page, a guide. */
|
|
28
|
+
export declare const QUOTE_FORM_PLACEMENT_VARIANTS: Readonly<Record<QuoteFormPlacement, QuoteFormVariant>>;
|
|
29
|
+
export type QuoteFormSlot = 'root' | 'row' | 'field' | 'label' | 'input' | 'textarea' | 'drop' | 'dropIcon' | 'counter' | 'photos' | 'photo' | 'thumb' | 'remove' | 'submit' | 'consent' | 'errorSummary' | 'error' | 'status';
|
|
30
|
+
/** Labels always sit above their fields; density sets the spacing. */
|
|
31
|
+
export interface QuoteFormLayout {
|
|
32
|
+
readonly density?: 'comfortable' | 'compact';
|
|
33
|
+
}
|
|
34
|
+
export type QuoteFormSkin = SkinEntry<QuoteFormSlot, QuoteFormVariant, QuoteFormLayout>;
|
|
35
|
+
/** Every visible phrase of the form, in the site's Australian English. */
|
|
36
|
+
export interface QuoteFormCopy {
|
|
37
|
+
readonly labels: {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly suburb: string;
|
|
40
|
+
readonly phone: string;
|
|
41
|
+
readonly email: string;
|
|
42
|
+
/** For example "How can we help?". */
|
|
43
|
+
readonly message: string;
|
|
44
|
+
/** For example "Add photos". */
|
|
45
|
+
readonly photos: string;
|
|
46
|
+
};
|
|
47
|
+
/** The message placeholder, for example "Tell us about your project". */
|
|
48
|
+
readonly messagePlaceholder: string;
|
|
49
|
+
/** The site's recorded button label, for example "Get my free quote". */
|
|
50
|
+
readonly submit: string;
|
|
51
|
+
/** Busy labels: "Sending…", "Sending photos…" and "Preparing photos…". */
|
|
52
|
+
readonly sending: string;
|
|
53
|
+
readonly sendingPhotos: string;
|
|
54
|
+
readonly preparingPhotos: string;
|
|
55
|
+
/** The photo counter with `{count}` and `{max}`, for example "{count} of {max}". */
|
|
56
|
+
readonly photoCount: string;
|
|
57
|
+
/** Each remove button's name with `{number}`, for example "Remove photo {number}". */
|
|
58
|
+
readonly removePhoto: string;
|
|
59
|
+
/** The one consent line: the sentence, the words in it that link, and the privacy policy's path. */
|
|
60
|
+
readonly consent: {
|
|
61
|
+
readonly sentence: string;
|
|
62
|
+
readonly linkText: string;
|
|
63
|
+
readonly href: string;
|
|
64
|
+
};
|
|
65
|
+
/** The error summary's heading, for example "Please check these details". */
|
|
66
|
+
readonly errorSummary: string;
|
|
67
|
+
/** A message under each field after a submit attempt. `photos` is needed only when photos are required. */
|
|
68
|
+
readonly errors: {
|
|
69
|
+
readonly name: string;
|
|
70
|
+
readonly suburb: string;
|
|
71
|
+
readonly phone: string;
|
|
72
|
+
readonly phoneFormat: string;
|
|
73
|
+
readonly email: string;
|
|
74
|
+
readonly emailFormat: string;
|
|
75
|
+
readonly message: string;
|
|
76
|
+
readonly photos?: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Why a chosen photo was refused: not an image type, too large, all photos together too large, more than the
|
|
80
|
+
* limit (`{max}` allowed), or it couldn't be prepared (an iPhone photo that wouldn't convert). Needed when the
|
|
81
|
+
* contract takes photos.
|
|
82
|
+
*/
|
|
83
|
+
readonly photoErrors?: Readonly<Record<BrandlabContactAttachmentErrorReason, string>>;
|
|
84
|
+
/** Shown when sending fails: the photos were refused, or anything else (say how to call instead). */
|
|
85
|
+
readonly failed: {
|
|
86
|
+
readonly attachment: string;
|
|
87
|
+
readonly general: string;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export interface QuoteFormProps extends QuoteFormSkin {
|
|
91
|
+
readonly formId: QuoteFormPlacement;
|
|
92
|
+
/** The contact contract from src/config/contact.ts. */
|
|
93
|
+
readonly contract: BrandlabContactContract;
|
|
94
|
+
/** The site's defineBrandlabAnalytics binding. */
|
|
95
|
+
readonly analytics?: BrandlabAnalytics | undefined;
|
|
96
|
+
/** Photo limits; the contract's attachment mode decides the default. */
|
|
97
|
+
readonly attachmentPolicy?: ContactAttachmentPolicy | undefined;
|
|
98
|
+
/** The thank-you page opened after an accepted enquiry, for example `/thank-you`. */
|
|
99
|
+
readonly redirectTo: string;
|
|
100
|
+
readonly copy: QuoteFormCopy;
|
|
101
|
+
/** The enquiry email's page row, for example "Contact". */
|
|
102
|
+
readonly pageLabel?: string | undefined;
|
|
103
|
+
/** The runtime's honeypot field (default `website`). */
|
|
104
|
+
readonly honeypotField?: string | undefined;
|
|
105
|
+
/** The contact endpoint (default `/api/contact`). */
|
|
106
|
+
readonly contactEndpoint?: string | undefined;
|
|
107
|
+
/** Client navigation to the thank-you page, for example `router.push`; a full page load by default. */
|
|
108
|
+
readonly navigate?: ((href: string) => void) | undefined;
|
|
109
|
+
/** Test seams: a fetch implementation and the photo preparation's options. */
|
|
110
|
+
readonly fetcher?: typeof fetch | undefined;
|
|
111
|
+
readonly attachmentOptions?: Omit<UseBrandlabContactAttachmentsOptions, 'policy'> | undefined;
|
|
112
|
+
}
|
|
113
|
+
export declare function QuoteForm({ formId, contract, analytics, attachmentPolicy, redirectTo, copy, pageLabel, honeypotField, contactEndpoint, navigate, fetcher, attachmentOptions, variant, layout, classes, }: QuoteFormProps): import("react/jsx-runtime").JSX.Element;
|
|
114
|
+
export interface CreateQuoteFormOptions {
|
|
115
|
+
readonly contract: BrandlabContactContract;
|
|
116
|
+
readonly analytics?: BrandlabAnalytics | undefined;
|
|
117
|
+
readonly attachmentPolicy?: ContactAttachmentPolicy | undefined;
|
|
118
|
+
/** The thank-you page, for example `/thank-you`. */
|
|
119
|
+
readonly redirectTo: string;
|
|
120
|
+
readonly copy: QuoteFormCopy;
|
|
121
|
+
/** The site's quote form skin; leave `variant` unset so each placement takes its own. */
|
|
122
|
+
readonly skin?: QuoteFormSkin | undefined;
|
|
123
|
+
/** The quote dialog's skin. */
|
|
124
|
+
readonly dialogSkin?: QuoteDialogSkin | undefined;
|
|
125
|
+
readonly honeypotField?: string | undefined;
|
|
126
|
+
readonly contactEndpoint?: string | undefined;
|
|
127
|
+
readonly navigate?: ((href: string) => void) | undefined;
|
|
128
|
+
readonly fetcher?: typeof fetch | undefined;
|
|
129
|
+
readonly attachmentOptions?: Omit<UseBrandlabContactAttachmentsOptions, 'policy'> | undefined;
|
|
130
|
+
}
|
|
131
|
+
/** What a page passes to the bound form: plain data, so a server component can render it. */
|
|
132
|
+
export interface BoundQuoteFormProps {
|
|
133
|
+
readonly formId: QuoteFormPlacement;
|
|
134
|
+
readonly pageLabel?: string;
|
|
135
|
+
/** This placement's own placeholder, for example on a cost guide. */
|
|
136
|
+
readonly messagePlaceholder?: string;
|
|
137
|
+
readonly layout?: QuoteFormLayout;
|
|
138
|
+
}
|
|
139
|
+
export interface BoundQuoteDialogProps {
|
|
140
|
+
/** The dialog's H2, for example "Get a free quote". */
|
|
141
|
+
readonly title: string;
|
|
142
|
+
/** The close button's accessible name, for example "Close". */
|
|
143
|
+
readonly closeLabel: string;
|
|
144
|
+
readonly pageLabel?: string;
|
|
145
|
+
readonly messagePlaceholder?: string;
|
|
146
|
+
}
|
|
147
|
+
export interface BoundQuoteForm {
|
|
148
|
+
readonly QuoteForm: (props: BoundQuoteFormProps) => JSX.Element;
|
|
149
|
+
readonly QuoteDialog: (props: BoundQuoteDialogProps) => JSX.Element;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Binds the form to the site's contract, analytics, photo policy, thank-you page, copy and skin. Call it once in the
|
|
153
|
+
* site's own `'use client'` file (`src/site/quote-form.tsx`) and export the two components:
|
|
154
|
+
*
|
|
155
|
+
* ```tsx
|
|
156
|
+
* 'use client'
|
|
157
|
+
* const quote = createQuoteForm({ contract: contactContract, analytics, redirectTo: '/thank-you', copy, skin: skin.quoteForm })
|
|
158
|
+
* export const QuoteForm = quote.QuoteForm
|
|
159
|
+
* export const QuoteDialog = quote.QuoteDialog
|
|
160
|
+
* ```
|
|
161
|
+
*
|
|
162
|
+
* It checks the contract and copy when the module loads, so a form that couldn't send fails the build instead.
|
|
163
|
+
*/
|
|
164
|
+
export declare function createQuoteForm(options: CreateQuoteFormOptions): BoundQuoteForm;
|
|
165
|
+
//# sourceMappingURL=quote-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-form.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/form/quote-form.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* `QuoteForm`: the one enquiry form for the hero, the quote dialog, the contact page and cost guides, on the shared
|
|
5
|
+
* contact runtime (`useBrandlabContactForm`, `POST /api/contact`, photo preparation, idempotent retries).
|
|
6
|
+
*
|
|
7
|
+
* Locked layout (F8): Name · Suburb; Phone · Email, both required; the message ("How can we help?"), 5 rows; the
|
|
8
|
+
* "Add photos" drop box with its "0 of 5" counter; the button; then one consent line linking the privacy policy. One
|
|
9
|
+
* column below 640 px. No job select, no "(required)", no helper or reassurance lines: every label and message is
|
|
10
|
+
* the site's copy. Errors appear only after a submit attempt, as a summary plus a message under each field; every
|
|
11
|
+
* typed value and chosen photo is kept on any error. The button stays disabled until the page has hydrated and is
|
|
12
|
+
* locked with a busy label while photos are prepared or the enquiry sends. An accepted enquiry sends one
|
|
13
|
+
* `contact_form` (with `has_photos`) and opens the thank-you page.
|
|
14
|
+
*
|
|
15
|
+
* `contract` and `analytics` can't cross from a server component, so a site binds them once with `createQuoteForm`
|
|
16
|
+
* in its own `'use client'` file and renders the bound `QuoteForm` and `QuoteDialog` from its pages.
|
|
17
|
+
*/
|
|
18
|
+
import { defineContactContract, isValidAustralianPhoneNumber, } from '@brandlab-digital/site-core/contact';
|
|
19
|
+
import { usePathname } from 'next/navigation';
|
|
20
|
+
import { useMemo, useRef, useState } from 'react';
|
|
21
|
+
import { contactFieldProps, useBrandlabContactForm } from '../../contact/form-client.js';
|
|
22
|
+
import { CONTACT_FORM_FIELDS, CONTACT_PHONE_CHARACTERS } from '../../contact/shared.js';
|
|
23
|
+
import { HeadlessErrorSummary } from '../../ui.js';
|
|
24
|
+
import { HeadlessErrorFocus } from '../../ui-client.js';
|
|
25
|
+
import { TextLink } from '../primitives/links.js';
|
|
26
|
+
import { slot } from '../skin.js';
|
|
27
|
+
import { Field } from './field.js';
|
|
28
|
+
import { PhotoUpload } from './photo-upload.js';
|
|
29
|
+
import { QuoteDialog } from './quote-dialog.js';
|
|
30
|
+
export const QUOTE_FORM_VARIANTS = ['card', 'plain', 'inline'];
|
|
31
|
+
/** The form's id and its lead diagnostics' `form_id`, one per placement. */
|
|
32
|
+
export const QUOTE_FORM_PLACEMENTS = ['quote-hero', 'quote-dialog', 'contact', 'guide'];
|
|
33
|
+
/** The variant each placement takes unless the skin names one: the hero aside, the dialog and contact page, a guide. */
|
|
34
|
+
export const QUOTE_FORM_PLACEMENT_VARIANTS = {
|
|
35
|
+
'quote-hero': 'card',
|
|
36
|
+
'quote-dialog': 'plain',
|
|
37
|
+
contact: 'plain',
|
|
38
|
+
guide: 'inline',
|
|
39
|
+
};
|
|
40
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
41
|
+
const PHOTOS = 'photos';
|
|
42
|
+
/**
|
|
43
|
+
* The contract's keys for the locked layout. A required field the form can't render would make every enquiry fail,
|
|
44
|
+
* so it throws instead, when the site binds the form (at build).
|
|
45
|
+
*/
|
|
46
|
+
function quoteFields(contract) {
|
|
47
|
+
defineContactContract(contract);
|
|
48
|
+
const role = (name) => contract.fields.find((field) => field.role === name);
|
|
49
|
+
const location = role('location');
|
|
50
|
+
if (location && location.kind !== 'text') {
|
|
51
|
+
throw new Error(`QuoteForm renders the suburb as a text field; the contract's "${location.key}" is a ${location.kind}.`);
|
|
52
|
+
}
|
|
53
|
+
const unrendered = contract.fields.find((field) => field.role === 'custom' && field.required);
|
|
54
|
+
if (unrendered) {
|
|
55
|
+
throw new Error(`QuoteForm has no place for the required field "${unrendered.key}"; make it optional or drop it.`);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
name: role('name').key,
|
|
59
|
+
location: location?.key,
|
|
60
|
+
phone: role('phone').key,
|
|
61
|
+
email: role('email').key,
|
|
62
|
+
message: role('message').key,
|
|
63
|
+
maxLength: Object.fromEntries(contract.fields.map((field) => [field.key, field.maxLength])),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const PHOTO_ERROR_REASONS = ['type', 'size', 'total', 'count', 'prepare'];
|
|
67
|
+
function assertCopy(copy, photos) {
|
|
68
|
+
const text = [
|
|
69
|
+
...Object.values(copy.labels),
|
|
70
|
+
copy.messagePlaceholder,
|
|
71
|
+
copy.submit,
|
|
72
|
+
copy.sending,
|
|
73
|
+
copy.sendingPhotos,
|
|
74
|
+
copy.preparingPhotos,
|
|
75
|
+
copy.photoCount,
|
|
76
|
+
copy.removePhoto,
|
|
77
|
+
copy.consent.sentence,
|
|
78
|
+
copy.consent.linkText,
|
|
79
|
+
copy.consent.href,
|
|
80
|
+
copy.errorSummary,
|
|
81
|
+
copy.errors.name,
|
|
82
|
+
copy.errors.suburb,
|
|
83
|
+
copy.errors.phone,
|
|
84
|
+
copy.errors.phoneFormat,
|
|
85
|
+
copy.errors.email,
|
|
86
|
+
copy.errors.emailFormat,
|
|
87
|
+
copy.errors.message,
|
|
88
|
+
copy.failed.attachment,
|
|
89
|
+
copy.failed.general,
|
|
90
|
+
...(photos === 'none' ? [] : PHOTO_ERROR_REASONS.map((reason) => copy.photoErrors?.[reason])),
|
|
91
|
+
...(photos === 'required' ? [copy.errors.photos] : []),
|
|
92
|
+
];
|
|
93
|
+
if (text.some((value) => typeof value !== 'string' || !value.trim())) {
|
|
94
|
+
throw new Error('QuoteForm copy is incomplete: every label, message and the consent line is the site\'s own text.');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** The checks the server makes, run before sending: never stricter than the server, so no real enquiry is blocked. */
|
|
98
|
+
function validate(form, fields, copy, photos) {
|
|
99
|
+
const value = (name) => {
|
|
100
|
+
const element = form.elements.namedItem(name);
|
|
101
|
+
return element && 'value' in element ? String(element.value).trim() : '';
|
|
102
|
+
};
|
|
103
|
+
const errors = {};
|
|
104
|
+
if (!value(fields.name))
|
|
105
|
+
errors[fields.name] = copy.errors.name;
|
|
106
|
+
if (fields.location && !value(fields.location))
|
|
107
|
+
errors[fields.location] = copy.errors.suburb;
|
|
108
|
+
const phone = value(fields.phone);
|
|
109
|
+
if (!phone)
|
|
110
|
+
errors[fields.phone] = copy.errors.phone;
|
|
111
|
+
else if (!CONTACT_PHONE_CHARACTERS.test(phone) || !isValidAustralianPhoneNumber(phone)) {
|
|
112
|
+
errors[fields.phone] = copy.errors.phoneFormat;
|
|
113
|
+
}
|
|
114
|
+
const email = value(fields.email).toLowerCase();
|
|
115
|
+
if (!email)
|
|
116
|
+
errors[fields.email] = copy.errors.email;
|
|
117
|
+
else if (!EMAIL_PATTERN.test(email))
|
|
118
|
+
errors[fields.email] = copy.errors.emailFormat;
|
|
119
|
+
if (!value(fields.message))
|
|
120
|
+
errors[fields.message] = copy.errors.message;
|
|
121
|
+
if (photos.required && photos.count === 0 && copy.errors.photos)
|
|
122
|
+
errors[PHOTOS] = copy.errors.photos;
|
|
123
|
+
return errors;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The site's message for a field the server refused: the runtime's own wording never shows. An empty field gets its
|
|
127
|
+
* "enter" message; a filled phone or email its format message.
|
|
128
|
+
*/
|
|
129
|
+
function serverFieldMessage(form, key, fields, copy) {
|
|
130
|
+
const element = form?.elements.namedItem(key);
|
|
131
|
+
const filled = Boolean(element && 'value' in element && String(element.value).trim());
|
|
132
|
+
if (key === fields.phone)
|
|
133
|
+
return filled ? copy.errors.phoneFormat : copy.errors.phone;
|
|
134
|
+
if (key === fields.email)
|
|
135
|
+
return filled ? copy.errors.emailFormat : copy.errors.email;
|
|
136
|
+
if (key === fields.name)
|
|
137
|
+
return copy.errors.name;
|
|
138
|
+
if (key === fields.location)
|
|
139
|
+
return copy.errors.suburb;
|
|
140
|
+
return copy.errors.message;
|
|
141
|
+
}
|
|
142
|
+
function ConsentLine({ consent, hooks }) {
|
|
143
|
+
const at = consent.sentence.indexOf(consent.linkText);
|
|
144
|
+
const link = _jsx(TextLink, { href: consent.href, children: at < 0 ? consent.sentence : consent.linkText });
|
|
145
|
+
return (_jsx("p", { ...hooks, children: at < 0 ? link : (_jsxs(_Fragment, { children: [consent.sentence.slice(0, at), link, consent.sentence.slice(at + consent.linkText.length)] })) }));
|
|
146
|
+
}
|
|
147
|
+
export function QuoteForm({ formId, contract, analytics, attachmentPolicy, redirectTo, copy, pageLabel, honeypotField = CONTACT_FORM_FIELDS.honeypot, contactEndpoint, navigate, fetcher, attachmentOptions, variant, layout, classes, }) {
|
|
148
|
+
const fields = useMemo(() => quoteFields(contract), [contract]);
|
|
149
|
+
const mode = contract.attachments.mode;
|
|
150
|
+
const photosEnabled = mode !== 'none';
|
|
151
|
+
const photosRequired = mode === 'images-required';
|
|
152
|
+
assertCopy(copy, photosRequired ? 'required' : photosEnabled ? 'optional' : 'none');
|
|
153
|
+
const pagePath = usePathname() || '/';
|
|
154
|
+
const [clientErrors, setClientErrors] = useState(null);
|
|
155
|
+
const [attempt, setAttempt] = useState(0);
|
|
156
|
+
const [withPhotos, setWithPhotos] = useState(false);
|
|
157
|
+
const photoCount = useRef(0);
|
|
158
|
+
const form = useBrandlabContactForm({
|
|
159
|
+
contract,
|
|
160
|
+
...(analytics ? { analytics } : {}),
|
|
161
|
+
pagePath,
|
|
162
|
+
...(pageLabel ? { pageLabel } : {}),
|
|
163
|
+
formId,
|
|
164
|
+
redirectTo,
|
|
165
|
+
redirectDelayMs: 0,
|
|
166
|
+
...(navigate ? { navigate } : {}),
|
|
167
|
+
...(contactEndpoint ? { contactEndpoint } : {}),
|
|
168
|
+
...(fetcher ? { fetcher } : {}),
|
|
169
|
+
attachments: { ...attachmentOptions, ...(attachmentPolicy ? { policy: attachmentPolicy } : {}) },
|
|
170
|
+
onSubmitStart: ({ form: element }) => {
|
|
171
|
+
const errors = validate(element, fields, copy, { required: photosRequired, count: photoCount.current });
|
|
172
|
+
setAttempt((count) => count + 1);
|
|
173
|
+
if (Object.keys(errors).length > 0) {
|
|
174
|
+
setClientErrors(errors);
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
setClientErrors(null);
|
|
178
|
+
setWithPhotos(photoCount.current > 0);
|
|
179
|
+
return undefined;
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
photoCount.current = form.attachments.images.length;
|
|
183
|
+
const fieldId = (key) => `${formId}-${key}`;
|
|
184
|
+
const order = [fields.name, fields.location, fields.phone, fields.email, fields.message, photosEnabled ? PHOTOS : undefined]
|
|
185
|
+
.filter((key) => Boolean(key));
|
|
186
|
+
const errors = clientErrors ?? Object.fromEntries(order.flatMap((key) => key !== PHOTOS && form.fieldError(key) ? [[key, serverFieldMessage(form.formProps.ref.current, key, fields, copy)]] : []));
|
|
187
|
+
const summary = order.flatMap((key) => (errors[key] ? [{ fieldId: fieldId(key), message: errors[key] }] : []));
|
|
188
|
+
const leaving = form.succeeded;
|
|
189
|
+
const busyLabel = form.attachments.isProcessing
|
|
190
|
+
? copy.preparingPhotos
|
|
191
|
+
: form.pending || leaving
|
|
192
|
+
? withPhotos ? copy.sendingPhotos : copy.sending
|
|
193
|
+
: undefined;
|
|
194
|
+
const result = form.result;
|
|
195
|
+
const failure = !busyLabel && clientErrors === null && result && !result.ok && summary.length === 0
|
|
196
|
+
? result.code === 'attachment_rejected' ? copy.failed.attachment : copy.failed.general
|
|
197
|
+
: undefined;
|
|
198
|
+
const fieldClasses = { field: classes?.field, label: classes?.label, error: classes?.error };
|
|
199
|
+
const max = (key) => fields.maxLength[key];
|
|
200
|
+
const textField = (key, label, props) => (_jsx(Field, { id: fieldId(key), label: label, error: errors[key], required: true, classes: fieldClasses, children: (control) => (_jsx("input", { ...contactFieldProps(form, key), ...control, ...slot(classes, 'input'), type: props.type, autoComplete: props.autoComplete, ...(props.type === 'tel' ? { inputMode: 'tel' } : { maxLength: max(key) }), ...(props.type === 'email' ? { autoCapitalize: 'none', spellCheck: false } : {}) })) }));
|
|
201
|
+
return (_jsxs("form", { ...form.formProps, noValidate: true, "data-bl": "quote-form", "data-variant": variant ?? QUOTE_FORM_PLACEMENT_VARIANTS[formId], "data-density": layout?.density ?? 'comfortable', className: classes?.root, children: [_jsx(HeadlessErrorSummary, { ...slot(classes, 'errorSummary'), id: `${formId}-errors`, headingId: `${formId}-errors-heading`, heading: copy.errorSummary, errors: summary }), _jsx(HeadlessErrorFocus, { active: summary.length > 0, formId: formId, summaryId: `${formId}-errors` }, attempt), _jsx("div", { "data-bl-honeypot": "", "data-bl-visually-hidden": "", "aria-hidden": "true", inert: true, children: _jsx("input", { type: "text", name: honeypotField, tabIndex: -1, autoComplete: "off" }) }), _jsxs("div", { ...slot(classes, 'row'), children: [textField(fields.name, copy.labels.name, { type: 'text', autoComplete: 'name' }), fields.location ? (_jsx(Field, { id: fieldId(fields.location), label: copy.labels.suburb, error: errors[fields.location], required: true, classes: fieldClasses, children: (control) => (_jsx("input", { ...contactFieldProps(form, fields.location), ...control, ...slot(classes, 'input'), type: "text", autoComplete: "address-level2", maxLength: max(fields.location) })) })) : null] }), _jsxs("div", { ...slot(classes, 'row'), children: [textField(fields.phone, copy.labels.phone, { type: 'tel', autoComplete: 'tel' }), textField(fields.email, copy.labels.email, { type: 'email', autoComplete: 'email' })] }), _jsx(Field, { id: fieldId(fields.message), label: copy.labels.message, error: errors[fields.message], required: true, classes: fieldClasses, children: (control) => (_jsx("textarea", { ...contactFieldProps(form, fields.message), ...control, ...slot(classes, 'textarea'), rows: 5, placeholder: copy.messagePlaceholder, maxLength: max(fields.message) })) }), photosEnabled ? (_jsx(PhotoUpload, { id: fieldId(PHOTOS), label: copy.labels.photos, attachments: form.attachments, counter: copy.photoCount, removeLabel: copy.removePhoto, errorMessages: copy.photoErrors, accept: mode === 'images-and-pdfs-optional' ? 'image/*,application/pdf' : 'image/*', error: errors[PHOTOS], required: photosRequired, disabled: form.pending || leaving, classes: classes })) : null, _jsx("button", { ...form.submitProps, ...slot(classes, 'submit'), disabled: form.submitDisabled || leaving, "data-state": busyLabel ? 'busy' : 'idle', children: busyLabel ?? copy.submit }), _jsx(ConsentLine, { consent: copy.consent, hooks: slot(classes, 'consent') }), _jsx("div", { ...slot(classes, 'status'), ref: form.statusRef, role: "status", "aria-live": "polite", tabIndex: -1, "data-state": failure ? 'error' : busyLabel ? 'busy' : 'idle', children: failure ?? (busyLabel ? _jsx("span", { "data-bl-visually-hidden": "", children: busyLabel }) : null) })] }));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Binds the form to the site's contract, analytics, photo policy, thank-you page, copy and skin. Call it once in the
|
|
205
|
+
* site's own `'use client'` file (`src/site/quote-form.tsx`) and export the two components:
|
|
206
|
+
*
|
|
207
|
+
* ```tsx
|
|
208
|
+
* 'use client'
|
|
209
|
+
* const quote = createQuoteForm({ contract: contactContract, analytics, redirectTo: '/thank-you', copy, skin: skin.quoteForm })
|
|
210
|
+
* export const QuoteForm = quote.QuoteForm
|
|
211
|
+
* export const QuoteDialog = quote.QuoteDialog
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
* It checks the contract and copy when the module loads, so a form that couldn't send fails the build instead.
|
|
215
|
+
*/
|
|
216
|
+
export function createQuoteForm(options) {
|
|
217
|
+
if (!options || typeof options !== 'object')
|
|
218
|
+
throw new TypeError('createQuoteForm needs the contract, copy and thank-you page.');
|
|
219
|
+
if (typeof options.redirectTo !== 'string' || !/^\/(?!\/)[^\s]*$/.test(options.redirectTo)) {
|
|
220
|
+
throw new TypeError('redirectTo must be a root-relative path such as /thank-you.');
|
|
221
|
+
}
|
|
222
|
+
quoteFields(options.contract);
|
|
223
|
+
const mode = options.contract.attachments.mode;
|
|
224
|
+
assertCopy(options.copy, mode === 'images-required' ? 'required' : mode === 'none' ? 'none' : 'optional');
|
|
225
|
+
const { skin, dialogSkin, ...shared } = options;
|
|
226
|
+
function BoundQuoteForm({ formId, pageLabel, messagePlaceholder, layout }) {
|
|
227
|
+
const merged = skin?.layout || layout ? { ...skin?.layout, ...layout } : undefined;
|
|
228
|
+
return (_jsx(QuoteForm, { ...shared, formId: formId, pageLabel: pageLabel, copy: messagePlaceholder ? { ...shared.copy, messagePlaceholder } : shared.copy, variant: skin?.variant, layout: merged, classes: skin?.classes }));
|
|
229
|
+
}
|
|
230
|
+
function BoundQuoteDialog({ title, closeLabel, pageLabel, messagePlaceholder }) {
|
|
231
|
+
return (_jsx(QuoteDialog, { title: title, closeLabel: closeLabel, classes: dialogSkin?.classes, children: _jsx(BoundQuoteForm, { formId: "quote-dialog", ...(pageLabel ? { pageLabel } : {}), ...(messagePlaceholder ? { messagePlaceholder } : {}) }) }));
|
|
232
|
+
}
|
|
233
|
+
return Object.freeze({ QuoteForm: BoundQuoteForm, QuoteDialog: BoundQuoteDialog });
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=quote-form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quote-form.js","sourceRoot":"","sources":["../../../src/ui-kit/form/quote-form.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client'\n\n/**\n * `QuoteForm`: the one enquiry form for the hero, the quote dialog, the contact page and cost guides, on the shared\n * contact runtime (`useBrandlabContactForm`, `POST /api/contact`, photo preparation, idempotent retries).\n *\n * Locked layout (F8): Name · Suburb; Phone · Email, both required; the message (\"How can we help?\"), 5 rows; the\n * \"Add photos\" drop box with its \"0 of 5\" counter; the button; then one consent line linking the privacy policy. One\n * column below 640 px. No job select, no \"(required)\", no helper or reassurance lines: every label and message is\n * the site's copy. Errors appear only after a submit attempt, as a summary plus a message under each field; every\n * typed value and chosen photo is kept on any error. The button stays disabled until the page has hydrated and is\n * locked with a busy label while photos are prepared or the enquiry sends. An accepted enquiry sends one\n * `contact_form` (with `has_photos`) and opens the thank-you page.\n *\n * `contract` and `analytics` can't cross from a server component, so a site binds them once with `createQuoteForm`\n * in its own `'use client'` file and renders the bound `QuoteForm` and `QuoteDialog` from its pages.\n */\nimport {\n defineContactContract,\n isValidAustralianPhoneNumber,\n type BrandlabContactContract,\n type ContactAttachmentPolicy,\n} from '@brandlab-digital/site-core/contact'\nimport { usePathname } from 'next/navigation'\nimport { useMemo, useRef, useState, type JSX, type RefObject } from 'react'\nimport type { BrandlabAnalytics } from '../../analytics.js'\nimport type {\n BrandlabContactAttachmentErrorReason,\n UseBrandlabContactAttachmentsOptions,\n} from '../../contact/attachments-client.js'\nimport { contactFieldProps, useBrandlabContactForm } from '../../contact/form-client.js'\nimport { CONTACT_FORM_FIELDS, CONTACT_PHONE_CHARACTERS } from '../../contact/shared.js'\nimport { HeadlessErrorSummary } from '../../ui.js'\nimport { HeadlessErrorFocus } from '../../ui-client.js'\nimport { TextLink } from '../primitives/links.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport { Field } from './field.js'\nimport { PhotoUpload } from './photo-upload.js'\nimport { QuoteDialog, type QuoteDialogSkin } from './quote-dialog.js'\n\nexport const QUOTE_FORM_VARIANTS = ['card', 'plain', 'inline'] as const\nexport type QuoteFormVariant = (typeof QUOTE_FORM_VARIANTS)[number]\n\n/** The form's id and its lead diagnostics' `form_id`, one per placement. */\nexport const QUOTE_FORM_PLACEMENTS = ['quote-hero', 'quote-dialog', 'contact', 'guide'] as const\nexport type QuoteFormPlacement = (typeof QUOTE_FORM_PLACEMENTS)[number]\n\n/** The variant each placement takes unless the skin names one: the hero aside, the dialog and contact page, a guide. */\nexport const QUOTE_FORM_PLACEMENT_VARIANTS: Readonly<Record<QuoteFormPlacement, QuoteFormVariant>> = {\n 'quote-hero': 'card',\n 'quote-dialog': 'plain',\n contact: 'plain',\n guide: 'inline',\n}\n\nexport type QuoteFormSlot =\n | 'root'\n | 'row'\n | 'field'\n | 'label'\n | 'input'\n | 'textarea'\n | 'drop'\n | 'dropIcon'\n | 'counter'\n | 'photos'\n | 'photo'\n | 'thumb'\n | 'remove'\n | 'submit'\n | 'consent'\n | 'errorSummary'\n | 'error'\n | 'status'\n/** Labels always sit above their fields; density sets the spacing. */\nexport interface QuoteFormLayout {\n readonly density?: 'comfortable' | 'compact'\n}\nexport type QuoteFormSkin = SkinEntry<QuoteFormSlot, QuoteFormVariant, QuoteFormLayout>\n\n/** Every visible phrase of the form, in the site's Australian English. */\nexport interface QuoteFormCopy {\n readonly labels: {\n readonly name: string\n readonly suburb: string\n readonly phone: string\n readonly email: string\n /** For example \"How can we help?\". */\n readonly message: string\n /** For example \"Add photos\". */\n readonly photos: string\n }\n /** The message placeholder, for example \"Tell us about your project\". */\n readonly messagePlaceholder: string\n /** The site's recorded button label, for example \"Get my free quote\". */\n readonly submit: string\n /** Busy labels: \"Sending…\", \"Sending photos…\" and \"Preparing photos…\". */\n readonly sending: string\n readonly sendingPhotos: string\n readonly preparingPhotos: string\n /** The photo counter with `{count}` and `{max}`, for example \"{count} of {max}\". */\n readonly photoCount: string\n /** Each remove button's name with `{number}`, for example \"Remove photo {number}\". */\n readonly removePhoto: string\n /** The one consent line: the sentence, the words in it that link, and the privacy policy's path. */\n readonly consent: { readonly sentence: string; readonly linkText: string; readonly href: string }\n /** The error summary's heading, for example \"Please check these details\". */\n readonly errorSummary: string\n /** A message under each field after a submit attempt. `photos` is needed only when photos are required. */\n readonly errors: {\n readonly name: string\n readonly suburb: string\n readonly phone: string\n readonly phoneFormat: string\n readonly email: string\n readonly emailFormat: string\n readonly message: string\n readonly photos?: string\n }\n /**\n * Why a chosen photo was refused: not an image type, too large, all photos together too large, more than the\n * limit (`{max}` allowed), or it couldn't be prepared (an iPhone photo that wouldn't convert). Needed when the\n * contract takes photos.\n */\n readonly photoErrors?: Readonly<Record<BrandlabContactAttachmentErrorReason, string>>\n /** Shown when sending fails: the photos were refused, or anything else (say how to call instead). */\n readonly failed: { readonly attachment: string; readonly general: string }\n}\n\nexport interface QuoteFormProps extends QuoteFormSkin {\n readonly formId: QuoteFormPlacement\n /** The contact contract from src/config/contact.ts. */\n readonly contract: BrandlabContactContract\n /** The site's defineBrandlabAnalytics binding. */\n readonly analytics?: BrandlabAnalytics | undefined\n /** Photo limits; the contract's attachment mode decides the default. */\n readonly attachmentPolicy?: ContactAttachmentPolicy | undefined\n /** The thank-you page opened after an accepted enquiry, for example `/thank-you`. */\n readonly redirectTo: string\n readonly copy: QuoteFormCopy\n /** The enquiry email's page row, for example \"Contact\". */\n readonly pageLabel?: string | undefined\n /** The runtime's honeypot field (default `website`). */\n readonly honeypotField?: string | undefined\n /** The contact endpoint (default `/api/contact`). */\n readonly contactEndpoint?: string | undefined\n /** Client navigation to the thank-you page, for example `router.push`; a full page load by default. */\n readonly navigate?: ((href: string) => void) | undefined\n /** Test seams: a fetch implementation and the photo preparation's options. */\n readonly fetcher?: typeof fetch | undefined\n readonly attachmentOptions?: Omit<UseBrandlabContactAttachmentsOptions, 'policy'> | undefined\n}\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\nconst PHOTOS = 'photos'\n\ninterface QuoteFields {\n readonly name: string\n /** The suburb: always required when the contract has one, as the server requires every location field. */\n readonly location: string | undefined\n readonly phone: string\n readonly email: string\n readonly message: string\n readonly maxLength: Readonly<Record<string, number | undefined>>\n}\n\n/**\n * The contract's keys for the locked layout. A required field the form can't render would make every enquiry fail,\n * so it throws instead, when the site binds the form (at build).\n */\nfunction quoteFields(contract: BrandlabContactContract): QuoteFields {\n defineContactContract(contract)\n const role = (name: string) => contract.fields.find((field) => field.role === name)\n const location = role('location')\n if (location && location.kind !== 'text') {\n throw new Error(`QuoteForm renders the suburb as a text field; the contract's \"${location.key}\" is a ${location.kind}.`)\n }\n const unrendered = contract.fields.find((field) => field.role === 'custom' && field.required)\n if (unrendered) {\n throw new Error(`QuoteForm has no place for the required field \"${unrendered.key}\"; make it optional or drop it.`)\n }\n return {\n name: role('name')!.key,\n location: location?.key,\n phone: role('phone')!.key,\n email: role('email')!.key,\n message: role('message')!.key,\n maxLength: Object.fromEntries(contract.fields.map((field) => [field.key, field.maxLength])),\n }\n}\n\nconst PHOTO_ERROR_REASONS = ['type', 'size', 'total', 'count', 'prepare'] as const\n\nfunction assertCopy(copy: QuoteFormCopy, photos: 'none' | 'optional' | 'required') {\n const text = [\n ...Object.values(copy.labels),\n copy.messagePlaceholder,\n copy.submit,\n copy.sending,\n copy.sendingPhotos,\n copy.preparingPhotos,\n copy.photoCount,\n copy.removePhoto,\n copy.consent.sentence,\n copy.consent.linkText,\n copy.consent.href,\n copy.errorSummary,\n copy.errors.name,\n copy.errors.suburb,\n copy.errors.phone,\n copy.errors.phoneFormat,\n copy.errors.email,\n copy.errors.emailFormat,\n copy.errors.message,\n copy.failed.attachment,\n copy.failed.general,\n ...(photos === 'none' ? [] : PHOTO_ERROR_REASONS.map((reason) => copy.photoErrors?.[reason])),\n ...(photos === 'required' ? [copy.errors.photos] : []),\n ]\n if (text.some((value) => typeof value !== 'string' || !value.trim())) {\n throw new Error('QuoteForm copy is incomplete: every label, message and the consent line is the site\\'s own text.')\n }\n}\n\n/** The checks the server makes, run before sending: never stricter than the server, so no real enquiry is blocked. */\nfunction validate(\n form: HTMLFormElement,\n fields: QuoteFields,\n copy: QuoteFormCopy,\n photos: { required: boolean; count: number },\n): Record<string, string> {\n const value = (name: string) => {\n const element = form.elements.namedItem(name)\n return element && 'value' in element ? String(element.value).trim() : ''\n }\n const errors: Record<string, string> = {}\n if (!value(fields.name)) errors[fields.name] = copy.errors.name\n if (fields.location && !value(fields.location)) errors[fields.location] = copy.errors.suburb\n const phone = value(fields.phone)\n if (!phone) errors[fields.phone] = copy.errors.phone\n else if (!CONTACT_PHONE_CHARACTERS.test(phone) || !isValidAustralianPhoneNumber(phone)) {\n errors[fields.phone] = copy.errors.phoneFormat\n }\n const email = value(fields.email).toLowerCase()\n if (!email) errors[fields.email] = copy.errors.email\n else if (!EMAIL_PATTERN.test(email)) errors[fields.email] = copy.errors.emailFormat\n if (!value(fields.message)) errors[fields.message] = copy.errors.message\n if (photos.required && photos.count === 0 && copy.errors.photos) errors[PHOTOS] = copy.errors.photos\n return errors\n}\n\n/**\n * The site's message for a field the server refused: the runtime's own wording never shows. An empty field gets its\n * \"enter\" message; a filled phone or email its format message.\n */\nfunction serverFieldMessage(form: HTMLFormElement | null, key: string, fields: QuoteFields, copy: QuoteFormCopy) {\n const element = form?.elements.namedItem(key)\n const filled = Boolean(element && 'value' in element && String(element.value).trim())\n if (key === fields.phone) return filled ? copy.errors.phoneFormat : copy.errors.phone\n if (key === fields.email) return filled ? copy.errors.emailFormat : copy.errors.email\n if (key === fields.name) return copy.errors.name\n if (key === fields.location) return copy.errors.suburb\n return copy.errors.message\n}\n\nfunction ConsentLine({ consent, hooks }: { consent: QuoteFormCopy['consent']; hooks: ReturnType<typeof slot> }) {\n const at = consent.sentence.indexOf(consent.linkText)\n const link = <TextLink href={consent.href}>{at < 0 ? consent.sentence : consent.linkText}</TextLink>\n return (\n <p {...hooks}>\n {at < 0 ? link : (\n <>\n {consent.sentence.slice(0, at)}\n {link}\n {consent.sentence.slice(at + consent.linkText.length)}\n </>\n )}\n </p>\n )\n}\n\nexport function QuoteForm({\n formId,\n contract,\n analytics,\n attachmentPolicy,\n redirectTo,\n copy,\n pageLabel,\n honeypotField = CONTACT_FORM_FIELDS.honeypot,\n contactEndpoint,\n navigate,\n fetcher,\n attachmentOptions,\n variant,\n layout,\n classes,\n}: QuoteFormProps) {\n const fields = useMemo(() => quoteFields(contract), [contract])\n const mode = contract.attachments.mode\n const photosEnabled = mode !== 'none'\n const photosRequired = mode === 'images-required'\n assertCopy(copy, photosRequired ? 'required' : photosEnabled ? 'optional' : 'none')\n const pagePath = usePathname() || '/'\n const [clientErrors, setClientErrors] = useState<Record<string, string> | null>(null)\n const [attempt, setAttempt] = useState(0)\n const [withPhotos, setWithPhotos] = useState(false)\n const photoCount = useRef(0)\n\n const form = useBrandlabContactForm({\n contract,\n ...(analytics ? { analytics } : {}),\n pagePath,\n ...(pageLabel ? { pageLabel } : {}),\n formId,\n redirectTo,\n redirectDelayMs: 0,\n ...(navigate ? { navigate } : {}),\n ...(contactEndpoint ? { contactEndpoint } : {}),\n ...(fetcher ? { fetcher } : {}),\n attachments: { ...attachmentOptions, ...(attachmentPolicy ? { policy: attachmentPolicy } : {}) },\n onSubmitStart: ({ form: element }) => {\n const errors = validate(element, fields, copy, { required: photosRequired, count: photoCount.current })\n setAttempt((count) => count + 1)\n if (Object.keys(errors).length > 0) {\n setClientErrors(errors)\n return false\n }\n setClientErrors(null)\n setWithPhotos(photoCount.current > 0)\n return undefined\n },\n })\n photoCount.current = form.attachments.images.length\n\n const fieldId = (key: string) => `${formId}-${key}`\n const order = [fields.name, fields.location, fields.phone, fields.email, fields.message, photosEnabled ? PHOTOS : undefined]\n .filter((key): key is string => Boolean(key))\n const errors: Record<string, string> = clientErrors ?? Object.fromEntries(\n order.flatMap((key) =>\n key !== PHOTOS && form.fieldError(key) ? [[key, serverFieldMessage(form.formProps.ref.current, key, fields, copy)]] : []),\n )\n const summary = order.flatMap((key) => (errors[key] ? [{ fieldId: fieldId(key), message: errors[key] }] : []))\n\n const leaving = form.succeeded\n const busyLabel = form.attachments.isProcessing\n ? copy.preparingPhotos\n : form.pending || leaving\n ? withPhotos ? copy.sendingPhotos : copy.sending\n : undefined\n const result = form.result\n const failure = !busyLabel && clientErrors === null && result && !result.ok && summary.length === 0\n ? result.code === 'attachment_rejected' ? copy.failed.attachment : copy.failed.general\n : undefined\n\n const fieldClasses = { field: classes?.field, label: classes?.label, error: classes?.error }\n const max = (key: string) => fields.maxLength[key]\n const textField = (key: string, label: string, props: { type: 'text' | 'tel' | 'email'; autoComplete: string }) => (\n <Field id={fieldId(key)} label={label} error={errors[key]} required classes={fieldClasses}>\n {(control) => (\n <input\n {...contactFieldProps(form, key)}\n {...control}\n {...slot(classes, 'input')}\n type={props.type}\n autoComplete={props.autoComplete}\n {...(props.type === 'tel' ? { inputMode: 'tel' as const } : { maxLength: max(key) })}\n {...(props.type === 'email' ? { autoCapitalize: 'none', spellCheck: false } : {})}\n />\n )}\n </Field>\n )\n\n return (\n <form\n {...form.formProps}\n noValidate\n data-bl=\"quote-form\"\n data-variant={variant ?? QUOTE_FORM_PLACEMENT_VARIANTS[formId]}\n data-density={layout?.density ?? 'comfortable'}\n className={classes?.root}\n >\n <HeadlessErrorSummary\n {...slot(classes, 'errorSummary')}\n id={`${formId}-errors`}\n headingId={`${formId}-errors-heading`}\n heading={copy.errorSummary}\n errors={summary}\n />\n <HeadlessErrorFocus key={attempt} active={summary.length > 0} formId={formId} summaryId={`${formId}-errors`} />\n <div data-bl-honeypot=\"\" data-bl-visually-hidden=\"\" aria-hidden=\"true\" inert>\n <input type=\"text\" name={honeypotField} tabIndex={-1} autoComplete=\"off\" />\n </div>\n\n <div {...slot(classes, 'row')}>\n {textField(fields.name, copy.labels.name, { type: 'text', autoComplete: 'name' })}\n {fields.location ? (\n <Field id={fieldId(fields.location)} label={copy.labels.suburb} error={errors[fields.location]} required classes={fieldClasses}>\n {(control) => (\n <input\n {...contactFieldProps(form, fields.location!)}\n {...control}\n {...slot(classes, 'input')}\n type=\"text\"\n autoComplete=\"address-level2\"\n maxLength={max(fields.location!)}\n />\n )}\n </Field>\n ) : null}\n </div>\n <div {...slot(classes, 'row')}>\n {textField(fields.phone, copy.labels.phone, { type: 'tel', autoComplete: 'tel' })}\n {textField(fields.email, copy.labels.email, { type: 'email', autoComplete: 'email' })}\n </div>\n <Field id={fieldId(fields.message)} label={copy.labels.message} error={errors[fields.message]} required classes={fieldClasses}>\n {(control) => (\n <textarea\n {...contactFieldProps(form, fields.message)}\n {...control}\n {...slot(classes, 'textarea')}\n rows={5}\n placeholder={copy.messagePlaceholder}\n maxLength={max(fields.message)}\n />\n )}\n </Field>\n {photosEnabled ? (\n <PhotoUpload\n id={fieldId(PHOTOS)}\n label={copy.labels.photos}\n attachments={form.attachments}\n counter={copy.photoCount}\n removeLabel={copy.removePhoto}\n errorMessages={copy.photoErrors}\n accept={mode === 'images-and-pdfs-optional' ? 'image/*,application/pdf' : 'image/*'}\n error={errors[PHOTOS]}\n required={photosRequired}\n disabled={form.pending || leaving}\n classes={classes}\n />\n ) : null}\n\n <button\n {...form.submitProps}\n {...slot(classes, 'submit')}\n disabled={form.submitDisabled || leaving}\n data-state={busyLabel ? 'busy' : 'idle'}\n >\n {busyLabel ?? copy.submit}\n </button>\n <ConsentLine consent={copy.consent} hooks={slot(classes, 'consent')} />\n <div\n {...slot(classes, 'status')}\n ref={form.statusRef as RefObject<HTMLDivElement>}\n role=\"status\"\n aria-live=\"polite\"\n tabIndex={-1}\n data-state={failure ? 'error' : busyLabel ? 'busy' : 'idle'}\n >\n {failure ?? (busyLabel ? <span data-bl-visually-hidden=\"\">{busyLabel}</span> : null)}\n </div>\n </form>\n )\n}\n\nexport interface CreateQuoteFormOptions {\n readonly contract: BrandlabContactContract\n readonly analytics?: BrandlabAnalytics | undefined\n readonly attachmentPolicy?: ContactAttachmentPolicy | undefined\n /** The thank-you page, for example `/thank-you`. */\n readonly redirectTo: string\n readonly copy: QuoteFormCopy\n /** The site's quote form skin; leave `variant` unset so each placement takes its own. */\n readonly skin?: QuoteFormSkin | undefined\n /** The quote dialog's skin. */\n readonly dialogSkin?: QuoteDialogSkin | undefined\n readonly honeypotField?: string | undefined\n readonly contactEndpoint?: string | undefined\n readonly navigate?: ((href: string) => void) | undefined\n readonly fetcher?: typeof fetch | undefined\n readonly attachmentOptions?: Omit<UseBrandlabContactAttachmentsOptions, 'policy'> | undefined\n}\n\n/** What a page passes to the bound form: plain data, so a server component can render it. */\nexport interface BoundQuoteFormProps {\n readonly formId: QuoteFormPlacement\n readonly pageLabel?: string\n /** This placement's own placeholder, for example on a cost guide. */\n readonly messagePlaceholder?: string\n readonly layout?: QuoteFormLayout\n}\n\nexport interface BoundQuoteDialogProps {\n /** The dialog's H2, for example \"Get a free quote\". */\n readonly title: string\n /** The close button's accessible name, for example \"Close\". */\n readonly closeLabel: string\n readonly pageLabel?: string\n readonly messagePlaceholder?: string\n}\n\nexport interface BoundQuoteForm {\n readonly QuoteForm: (props: BoundQuoteFormProps) => JSX.Element\n readonly QuoteDialog: (props: BoundQuoteDialogProps) => JSX.Element\n}\n\n/**\n * Binds the form to the site's contract, analytics, photo policy, thank-you page, copy and skin. Call it once in the\n * site's own `'use client'` file (`src/site/quote-form.tsx`) and export the two components:\n *\n * ```tsx\n * 'use client'\n * const quote = createQuoteForm({ contract: contactContract, analytics, redirectTo: '/thank-you', copy, skin: skin.quoteForm })\n * export const QuoteForm = quote.QuoteForm\n * export const QuoteDialog = quote.QuoteDialog\n * ```\n *\n * It checks the contract and copy when the module loads, so a form that couldn't send fails the build instead.\n */\nexport function createQuoteForm(options: CreateQuoteFormOptions): BoundQuoteForm {\n if (!options || typeof options !== 'object') throw new TypeError('createQuoteForm needs the contract, copy and thank-you page.')\n if (typeof options.redirectTo !== 'string' || !/^\\/(?!\\/)[^\\s]*$/.test(options.redirectTo)) {\n throw new TypeError('redirectTo must be a root-relative path such as /thank-you.')\n }\n quoteFields(options.contract)\n const mode = options.contract.attachments.mode\n assertCopy(options.copy, mode === 'images-required' ? 'required' : mode === 'none' ? 'none' : 'optional')\n const { skin, dialogSkin, ...shared } = options\n\n function BoundQuoteForm({ formId, pageLabel, messagePlaceholder, layout }: BoundQuoteFormProps) {\n const merged = skin?.layout || layout ? { ...skin?.layout, ...layout } : undefined\n return (\n <QuoteForm\n {...shared}\n formId={formId}\n pageLabel={pageLabel}\n copy={messagePlaceholder ? { ...shared.copy, messagePlaceholder } : shared.copy}\n variant={skin?.variant}\n layout={merged}\n classes={skin?.classes}\n />\n )\n }\n\n function BoundQuoteDialog({ title, closeLabel, pageLabel, messagePlaceholder }: BoundQuoteDialogProps) {\n return (\n <QuoteDialog title={title} closeLabel={closeLabel} classes={dialogSkin?.classes}>\n <BoundQuoteForm\n formId=\"quote-dialog\"\n {...(pageLabel ? { pageLabel } : {})}\n {...(messagePlaceholder ? { messagePlaceholder } : {})}\n />\n </QuoteDialog>\n )\n }\n\n return Object.freeze({ QuoteForm: BoundQuoteForm, QuoteDialog: BoundQuoteDialog })\n}\n"]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type SkinEntry } from '../skin.js';
|
|
2
|
+
import type { UiPhone } from '../types.js';
|
|
3
|
+
/** The reply time when the owner hasn't confirmed one (Abloom's live wording). */
|
|
4
|
+
export declare const THANK_YOU_DEFAULT_REPLY_TIME = "within one business day";
|
|
5
|
+
export type ThankYouSlot = 'root' | 'heading' | 'reply' | 'steps' | 'step' | 'call' | 'phone';
|
|
6
|
+
export type ThankYouSkin = SkinEntry<ThankYouSlot>;
|
|
7
|
+
export interface ThankYouProps extends ThankYouSkin {
|
|
8
|
+
/** The H1, for example "Thanks. We've got your request." */
|
|
9
|
+
readonly heading: string;
|
|
10
|
+
/** The reply sentence with `{replyTime}`, for example "We'll get back to you {replyTime}." */
|
|
11
|
+
readonly reply: string;
|
|
12
|
+
/** The owner-confirmed reply time, for example "within two hours"; the default otherwise. */
|
|
13
|
+
readonly replyTime?: string | undefined;
|
|
14
|
+
/** Two or three plain next steps. */
|
|
15
|
+
readonly steps: readonly string[];
|
|
16
|
+
/** The words before the call button, for example "Anything urgent? Call us now." */
|
|
17
|
+
readonly urgent: string;
|
|
18
|
+
readonly phone: UiPhone;
|
|
19
|
+
/** Optional words in the button before the number, for example "Call". */
|
|
20
|
+
readonly callLabel?: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
export declare function ThankYou({ heading, reply, replyTime, steps, urgent, phone, callLabel, classes }: ThankYouProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
/**
|
|
24
|
+
* The thank-you page's robots: `noindex, follow` (Google and every other crawler). Spread it into the page's metadata:
|
|
25
|
+
* `export const metadata = { ...pageMetadata('thank-you'), ...thankYouMetadata() }`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function thankYouMetadata(): {
|
|
28
|
+
readonly robots: {
|
|
29
|
+
readonly index: false;
|
|
30
|
+
readonly follow: true;
|
|
31
|
+
readonly googleBot: {
|
|
32
|
+
readonly index: false;
|
|
33
|
+
readonly follow: true;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=thank-you.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thank-you.d.ts","sourceRoot":"","sources":["../../../src/ui-kit/form/thank-you.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAG1C,kFAAkF;AAClF,eAAO,MAAM,4BAA4B,4BAA4B,CAAA;AAErE,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAA;AAC7F,MAAM,MAAM,YAAY,GAAG,SAAS,CAAC,YAAY,CAAC,CAAA;AAElD,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,8FAA8F;IAC9F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACvC,qCAAqC;IACrC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACxC;AAED,wBAAgB,QAAQ,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE,aAAa,2CA+B9G;AAED;;;GAGG;AACH,wBAAgB,gBAAgB;;;;;;;;;EAQ/B"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createElement as _createElement } from "react";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* `ThankYou`: the block on the thank-you page an accepted enquiry opens. Server component, all text in the server
|
|
5
|
+
* HTML (nothing waits for a script): the H1, the reply time ("within one business day" unless the owner confirmed
|
|
6
|
+
* another), two or three plain next steps and a large call button for anything urgent (placement `contact`). The
|
|
7
|
+
* page is `noindex, follow`: export `thankYouMetadata()` from it.
|
|
8
|
+
*/
|
|
9
|
+
import { PhoneLink } from '../primitives/links.js';
|
|
10
|
+
import { slot } from '../skin.js';
|
|
11
|
+
import { fillCopy } from './copy.js';
|
|
12
|
+
/** The reply time when the owner hasn't confirmed one (Abloom's live wording). */
|
|
13
|
+
export const THANK_YOU_DEFAULT_REPLY_TIME = 'within one business day';
|
|
14
|
+
export function ThankYou({ heading, reply, replyTime, steps, urgent, phone, callLabel, classes }) {
|
|
15
|
+
if (!reply.includes('{replyTime}')) {
|
|
16
|
+
throw new Error('ThankYou reply must include {replyTime}, so the reply time always shows.');
|
|
17
|
+
}
|
|
18
|
+
if (steps.length < 2 || steps.length > 3) {
|
|
19
|
+
throw new Error('ThankYou takes two or three next steps.');
|
|
20
|
+
}
|
|
21
|
+
return (_jsxs("div", { "data-bl": "thank-you", className: classes?.root, children: [_jsx("h1", { ...slot(classes, 'heading'), children: heading }), _jsx("p", { ...slot(classes, 'reply'), children: fillCopy(reply, { replyTime: replyTime?.trim() || THANK_YOU_DEFAULT_REPLY_TIME }) }), _jsx("ol", { ...slot(classes, 'steps'), children: steps.map((step) => (_createElement("li", { ...slot(classes, 'step'), key: step }, step))) }), _jsxs("p", { ...slot(classes, 'call'), children: [_jsx("span", { children: urgent }), ' ', _jsx(PhoneLink, { phone: phone, placement: "contact", variant: "button", showIcon: true, ...(callLabel ? { label: callLabel } : {}), ...(classes?.phone ? { classes: { root: classes.phone } } : {}) })] })] }));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The thank-you page's robots: `noindex, follow` (Google and every other crawler). Spread it into the page's metadata:
|
|
25
|
+
* `export const metadata = { ...pageMetadata('thank-you'), ...thankYouMetadata() }`.
|
|
26
|
+
*/
|
|
27
|
+
export function thankYouMetadata() {
|
|
28
|
+
return {
|
|
29
|
+
robots: {
|
|
30
|
+
index: false,
|
|
31
|
+
follow: true,
|
|
32
|
+
googleBot: { index: false, follow: true },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=thank-you.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thank-you.js","sourceRoot":"","sources":["../../../src/ui-kit/form/thank-you.tsx"],"names":[],"mappings":";;AAAA;;;;;GAKG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAClD,OAAO,EAAE,IAAI,EAAkB,MAAM,YAAY,CAAA;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEpC,kFAAkF;AAClF,MAAM,CAAC,MAAM,4BAA4B,GAAG,yBAAyB,CAAA;AAqBrE,MAAM,UAAU,QAAQ,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAiB;IAC7G,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,0EAA0E,CAAC,CAAA;IAC7F,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAA;IAC5D,CAAC;IACD,OAAO,CACL,0BAAa,WAAW,EAAC,SAAS,EAAE,OAAO,EAAE,IAAI,aAC/C,gBAAQ,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,YAAG,OAAO,GAAM,EAChD,eAAO,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,YAAG,QAAQ,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,4BAA4B,EAAE,CAAC,GAAK,EACtH,gBAAQ,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,YAC3B,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CACnB,0BAAQ,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,IAAI,IACrC,IAAI,CACF,CACN,CAAC,GACC,EACL,gBAAO,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,aAC1B,yBAAO,MAAM,GAAQ,EAAC,GAAG,EACzB,KAAC,SAAS,IACR,KAAK,EAAE,KAAK,EACZ,SAAS,EAAC,SAAS,EACnB,OAAO,EAAC,QAAQ,EAChB,QAAQ,WACJ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KACvC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GAChE,IACA,IACA,CACP,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO;QACL,MAAM,EAAE;YACN,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,IAAI;YACZ,SAAS,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE;SAC1C;KACO,CAAA;AACZ,CAAC","sourcesContent":["/**\n * `ThankYou`: the block on the thank-you page an accepted enquiry opens. Server component, all text in the server\n * HTML (nothing waits for a script): the H1, the reply time (\"within one business day\" unless the owner confirmed\n * another), two or three plain next steps and a large call button for anything urgent (placement `contact`). The\n * page is `noindex, follow`: export `thankYouMetadata()` from it.\n */\nimport { PhoneLink } from '../primitives/links.js'\nimport { slot, type SkinEntry } from '../skin.js'\nimport type { UiPhone } from '../types.js'\nimport { fillCopy } from './copy.js'\n\n/** The reply time when the owner hasn't confirmed one (Abloom's live wording). */\nexport const THANK_YOU_DEFAULT_REPLY_TIME = 'within one business day'\n\nexport type ThankYouSlot = 'root' | 'heading' | 'reply' | 'steps' | 'step' | 'call' | 'phone'\nexport type ThankYouSkin = SkinEntry<ThankYouSlot>\n\nexport interface ThankYouProps extends ThankYouSkin {\n /** The H1, for example \"Thanks. We've got your request.\" */\n readonly heading: string\n /** The reply sentence with `{replyTime}`, for example \"We'll get back to you {replyTime}.\" */\n readonly reply: string\n /** The owner-confirmed reply time, for example \"within two hours\"; the default otherwise. */\n readonly replyTime?: string | undefined\n /** Two or three plain next steps. */\n readonly steps: readonly string[]\n /** The words before the call button, for example \"Anything urgent? Call us now.\" */\n readonly urgent: string\n readonly phone: UiPhone\n /** Optional words in the button before the number, for example \"Call\". */\n readonly callLabel?: string | undefined\n}\n\nexport function ThankYou({ heading, reply, replyTime, steps, urgent, phone, callLabel, classes }: ThankYouProps) {\n if (!reply.includes('{replyTime}')) {\n throw new Error('ThankYou reply must include {replyTime}, so the reply time always shows.')\n }\n if (steps.length < 2 || steps.length > 3) {\n throw new Error('ThankYou takes two or three next steps.')\n }\n return (\n <div data-bl=\"thank-you\" className={classes?.root}>\n <h1 {...slot(classes, 'heading')}>{heading}</h1>\n <p {...slot(classes, 'reply')}>{fillCopy(reply, { replyTime: replyTime?.trim() || THANK_YOU_DEFAULT_REPLY_TIME })}</p>\n <ol {...slot(classes, 'steps')}>\n {steps.map((step) => (\n <li {...slot(classes, 'step')} key={step}>\n {step}\n </li>\n ))}\n </ol>\n <p {...slot(classes, 'call')}>\n <span>{urgent}</span>{' '}\n <PhoneLink\n phone={phone}\n placement=\"contact\"\n variant=\"button\"\n showIcon\n {...(callLabel ? { label: callLabel } : {})}\n {...(classes?.phone ? { classes: { root: classes.phone } } : {})}\n />\n </p>\n </div>\n )\n}\n\n/**\n * The thank-you page's robots: `noindex, follow` (Google and every other crawler). Spread it into the page's metadata:\n * `export const metadata = { ...pageMetadata('thank-you'), ...thankYouMetadata() }`.\n */\nexport function thankYouMetadata() {\n return {\n robots: {\n index: false,\n follow: true,\n googleBot: { index: false, follow: true },\n },\n } as const\n}\n"]}
|