keystone-design-bootstrap 1.0.42 → 1.0.44
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/package.json +1 -1
- package/src/design_system/components/DynamicFormFields.tsx +76 -6
- package/src/design_system/sections/contact-section-form.aman.tsx +11 -3
- package/src/design_system/sections/contact-section-form.balance.tsx +11 -3
- package/src/design_system/sections/contact-section-form.barelux.tsx +11 -3
- package/src/design_system/sections/contact-section-form.tsx +12 -2
- package/src/design_system/sections/contact-section.aman.tsx +17 -1
- package/src/design_system/sections/contact-section.balance.tsx +17 -1
- package/src/design_system/sections/contact-section.barelux.tsx +17 -1
- package/src/design_system/sections/contact-section.tsx +18 -1
- package/src/types/api/form.ts +2 -0
- package/src/types/api/website-photos.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useState } from 'react';
|
|
4
|
+
import ReactMarkdown from 'react-markdown';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
4
6
|
import { Input, InputBase, InputGroup, NativeSelect, Textarea, PrivacyCheckbox } from '../elements';
|
|
5
7
|
import type { FormDefinition, FormFieldDefinition } from '../../types/api/form';
|
|
6
8
|
import countries from '../../utils/countries';
|
|
@@ -10,6 +12,9 @@ export interface DynamicFormFieldsProps {
|
|
|
10
12
|
form: FormDefinition;
|
|
11
13
|
/** For job_application forms: add hidden jobSlug input. */
|
|
12
14
|
jobSlug?: string;
|
|
15
|
+
/** Optional URLs for ToS/Privacy links in consent checkbox label. */
|
|
16
|
+
privacyPolicyUrl?: string;
|
|
17
|
+
termsOfServiceUrl?: string;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
const INPUT_TYPES = ['text', 'email', 'tel'] as const;
|
|
@@ -54,15 +59,63 @@ function renderField(
|
|
|
54
59
|
selectedCountryPhone: string,
|
|
55
60
|
onCountryChange: (code: string) => void,
|
|
56
61
|
phoneValues: Record<string, string>,
|
|
57
|
-
setPhoneValues: React.Dispatch<React.SetStateAction<Record<string, string
|
|
62
|
+
setPhoneValues: React.Dispatch<React.SetStateAction<Record<string, string>>>,
|
|
63
|
+
companyName: string,
|
|
64
|
+
privacyPolicyUrl: string | undefined,
|
|
65
|
+
termsOfServiceUrl: string | undefined
|
|
58
66
|
): React.ReactNode {
|
|
59
67
|
const name = field.name ?? `field-${index}`;
|
|
68
|
+
const type = (field.type ?? 'text').toString().toLowerCase();
|
|
60
69
|
|
|
61
70
|
if (field.type === 'hidden') {
|
|
62
71
|
const val = field.value ?? '';
|
|
63
72
|
return <input key={name} type="hidden" name={name} value={val} />;
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
if (type === 'checkbox') {
|
|
76
|
+
const labelRaw = field.label ?? '';
|
|
77
|
+
const companyNameClean = companyName.replace(/\*\*/g, '').trim();
|
|
78
|
+
let labelWithCompany = companyNameClean
|
|
79
|
+
? labelRaw.replace(/\{\{company_name\}\}/gi, companyNameClean)
|
|
80
|
+
: labelRaw;
|
|
81
|
+
// Inject ToS/Privacy links as markdown so they render as links (then whole label is rendered as markdown)
|
|
82
|
+
if (name === 'tos_privacy_consent' && privacyPolicyUrl && termsOfServiceUrl) {
|
|
83
|
+
labelWithCompany = labelWithCompany
|
|
84
|
+
.replace(/\*\*Terms of Service\*\*/gi, `**[Terms of Service](${termsOfServiceUrl})**`)
|
|
85
|
+
.replace(/\*\*Privacy Policy\*\*/gi, `**[Privacy Policy](${privacyPolicyUrl})**`);
|
|
86
|
+
}
|
|
87
|
+
const id = `checkbox-${name}-${index}`;
|
|
88
|
+
return (
|
|
89
|
+
<div key={name} className="flex items-start gap-3">
|
|
90
|
+
<input
|
|
91
|
+
type="checkbox"
|
|
92
|
+
id={id}
|
|
93
|
+
name={name}
|
|
94
|
+
value="on"
|
|
95
|
+
required={Boolean(field.required)}
|
|
96
|
+
aria-describedby={id ? `${id}-desc` : undefined}
|
|
97
|
+
className="mt-1 h-4 w-4 shrink-0 rounded border-secondary focus:ring-focus-ring"
|
|
98
|
+
/>
|
|
99
|
+
<label
|
|
100
|
+
id={id ? `${id}-desc` : undefined}
|
|
101
|
+
htmlFor={id}
|
|
102
|
+
className="font-body text-sm text-tertiary [&_a]:underline [&_a]:outline-focus-ring [&_strong]:font-semibold"
|
|
103
|
+
>
|
|
104
|
+
<ReactMarkdown
|
|
105
|
+
remarkPlugins={[remarkGfm]}
|
|
106
|
+
components={{
|
|
107
|
+
p: ({ children }) => <span>{children}</span>,
|
|
108
|
+
strong: (props) => <strong className="font-semibold" {...props} />,
|
|
109
|
+
a: (props) => <a {...props} className="underline outline-focus-ring focus-visible:outline-2 focus-visible:outline-offset-2" />,
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
{labelWithCompany}
|
|
113
|
+
</ReactMarkdown>
|
|
114
|
+
</label>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
66
119
|
if (field.type === 'tel' && showCountryCode) {
|
|
67
120
|
const countryOptions = countries.map((c) => ({
|
|
68
121
|
label: c.phoneCode.startsWith('+') ? c.phoneCode : `+${c.phoneCode}`,
|
|
@@ -150,11 +203,12 @@ function renderField(
|
|
|
150
203
|
);
|
|
151
204
|
}
|
|
152
205
|
|
|
153
|
-
export function DynamicFormFields({ form, jobSlug }: DynamicFormFieldsProps) {
|
|
206
|
+
export function DynamicFormFields({ form, jobSlug, privacyPolicyUrl, termsOfServiceUrl }: DynamicFormFieldsProps) {
|
|
154
207
|
const [selectedCountryPhone, setSelectedCountryPhone] = useState('US');
|
|
155
208
|
const [phoneValues, setPhoneValues] = useState<Record<string, string>>({});
|
|
156
209
|
const { settings } = form;
|
|
157
210
|
const fields: FormDefinition['fields'] = Array.isArray(form.fields) ? form.fields : [];
|
|
211
|
+
const companyName = form.company_name ?? '';
|
|
158
212
|
|
|
159
213
|
const handleCountryChange = (newCode: string) => {
|
|
160
214
|
setSelectedCountryPhone(newCode);
|
|
@@ -178,9 +232,25 @@ export function DynamicFormFields({ form, jobSlug }: DynamicFormFieldsProps) {
|
|
|
178
232
|
|
|
179
233
|
/** Show country code selector for tel fields; default true (US +1) unless backend sets to false. */
|
|
180
234
|
const showCountryCode = settings?.show_country_code !== false;
|
|
181
|
-
const showPrivacyCheckbox = settings?.show_privacy_checkbox !== false;
|
|
182
235
|
const flat = allFieldsFlat(fields);
|
|
183
236
|
const hasPhoneField = flat.some((f) => f.type === 'tel');
|
|
237
|
+
const hasCheckboxFields = flat.some((f) => (f.type ?? '').toString().toLowerCase() === 'checkbox');
|
|
238
|
+
const showPrivacyCheckbox =
|
|
239
|
+
settings?.show_privacy_checkbox !== false && hasPhoneField && !hasCheckboxFields;
|
|
240
|
+
|
|
241
|
+
const renderFieldWithProps = (item: FormFieldDefinition, i: number) =>
|
|
242
|
+
renderField(
|
|
243
|
+
item,
|
|
244
|
+
i,
|
|
245
|
+
showCountryCode,
|
|
246
|
+
selectedCountryPhone,
|
|
247
|
+
handleCountryChange,
|
|
248
|
+
phoneValues,
|
|
249
|
+
setPhoneValues,
|
|
250
|
+
companyName,
|
|
251
|
+
privacyPolicyUrl,
|
|
252
|
+
termsOfServiceUrl
|
|
253
|
+
);
|
|
184
254
|
|
|
185
255
|
return (
|
|
186
256
|
<div className="flex flex-col gap-6">
|
|
@@ -194,7 +264,7 @@ export function DynamicFormFields({ form, jobSlug }: DynamicFormFieldsProps) {
|
|
|
194
264
|
>
|
|
195
265
|
{item.map((f, i) => (
|
|
196
266
|
<div key={f.name} className="flex-1">
|
|
197
|
-
{
|
|
267
|
+
{renderFieldWithProps(f, i)}
|
|
198
268
|
</div>
|
|
199
269
|
))}
|
|
200
270
|
</div>
|
|
@@ -202,14 +272,14 @@ export function DynamicFormFields({ form, jobSlug }: DynamicFormFieldsProps) {
|
|
|
202
272
|
}
|
|
203
273
|
return (
|
|
204
274
|
<div key={item.name ?? `field-${index}`}>
|
|
205
|
-
{
|
|
275
|
+
{renderFieldWithProps(item, index)}
|
|
206
276
|
</div>
|
|
207
277
|
);
|
|
208
278
|
})}
|
|
209
279
|
|
|
210
280
|
{jobSlug ? <input type="hidden" name="jobSlug" value={jobSlug} /> : null}
|
|
211
281
|
|
|
212
|
-
{showPrivacyCheckbox &&
|
|
282
|
+
{showPrivacyCheckbox && <PrivacyCheckbox />}
|
|
213
283
|
</div>
|
|
214
284
|
);
|
|
215
285
|
}
|
|
@@ -8,12 +8,13 @@ import type { FormDefinition } from '../../types/api/form';
|
|
|
8
8
|
import { useFormDefinitions } from '../../next/contexts/form-definitions';
|
|
9
9
|
|
|
10
10
|
interface ContactSectionFormAmanProps {
|
|
11
|
-
/** Form fields are rendered from this definition (required). */
|
|
12
11
|
formDefinition: FormDefinition | null | undefined;
|
|
13
12
|
submitButtonText?: string;
|
|
14
13
|
successMessage?: string;
|
|
15
14
|
thankYouMessage?: string;
|
|
16
15
|
onSuccess?: () => void;
|
|
16
|
+
privacyPolicyUrl?: string;
|
|
17
|
+
termsOfServiceUrl?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export const ContactSectionForm = ({
|
|
@@ -22,6 +23,8 @@ export const ContactSectionForm = ({
|
|
|
22
23
|
successMessage = "Thank you for contacting us! We'll get back to you soon.",
|
|
23
24
|
thankYouMessage,
|
|
24
25
|
onSuccess,
|
|
26
|
+
privacyPolicyUrl,
|
|
27
|
+
termsOfServiceUrl,
|
|
25
28
|
}: ContactSectionFormAmanProps) => {
|
|
26
29
|
const { leadFormDefinition } = useFormDefinitions();
|
|
27
30
|
const resolvedFormDefinition = formDefinition ?? leadFormDefinition;
|
|
@@ -43,7 +46,8 @@ export const ContactSectionForm = ({
|
|
|
43
46
|
const formData = new FormData(e.currentTarget);
|
|
44
47
|
const data: Record<string, string> = { formType: 'lead' };
|
|
45
48
|
formData.forEach((value, key) => {
|
|
46
|
-
if (key
|
|
49
|
+
if (key.endsWith('_prefix')) return;
|
|
50
|
+
if (typeof value === 'string') data[key] = value;
|
|
47
51
|
});
|
|
48
52
|
try {
|
|
49
53
|
const response = await fetch('/api/form/', {
|
|
@@ -74,7 +78,11 @@ export const ContactSectionForm = ({
|
|
|
74
78
|
|
|
75
79
|
return (
|
|
76
80
|
<Form ref={formRef} onSubmit={handleSubmit} className="flex flex-col gap-6">
|
|
77
|
-
<DynamicFormFields
|
|
81
|
+
<DynamicFormFields
|
|
82
|
+
form={resolvedFormDefinition}
|
|
83
|
+
privacyPolicyUrl={privacyPolicyUrl}
|
|
84
|
+
termsOfServiceUrl={termsOfServiceUrl}
|
|
85
|
+
/>
|
|
78
86
|
<Button
|
|
79
87
|
type="submit"
|
|
80
88
|
color="primary"
|
|
@@ -8,12 +8,13 @@ import type { FormDefinition } from '../../types/api/form';
|
|
|
8
8
|
import { useFormDefinitions } from '../../next/contexts/form-definitions';
|
|
9
9
|
|
|
10
10
|
interface ContactSectionFormBalanceProps {
|
|
11
|
-
/** Form fields are rendered from this definition (required). */
|
|
12
11
|
formDefinition: FormDefinition | null | undefined;
|
|
13
12
|
submitButtonText?: string;
|
|
14
13
|
successMessage?: string;
|
|
15
14
|
thankYouMessage?: string;
|
|
16
15
|
onSuccess?: () => void;
|
|
16
|
+
privacyPolicyUrl?: string;
|
|
17
|
+
termsOfServiceUrl?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export const ContactSectionForm = ({
|
|
@@ -22,6 +23,8 @@ export const ContactSectionForm = ({
|
|
|
22
23
|
successMessage = "Thank you for contacting us! We'll get back to you soon.",
|
|
23
24
|
thankYouMessage,
|
|
24
25
|
onSuccess,
|
|
26
|
+
privacyPolicyUrl,
|
|
27
|
+
termsOfServiceUrl,
|
|
25
28
|
}: ContactSectionFormBalanceProps) => {
|
|
26
29
|
const { leadFormDefinition } = useFormDefinitions();
|
|
27
30
|
const resolvedFormDefinition = formDefinition ?? leadFormDefinition;
|
|
@@ -43,7 +46,8 @@ export const ContactSectionForm = ({
|
|
|
43
46
|
const formData = new FormData(e.currentTarget);
|
|
44
47
|
const data: Record<string, string> = { formType: 'lead' };
|
|
45
48
|
formData.forEach((value, key) => {
|
|
46
|
-
if (key
|
|
49
|
+
if (key.endsWith('_prefix')) return;
|
|
50
|
+
if (typeof value === 'string') data[key] = value;
|
|
47
51
|
});
|
|
48
52
|
try {
|
|
49
53
|
const response = await fetch('/api/form/', {
|
|
@@ -74,7 +78,11 @@ export const ContactSectionForm = ({
|
|
|
74
78
|
|
|
75
79
|
return (
|
|
76
80
|
<Form ref={formRef} onSubmit={handleSubmit} className="flex flex-col gap-6">
|
|
77
|
-
<DynamicFormFields
|
|
81
|
+
<DynamicFormFields
|
|
82
|
+
form={resolvedFormDefinition}
|
|
83
|
+
privacyPolicyUrl={privacyPolicyUrl}
|
|
84
|
+
termsOfServiceUrl={termsOfServiceUrl}
|
|
85
|
+
/>
|
|
78
86
|
<Button
|
|
79
87
|
type="submit"
|
|
80
88
|
color="primary"
|
|
@@ -8,12 +8,13 @@ import type { FormDefinition } from '../../types/api/form';
|
|
|
8
8
|
import { useFormDefinitions } from '../../next/contexts/form-definitions';
|
|
9
9
|
|
|
10
10
|
interface ContactSectionFormBareluxProps {
|
|
11
|
-
/** Form fields are rendered from this definition (required). */
|
|
12
11
|
formDefinition: FormDefinition | null | undefined;
|
|
13
12
|
submitButtonText?: string;
|
|
14
13
|
successMessage?: string;
|
|
15
14
|
thankYouMessage?: string;
|
|
16
15
|
onSuccess?: () => void;
|
|
16
|
+
privacyPolicyUrl?: string;
|
|
17
|
+
termsOfServiceUrl?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export const ContactSectionForm = ({
|
|
@@ -22,6 +23,8 @@ export const ContactSectionForm = ({
|
|
|
22
23
|
successMessage = "Thank you for contacting us! We'll get back to you soon.",
|
|
23
24
|
thankYouMessage,
|
|
24
25
|
onSuccess,
|
|
26
|
+
privacyPolicyUrl,
|
|
27
|
+
termsOfServiceUrl,
|
|
25
28
|
}: ContactSectionFormBareluxProps) => {
|
|
26
29
|
const { leadFormDefinition } = useFormDefinitions();
|
|
27
30
|
const resolvedFormDefinition = formDefinition ?? leadFormDefinition;
|
|
@@ -43,7 +46,8 @@ export const ContactSectionForm = ({
|
|
|
43
46
|
const formData = new FormData(e.currentTarget);
|
|
44
47
|
const data: Record<string, string> = { formType: 'lead' };
|
|
45
48
|
formData.forEach((value, key) => {
|
|
46
|
-
if (key
|
|
49
|
+
if (key.endsWith('_prefix')) return;
|
|
50
|
+
if (typeof value === 'string') data[key] = value;
|
|
47
51
|
});
|
|
48
52
|
try {
|
|
49
53
|
const response = await fetch('/api/form/', {
|
|
@@ -74,7 +78,11 @@ export const ContactSectionForm = ({
|
|
|
74
78
|
|
|
75
79
|
return (
|
|
76
80
|
<Form ref={formRef} onSubmit={handleSubmit} className="flex flex-col gap-6">
|
|
77
|
-
<DynamicFormFields
|
|
81
|
+
<DynamicFormFields
|
|
82
|
+
form={resolvedFormDefinition}
|
|
83
|
+
privacyPolicyUrl={privacyPolicyUrl}
|
|
84
|
+
termsOfServiceUrl={termsOfServiceUrl}
|
|
85
|
+
/>
|
|
78
86
|
<Button
|
|
79
87
|
type="submit"
|
|
80
88
|
color="primary"
|
|
@@ -14,6 +14,9 @@ interface ContactSectionFormProps {
|
|
|
14
14
|
successMessage?: string;
|
|
15
15
|
thankYouMessage?: string;
|
|
16
16
|
onSuccess?: () => void;
|
|
17
|
+
/** Optional URLs for ToS/Privacy links in consent checkbox (passed to DynamicFormFields). */
|
|
18
|
+
privacyPolicyUrl?: string;
|
|
19
|
+
termsOfServiceUrl?: string;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export const ContactSectionForm = ({
|
|
@@ -22,6 +25,8 @@ export const ContactSectionForm = ({
|
|
|
22
25
|
successMessage = "Thank you for contacting us! We'll get back to you soon.",
|
|
23
26
|
thankYouMessage,
|
|
24
27
|
onSuccess,
|
|
28
|
+
privacyPolicyUrl,
|
|
29
|
+
termsOfServiceUrl,
|
|
25
30
|
}: ContactSectionFormProps) => {
|
|
26
31
|
const { leadFormDefinition } = useFormDefinitions();
|
|
27
32
|
const resolvedFormDefinition = formDefinition ?? leadFormDefinition;
|
|
@@ -47,7 +52,8 @@ export const ContactSectionForm = ({
|
|
|
47
52
|
const formData = new FormData(e.currentTarget);
|
|
48
53
|
const data: Record<string, string> = { formType: 'lead' };
|
|
49
54
|
formData.forEach((value, key) => {
|
|
50
|
-
if (key
|
|
55
|
+
if (key.endsWith('_prefix')) return;
|
|
56
|
+
if (typeof value === 'string') data[key] = value;
|
|
51
57
|
});
|
|
52
58
|
|
|
53
59
|
try {
|
|
@@ -79,7 +85,11 @@ export const ContactSectionForm = ({
|
|
|
79
85
|
return (
|
|
80
86
|
<Form ref={formRef} onSubmit={handleSubmit} className="flex flex-col gap-8">
|
|
81
87
|
<div className="flex flex-col gap-6">
|
|
82
|
-
<DynamicFormFields
|
|
88
|
+
<DynamicFormFields
|
|
89
|
+
form={resolvedFormDefinition}
|
|
90
|
+
privacyPolicyUrl={privacyPolicyUrl}
|
|
91
|
+
termsOfServiceUrl={termsOfServiceUrl}
|
|
92
|
+
/>
|
|
83
93
|
</div>
|
|
84
94
|
{submitStatus === 'success' && (
|
|
85
95
|
<div className="rounded-lg bg-success-50 p-4 text-success-700">
|
|
@@ -2,13 +2,23 @@ import React from 'react';
|
|
|
2
2
|
import { PhotoWithFallback } from '../elements';
|
|
3
3
|
import type { WebsitePhotos } from '../../types/api/website-photos';
|
|
4
4
|
import type { FormDefinition } from '../../types/api/form';
|
|
5
|
+
import type { SiteConfig } from '../../types/config';
|
|
5
6
|
import { ContactSectionForm } from './contact-section-form.aman';
|
|
6
7
|
|
|
8
|
+
function getLegalUrlsFromConfig(config: SiteConfig | null | undefined): { privacyPolicyUrl?: string; termsOfServiceUrl?: string } {
|
|
9
|
+
if (!config?.navigation?.footer) return {};
|
|
10
|
+
const flat = config.navigation.footer.flat();
|
|
11
|
+
const privacy = flat.find((l) => l.label === 'Privacy Policy')?.href;
|
|
12
|
+
const terms = flat.find((l) => l.label === 'Terms of Service')?.href;
|
|
13
|
+
return { privacyPolicyUrl: privacy, termsOfServiceUrl: terms };
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
interface ContactSectionProps {
|
|
8
17
|
websitePhotos?: WebsitePhotos | null;
|
|
9
18
|
title?: string;
|
|
10
19
|
subtitle?: string;
|
|
11
20
|
formDefinition?: FormDefinition | null;
|
|
21
|
+
config?: SiteConfig | null;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
const ContactSection = ({
|
|
@@ -16,7 +26,9 @@ const ContactSection = ({
|
|
|
16
26
|
title = "",
|
|
17
27
|
subtitle = "",
|
|
18
28
|
formDefinition,
|
|
29
|
+
config,
|
|
19
30
|
}: ContactSectionProps) => {
|
|
31
|
+
const { privacyPolicyUrl, termsOfServiceUrl } = getLegalUrlsFromConfig(config);
|
|
20
32
|
const contactPhoto = websitePhotos?.contact;
|
|
21
33
|
const contactImageUrl = contactPhoto?.url;
|
|
22
34
|
const finalContactImage = contactImageUrl && contactImageUrl.trim() !== "" ? contactImageUrl : undefined;
|
|
@@ -37,7 +49,11 @@ const ContactSection = ({
|
|
|
37
49
|
</p>
|
|
38
50
|
</div>
|
|
39
51
|
|
|
40
|
-
<ContactSectionForm
|
|
52
|
+
<ContactSectionForm
|
|
53
|
+
formDefinition={formDefinition}
|
|
54
|
+
privacyPolicyUrl={privacyPolicyUrl}
|
|
55
|
+
termsOfServiceUrl={termsOfServiceUrl}
|
|
56
|
+
/>
|
|
41
57
|
</div>
|
|
42
58
|
|
|
43
59
|
{/* Image Side - full height next to form */}
|
|
@@ -2,13 +2,23 @@ import React from 'react';
|
|
|
2
2
|
import { PhotoWithFallback } from '../elements';
|
|
3
3
|
import type { WebsitePhotos } from '../../types/api/website-photos';
|
|
4
4
|
import type { FormDefinition } from '../../types/api/form';
|
|
5
|
+
import type { SiteConfig } from '../../types/config';
|
|
5
6
|
import { ContactSectionForm } from './contact-section-form.balance';
|
|
6
7
|
|
|
8
|
+
function getLegalUrlsFromConfig(config: SiteConfig | null | undefined): { privacyPolicyUrl?: string; termsOfServiceUrl?: string } {
|
|
9
|
+
if (!config?.navigation?.footer) return {};
|
|
10
|
+
const flat = config.navigation.footer.flat();
|
|
11
|
+
const privacy = flat.find((l) => l.label === 'Privacy Policy')?.href;
|
|
12
|
+
const terms = flat.find((l) => l.label === 'Terms of Service')?.href;
|
|
13
|
+
return { privacyPolicyUrl: privacy, termsOfServiceUrl: terms };
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
interface ContactSectionProps {
|
|
8
17
|
websitePhotos?: WebsitePhotos | null;
|
|
9
18
|
title?: string;
|
|
10
19
|
subtitle?: string;
|
|
11
20
|
formDefinition?: FormDefinition | null;
|
|
21
|
+
config?: SiteConfig | null;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
const ContactSection = ({
|
|
@@ -16,7 +26,9 @@ const ContactSection = ({
|
|
|
16
26
|
title = "",
|
|
17
27
|
subtitle = "",
|
|
18
28
|
formDefinition,
|
|
29
|
+
config,
|
|
19
30
|
}: ContactSectionProps) => {
|
|
31
|
+
const { privacyPolicyUrl, termsOfServiceUrl } = getLegalUrlsFromConfig(config);
|
|
20
32
|
const contactPhoto = websitePhotos?.contact;
|
|
21
33
|
const contactImageUrl = contactPhoto?.url;
|
|
22
34
|
const finalContactImage = contactImageUrl && contactImageUrl.trim() !== "" ? contactImageUrl : undefined;
|
|
@@ -49,7 +61,11 @@ const ContactSection = ({
|
|
|
49
61
|
|
|
50
62
|
{/* Form centered */}
|
|
51
63
|
<div className="max-w-2xl mx-auto">
|
|
52
|
-
<ContactSectionForm
|
|
64
|
+
<ContactSectionForm
|
|
65
|
+
formDefinition={formDefinition}
|
|
66
|
+
privacyPolicyUrl={privacyPolicyUrl}
|
|
67
|
+
termsOfServiceUrl={termsOfServiceUrl}
|
|
68
|
+
/>
|
|
53
69
|
</div>
|
|
54
70
|
</div>
|
|
55
71
|
</section>
|
|
@@ -2,13 +2,23 @@ import React from "react";
|
|
|
2
2
|
import { PhotoWithFallback } from '../elements';
|
|
3
3
|
import type { WebsitePhotos } from '../../types/api/website-photos';
|
|
4
4
|
import type { FormDefinition } from '../../types/api/form';
|
|
5
|
+
import type { SiteConfig } from '../../types/config';
|
|
5
6
|
import { ContactSectionForm } from './contact-section-form.barelux';
|
|
6
7
|
|
|
8
|
+
function getLegalUrlsFromConfig(config: SiteConfig | null | undefined): { privacyPolicyUrl?: string; termsOfServiceUrl?: string } {
|
|
9
|
+
if (!config?.navigation?.footer) return {};
|
|
10
|
+
const flat = config.navigation.footer.flat();
|
|
11
|
+
const privacy = flat.find((l) => l.label === 'Privacy Policy')?.href;
|
|
12
|
+
const terms = flat.find((l) => l.label === 'Terms of Service')?.href;
|
|
13
|
+
return { privacyPolicyUrl: privacy, termsOfServiceUrl: terms };
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
interface ContactSectionProps {
|
|
8
17
|
websitePhotos?: WebsitePhotos | null;
|
|
9
18
|
title?: string;
|
|
10
19
|
subtitle?: string;
|
|
11
20
|
formDefinition?: FormDefinition | null;
|
|
21
|
+
config?: SiteConfig | null;
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
const ContactSection = ({
|
|
@@ -16,7 +26,9 @@ const ContactSection = ({
|
|
|
16
26
|
title = "",
|
|
17
27
|
subtitle = "",
|
|
18
28
|
formDefinition,
|
|
29
|
+
config,
|
|
19
30
|
}: ContactSectionProps) => {
|
|
31
|
+
const { privacyPolicyUrl, termsOfServiceUrl } = getLegalUrlsFromConfig(config);
|
|
20
32
|
const contactPhoto = websitePhotos?.contact;
|
|
21
33
|
const contactImageUrl = contactPhoto?.url;
|
|
22
34
|
const finalContactImage = contactImageUrl && contactImageUrl.trim() !== "" ? contactImageUrl : undefined;
|
|
@@ -35,7 +47,11 @@ const ContactSection = ({
|
|
|
35
47
|
{subtitle}
|
|
36
48
|
</p>
|
|
37
49
|
|
|
38
|
-
<ContactSectionForm
|
|
50
|
+
<ContactSectionForm
|
|
51
|
+
formDefinition={formDefinition}
|
|
52
|
+
privacyPolicyUrl={privacyPolicyUrl}
|
|
53
|
+
termsOfServiceUrl={termsOfServiceUrl}
|
|
54
|
+
/>
|
|
39
55
|
</div>
|
|
40
56
|
|
|
41
57
|
{/* Right Column - Image (full height next to form) */}
|
|
@@ -2,14 +2,25 @@ import React from 'react';
|
|
|
2
2
|
import { PhotoWithFallback } from '../elements';
|
|
3
3
|
import type { WebsitePhotos } from '../../types/api/website-photos';
|
|
4
4
|
import type { FormDefinition } from '../../types/api/form';
|
|
5
|
+
import type { SiteConfig } from '../../types/config';
|
|
5
6
|
import { ContactSectionForm } from './contact-section-form';
|
|
6
7
|
|
|
8
|
+
function getLegalUrlsFromConfig(config: SiteConfig | null | undefined): { privacyPolicyUrl?: string; termsOfServiceUrl?: string } {
|
|
9
|
+
if (!config?.navigation?.footer) return {};
|
|
10
|
+
const flat = config.navigation.footer.flat();
|
|
11
|
+
const privacy = flat.find((l) => l.label === 'Privacy Policy')?.href;
|
|
12
|
+
const terms = flat.find((l) => l.label === 'Terms of Service')?.href;
|
|
13
|
+
return { privacyPolicyUrl: privacy, termsOfServiceUrl: terms };
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
interface ContactSectionProps {
|
|
8
17
|
websitePhotos?: WebsitePhotos | null;
|
|
9
18
|
title?: string;
|
|
10
19
|
subtitle?: string;
|
|
11
20
|
/** When provided, contact form fields are rendered from this definition. */
|
|
12
21
|
formDefinition?: FormDefinition | null;
|
|
22
|
+
/** Optional site config; used to derive Privacy Policy / Terms of Service URLs for the consent checkbox links. */
|
|
23
|
+
config?: SiteConfig | null;
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
const ContactSection = ({
|
|
@@ -17,7 +28,9 @@ const ContactSection = ({
|
|
|
17
28
|
title = "",
|
|
18
29
|
subtitle = "",
|
|
19
30
|
formDefinition,
|
|
31
|
+
config,
|
|
20
32
|
}: ContactSectionProps) => {
|
|
33
|
+
const { privacyPolicyUrl, termsOfServiceUrl } = getLegalUrlsFromConfig(config);
|
|
21
34
|
const contactPhoto = websitePhotos?.contact;
|
|
22
35
|
const contactImageUrl = contactPhoto?.url;
|
|
23
36
|
const finalContactImage = contactImageUrl && contactImageUrl.trim() !== "" ? contactImageUrl : undefined;
|
|
@@ -38,7 +51,11 @@ const ContactSection = ({
|
|
|
38
51
|
</p>
|
|
39
52
|
)}
|
|
40
53
|
</div>
|
|
41
|
-
<ContactSectionForm
|
|
54
|
+
<ContactSectionForm
|
|
55
|
+
formDefinition={formDefinition}
|
|
56
|
+
privacyPolicyUrl={privacyPolicyUrl}
|
|
57
|
+
termsOfServiceUrl={termsOfServiceUrl}
|
|
58
|
+
/>
|
|
42
59
|
</div>
|
|
43
60
|
|
|
44
61
|
<div className="max-lg:hidden h-full min-h-0">
|
package/src/types/api/form.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface FormDefinition {
|
|
|
22
22
|
/** Ordered array; each element is a field or an array of fields (inline row). */
|
|
23
23
|
fields: FormFieldItem[];
|
|
24
24
|
settings?: Record<string, unknown>;
|
|
25
|
+
/** Business name for consent copy (e.g. "{{company_name}}" in checkbox labels). */
|
|
26
|
+
company_name?: string;
|
|
25
27
|
created_at?: string;
|
|
26
28
|
updated_at?: string;
|
|
27
29
|
}
|
|
@@ -14,6 +14,7 @@ export interface WebsitePhotos {
|
|
|
14
14
|
contact?: WebsitePhoto | null;
|
|
15
15
|
about?: WebsitePhoto | null;
|
|
16
16
|
careers?: WebsitePhoto | null;
|
|
17
|
+
preview_image?: WebsitePhoto | null;
|
|
17
18
|
stock_photos?: WebsitePhoto[]; // Array of 'none' type photos for stub mode fallback
|
|
18
19
|
}
|
|
19
20
|
|