keystone-design-bootstrap 1.0.43 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-design-bootstrap",
3
- "version": "1.0.43",
3
+ "version": "1.0.44",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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
- {renderField(f, i, showCountryCode, selectedCountryPhone, handleCountryChange, phoneValues, setPhoneValues)}
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
- {renderField(item, index, showCountryCode, selectedCountryPhone, handleCountryChange, phoneValues, setPhoneValues)}
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 && hasPhoneField && <PrivacyCheckbox />}
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 !== 'privacy' && typeof value === 'string') data[key] = value;
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 form={resolvedFormDefinition} />
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 !== 'privacy' && typeof value === 'string') data[key] = value;
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 form={resolvedFormDefinition} />
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 !== 'privacy' && typeof value === 'string') data[key] = value;
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 form={resolvedFormDefinition} />
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 !== 'privacy' && typeof value === 'string') data[key] = value;
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 form={resolvedFormDefinition} />
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 formDefinition={formDefinition} />
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 formDefinition={formDefinition} />
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 formDefinition={formDefinition} />
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 formDefinition={formDefinition} />
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">
@@ -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
  }