keystone-design-bootstrap 1.0.63 → 1.0.65

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.
Files changed (36) hide show
  1. package/README.md +31 -0
  2. package/dist/design_system/components/DynamicFormFields.d.ts +15 -0
  3. package/dist/design_system/components/DynamicFormFields.js +5049 -0
  4. package/dist/design_system/components/DynamicFormFields.js.map +1 -0
  5. package/dist/design_system/sections/index.d.ts +2 -1
  6. package/dist/design_system/sections/index.js +78 -31
  7. package/dist/design_system/sections/index.js.map +1 -1
  8. package/dist/form-C94A_PX_.d.ts +36 -0
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +78 -31
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/server-api.d.ts +6 -3
  13. package/dist/lib/server-api.js +4 -0
  14. package/dist/lib/server-api.js.map +1 -1
  15. package/dist/{package-ByCAZw9u.d.ts → package-DeHKpQp7.d.ts} +1 -30
  16. package/dist/tracking/index.d.ts +52 -0
  17. package/dist/tracking/index.js +175 -0
  18. package/dist/tracking/index.js.map +1 -0
  19. package/dist/types/index.d.ts +2 -1
  20. package/package.json +2 -1
  21. package/src/design_system/components/DynamicFormFields.tsx +100 -24
  22. package/src/design_system/portal/PortalPage.tsx +42 -33
  23. package/src/design_system/sections/contact-section-form.aman.tsx +1 -1
  24. package/src/design_system/sections/contact-section-form.balance.tsx +1 -1
  25. package/src/design_system/sections/contact-section-form.barelux.tsx +1 -1
  26. package/src/design_system/sections/contact-section-form.tsx +1 -1
  27. package/src/design_system/sections/header-navigation.aman.tsx +14 -3
  28. package/src/design_system/sections/header-navigation.balance.tsx +14 -3
  29. package/src/design_system/sections/header-navigation.barelux.tsx +14 -3
  30. package/src/design_system/sections/header-navigation.tsx +14 -3
  31. package/src/lib/server-api.ts +7 -1
  32. package/src/next/routes/form.ts +16 -0
  33. package/src/next/routes/proxy-headers.ts +26 -4
  34. package/src/tracking/firePixelEvent.ts +11 -3
  35. package/src/tracking/index.ts +20 -0
  36. package/src/types/api/form.ts +7 -0
@@ -1,4 +1,5 @@
1
- import { C as CompanyInformation, F as FormDefinition, P as Package, S as Service } from '../package-ByCAZw9u.js';
1
+ import { F as FormDefinition } from '../form-C94A_PX_.js';
2
+ import { C as CompanyInformation, P as Package, S as Service } from '../package-DeHKpQp7.js';
2
3
  import { W as WebsitePhotos } from '../website-photos-Cl1YqAno.js';
3
4
  import '../photos-CmBdWiuZ.js';
4
5
 
@@ -38,7 +39,9 @@ declare function getSocialPosts(): Promise<unknown>;
38
39
  declare function getPackages(): Promise<Package[] | null>;
39
40
  declare function getPackage(slug: string): Promise<Package | null>;
40
41
  declare function getTestimonials(): Promise<unknown>;
41
- /** Form definition for dynamic form rendering (fields may include optional placeholder). */
42
+ /** Form definition for dynamic form rendering, fetched by form_type. */
42
43
  declare function getForm(formType: string): Promise<FormDefinition | null>;
44
+ /** Form definition for dynamic form rendering, fetched by numeric ID. */
45
+ declare function getFormById(id: number | string): Promise<FormDefinition | null>;
43
46
 
44
- export { getAdsConfig, getBlogPost, getBlogPosts, getCompanyInformation, getFAQs, getForm, getJobPosting, getJobPostings, getLocation, getLocations, getMetaPixelId, getPackage, getPackages, getReviews, getService, getServices, getSocialPosts, getTeamMembers, getTestimonials, getWebsitePhotos, serverApi };
47
+ export { getAdsConfig, getBlogPost, getBlogPosts, getCompanyInformation, getFAQs, getForm, getFormById, getJobPosting, getJobPostings, getLocation, getLocations, getMetaPixelId, getPackage, getPackages, getReviews, getService, getServices, getSocialPosts, getTeamMembers, getTestimonials, getWebsitePhotos, serverApi };
@@ -96,6 +96,9 @@ async function getTestimonials() {
96
96
  async function getForm(formType) {
97
97
  return serverFetch(`/public/forms/${encodeURIComponent(formType)}`, defaultOptions);
98
98
  }
99
+ async function getFormById(id) {
100
+ return serverFetch(`/public/form_by_id/${encodeURIComponent(id)}`, defaultOptions);
101
+ }
99
102
  export {
100
103
  getAdsConfig,
101
104
  getBlogPost,
@@ -103,6 +106,7 @@ export {
103
106
  getCompanyInformation,
104
107
  getFAQs,
105
108
  getForm,
109
+ getFormById,
106
110
  getJobPosting,
107
111
  getJobPostings,
108
112
  getLocation,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/server-api.ts"],"sourcesContent":["/**\n * Server-side API client for SSR\n * API key is stored securely on the server - never exposed to browser\n */\n\nimport type { CompanyInformation } from '../types/api/company-information';\nimport type { Service } from '../types/api/service';\nimport type { Package } from '../types/api/package';\nimport type { WebsitePhotos } from '../types/api/website-photos';\n\nconst API_URL = process.env.API_URL || 'http://localhost:3000/api/v1';\nconst API_KEY = process.env.API_KEY || '';\n\ninterface FetchOptions {\n cache?: RequestCache;\n revalidate?: number;\n}\n\nasync function serverFetch<T>(endpoint: string, options: FetchOptions = {}): Promise<T | null> {\n const url = `${API_URL}${endpoint}`;\n \n try {\n const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {\n headers: {\n 'X-API-Key': API_KEY,\n 'Content-Type': 'application/json',\n },\n cache: options.cache,\n };\n \n if (options.revalidate) {\n fetchOptions.next = { revalidate: options.revalidate };\n }\n \n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n console.error(`[Server API] Error ${response.status} for ${endpoint}`);\n return null;\n }\n\n const json = await response.json();\n \n // Rails API returns { data: [...], meta: {...} }\n return json.data ?? json;\n } catch (error) {\n console.error(`[Server API] Failed to fetch ${endpoint}:`, error);\n return null;\n }\n}\n\n/**\n * Generic serverApi object for flexible endpoint access\n */\nexport const serverApi = {\n get: <T = unknown>(endpoint: string, options?: FetchOptions): Promise<T | null> => {\n return serverFetch<T>(endpoint, options || { revalidate: 60 });\n }\n};\n\n// Revalidate data every 60 seconds (ISR)\nconst defaultOptions: FetchOptions = { revalidate: 60 };\n\nexport async function getCompanyInformation(): Promise<CompanyInformation | null> {\n return serverFetch<CompanyInformation>('/public/company_information', defaultOptions);\n}\n\n/** Ads config (e.g. Meta Pixel). Returns { meta_pixel_id?: string } or {}. Only present when account has connected Meta Ads. */\nexport async function getAdsConfig(): Promise<{ meta_pixel_id?: string } | null> {\n const data = await serverFetch<{ meta_pixel_id?: string }>('/public/ads_config', defaultOptions);\n return data ?? null;\n}\n\n/** Extract Meta Pixel ID from ads config for use with <MetaPixel pixelId={...} />. Only returns a value when present and valid (numeric). */\nexport function getMetaPixelId(adsConfig: { meta_pixel_id?: string } | null | undefined): string | null {\n const id = adsConfig && typeof adsConfig === 'object' && 'meta_pixel_id' in adsConfig && adsConfig.meta_pixel_id;\n const str = id != null && id !== '' ? String(id).trim() : '';\n return str !== '' && str !== 'null' && /^\\d+$/.test(str) ? str : null;\n}\n\nexport async function getServices(): Promise<Service[] | null> {\n return serverFetch<Service[]>('/public/services', defaultOptions);\n}\n\nexport async function getService(slug: string): Promise<Service | null> {\n return serverFetch<Service>(`/public/services/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getLocations() {\n return serverFetch('/public/locations', defaultOptions);\n}\n\nexport async function getLocation(slug: string) {\n return serverFetch(`/public/locations/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getReviews() {\n return serverFetch('/public/reviews', defaultOptions);\n}\n\nexport async function getFAQs() {\n return serverFetch('/public/faq_questions', defaultOptions);\n}\n\nexport async function getBlogPosts() {\n return serverFetch('/public/blog_posts', defaultOptions);\n}\n\nexport async function getBlogPost(slug: string) {\n return serverFetch(`/public/blog_posts/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getTeamMembers() {\n return serverFetch('/public/team_members', defaultOptions);\n}\n\nexport async function getWebsitePhotos(): Promise<WebsitePhotos | null> {\n return serverFetch<WebsitePhotos>('/public/website_photos', defaultOptions);\n}\n\nexport async function getJobPostings() {\n return serverFetch('/public/job_postings', defaultOptions);\n}\n\nexport async function getJobPosting(slug: string) {\n return serverFetch(`/public/job_postings/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getSocialPosts() {\n return serverFetch('/public/social_posts', defaultOptions);\n}\n\n/** Packages (bundles of service items). */\nexport async function getPackages(): Promise<Package[] | null> {\n return serverFetch<Package[]>('/public/packages', defaultOptions);\n}\n\nexport async function getPackage(slug: string): Promise<Package | null> {\n return serverFetch<Package>(`/public/packages/by_slug/${encodeURIComponent(slug)}`, defaultOptions);\n}\n\n// Alias for testimonials (API uses \"reviews\")\nexport async function getTestimonials() {\n return getReviews();\n}\n\n/** Form definition for dynamic form rendering (fields may include optional placeholder). */\nexport async function getForm(formType: string) {\n type FormDefinition = import('../types/api/form').FormDefinition;\n return serverFetch<FormDefinition>(`/public/forms/${encodeURIComponent(formType)}`, defaultOptions);\n}\n\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/lib/server-api.ts"],"sourcesContent":["/**\n * Server-side API client for SSR\n * API key is stored securely on the server - never exposed to browser\n */\n\nimport type { CompanyInformation } from '../types/api/company-information';\nimport type { Service } from '../types/api/service';\nimport type { Package } from '../types/api/package';\nimport type { WebsitePhotos } from '../types/api/website-photos';\n\nconst API_URL = process.env.API_URL || 'http://localhost:3000/api/v1';\nconst API_KEY = process.env.API_KEY || '';\n\ninterface FetchOptions {\n cache?: RequestCache;\n revalidate?: number;\n}\n\nasync function serverFetch<T>(endpoint: string, options: FetchOptions = {}): Promise<T | null> {\n const url = `${API_URL}${endpoint}`;\n \n try {\n const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {\n headers: {\n 'X-API-Key': API_KEY,\n 'Content-Type': 'application/json',\n },\n cache: options.cache,\n };\n \n if (options.revalidate) {\n fetchOptions.next = { revalidate: options.revalidate };\n }\n \n const response = await fetch(url, fetchOptions);\n\n if (!response.ok) {\n console.error(`[Server API] Error ${response.status} for ${endpoint}`);\n return null;\n }\n\n const json = await response.json();\n \n // Rails API returns { data: [...], meta: {...} }\n return json.data ?? json;\n } catch (error) {\n console.error(`[Server API] Failed to fetch ${endpoint}:`, error);\n return null;\n }\n}\n\n/**\n * Generic serverApi object for flexible endpoint access\n */\nexport const serverApi = {\n get: <T = unknown>(endpoint: string, options?: FetchOptions): Promise<T | null> => {\n return serverFetch<T>(endpoint, options || { revalidate: 60 });\n }\n};\n\n// Revalidate data every 60 seconds (ISR)\nconst defaultOptions: FetchOptions = { revalidate: 60 };\n\nexport async function getCompanyInformation(): Promise<CompanyInformation | null> {\n return serverFetch<CompanyInformation>('/public/company_information', defaultOptions);\n}\n\n/** Ads config (e.g. Meta Pixel). Returns { meta_pixel_id?: string } or {}. Only present when account has connected Meta Ads. */\nexport async function getAdsConfig(): Promise<{ meta_pixel_id?: string } | null> {\n const data = await serverFetch<{ meta_pixel_id?: string }>('/public/ads_config', defaultOptions);\n return data ?? null;\n}\n\n/** Extract Meta Pixel ID from ads config for use with <MetaPixel pixelId={...} />. Only returns a value when present and valid (numeric). */\nexport function getMetaPixelId(adsConfig: { meta_pixel_id?: string } | null | undefined): string | null {\n const id = adsConfig && typeof adsConfig === 'object' && 'meta_pixel_id' in adsConfig && adsConfig.meta_pixel_id;\n const str = id != null && id !== '' ? String(id).trim() : '';\n return str !== '' && str !== 'null' && /^\\d+$/.test(str) ? str : null;\n}\n\nexport async function getServices(): Promise<Service[] | null> {\n return serverFetch<Service[]>('/public/services', defaultOptions);\n}\n\nexport async function getService(slug: string): Promise<Service | null> {\n return serverFetch<Service>(`/public/services/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getLocations() {\n return serverFetch('/public/locations', defaultOptions);\n}\n\nexport async function getLocation(slug: string) {\n return serverFetch(`/public/locations/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getReviews() {\n return serverFetch('/public/reviews', defaultOptions);\n}\n\nexport async function getFAQs() {\n return serverFetch('/public/faq_questions', defaultOptions);\n}\n\nexport async function getBlogPosts() {\n return serverFetch('/public/blog_posts', defaultOptions);\n}\n\nexport async function getBlogPost(slug: string) {\n return serverFetch(`/public/blog_posts/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getTeamMembers() {\n return serverFetch('/public/team_members', defaultOptions);\n}\n\nexport async function getWebsitePhotos(): Promise<WebsitePhotos | null> {\n return serverFetch<WebsitePhotos>('/public/website_photos', defaultOptions);\n}\n\nexport async function getJobPostings() {\n return serverFetch('/public/job_postings', defaultOptions);\n}\n\nexport async function getJobPosting(slug: string) {\n return serverFetch(`/public/job_postings/by_slug/${slug}`, defaultOptions);\n}\n\nexport async function getSocialPosts() {\n return serverFetch('/public/social_posts', defaultOptions);\n}\n\n/** Packages (bundles of service items). */\nexport async function getPackages(): Promise<Package[] | null> {\n return serverFetch<Package[]>('/public/packages', defaultOptions);\n}\n\nexport async function getPackage(slug: string): Promise<Package | null> {\n return serverFetch<Package>(`/public/packages/by_slug/${encodeURIComponent(slug)}`, defaultOptions);\n}\n\n// Alias for testimonials (API uses \"reviews\")\nexport async function getTestimonials() {\n return getReviews();\n}\n\n/** Form definition for dynamic form rendering, fetched by form_type. */\nexport async function getForm(formType: string) {\n type FormDefinition = import('../types/api/form').FormDefinition;\n return serverFetch<FormDefinition>(`/public/forms/${encodeURIComponent(formType)}`, defaultOptions);\n}\n\n/** Form definition for dynamic form rendering, fetched by numeric ID. */\nexport async function getFormById(id: number | string) {\n type FormDefinition = import('../types/api/form').FormDefinition;\n return serverFetch<FormDefinition>(`/public/form_by_id/${encodeURIComponent(id)}`, defaultOptions);\n}\n\n"],"mappings":"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","names":[]}
@@ -96,35 +96,6 @@ interface CompanyInformation {
96
96
  }
97
97
  type CompanyInformationResponse = CompanyInformation | null;
98
98
 
99
- /**
100
- * Form definition from API (GET /public/forms/:form_type).
101
- * fields is an ordered array; each item is a single field or an array of fields (inline row). Max depth 2.
102
- */
103
- interface FormFieldDefinition {
104
- name: string;
105
- type: string;
106
- label: string;
107
- required?: boolean;
108
- placeholder?: string;
109
- /** For hidden fields, optional value to submit. */
110
- value?: string;
111
- }
112
- /** One item in the fields array: either a single field or a row of fields (inline). */
113
- type FormFieldItem = FormFieldDefinition | FormFieldDefinition[];
114
- interface FormDefinition {
115
- id: number;
116
- name: string;
117
- form_type: string;
118
- /** Ordered array; each element is a field or an array of fields (inline row). */
119
- fields: FormFieldItem[];
120
- settings?: Record<string, unknown>;
121
- /** Business name for consent copy (e.g. "{{company_name}}" in checkbox labels). */
122
- company_name?: string;
123
- created_at?: string;
124
- updated_at?: string;
125
- }
126
- type FormType = 'lead' | 'job_application' | 'marketing_list_signup';
127
-
128
99
  interface PackageItem {
129
100
  quantity: number;
130
101
  service_item?: {
@@ -147,4 +118,4 @@ interface Package {
147
118
  offers?: OfferPublic[];
148
119
  }
149
120
 
150
- export type { CompanyInformation as C, FormDefinition as F, OfferPublic as O, Package as P, Service as S, CompanyInformationResponse as a, FormFieldDefinition as b, FormFieldItem as c, FormType as d, PackageItem as e, ServiceItem as f, ServiceParams as g, ServiceResponse as h };
121
+ export type { CompanyInformation as C, OfferPublic as O, Package as P, Service as S, CompanyInformationResponse as a, PackageItem as b, ServiceItem as c, ServiceParams as d, ServiceResponse as e };
@@ -0,0 +1,52 @@
1
+ import * as React from 'react';
2
+
3
+ type MetaPixelProps = {
4
+ /** Meta Pixel ID. When null/undefined, nothing is rendered. */
5
+ pixelId: string | null | undefined;
6
+ };
7
+ /**
8
+ * Renders the Meta (Facebook) Pixel base code: loads fbevents.js, initializes
9
+ * the pixel, and tracks PageView. Use in the root layout when ads config
10
+ * provides a meta_pixel_id.
11
+ */
12
+ declare function MetaPixel({ pixelId }: MetaPixelProps): React.JSX.Element | null;
13
+
14
+ type Props = {
15
+ /** External booking URL. When set, fires InitiateCheckout on any click targeting that URL. */
16
+ bookingUrl?: string | null;
17
+ };
18
+ /**
19
+ * Single client-side tracker placed once in KeystoneRootLayout.
20
+ * - Fires ViewContent on every route change for known page patterns.
21
+ * - Fires InitiateCheckout whenever a visitor clicks a link to the external booking URL.
22
+ *
23
+ * Portal booking tab tracking is handled directly inside PortalPage via PortalTabTracker
24
+ * since intent is only established when the Booking tab is explicitly opened.
25
+ */
26
+ declare function MetaPixelTracker({ bookingUrl }: Props): null;
27
+
28
+ type PixelEvent = 'PageView' | 'ViewContent' | 'InitiateCheckout' | 'Lead';
29
+ interface PixelEventParams {
30
+ contentName?: string;
31
+ contentCategory?: string;
32
+ }
33
+ /** Raw (unhashed) user identifiers for advanced matching. */
34
+ interface PixelUserData {
35
+ email?: string | null;
36
+ phone?: string | null;
37
+ }
38
+ /**
39
+ * Hash and store user identifiers so they are automatically included in all
40
+ * subsequent pixel events for this browser session. Call this as soon as
41
+ * identity is known (e.g. contact form submission, portal login step 1).
42
+ */
43
+ declare function setPixelUserData(userData: PixelUserData): Promise<void>;
44
+ /**
45
+ * Single entry point for all client-side Meta Pixel event fires.
46
+ * Automatically applies any stored user identity before firing so that Meta
47
+ * can match events to known users across the entire session.
48
+ * Silently no-ops if fbq is not loaded (pixel not configured for this site).
49
+ */
50
+ declare function firePixelEvent(event: PixelEvent, params?: PixelEventParams): void;
51
+
52
+ export { MetaPixel, type MetaPixelProps, MetaPixelTracker, type PixelEvent, type PixelEventParams, type PixelUserData, firePixelEvent, setPixelUserData };
@@ -0,0 +1,175 @@
1
+ // src/tracking/MetaPixel.tsx
2
+ import Script from "next/script";
3
+ var FBEVENTS_URL = "https://connect.facebook.net/en_US/fbevents.js";
4
+ var PIXEL_SCRIPT = (pixelId) => `
5
+ !function(f,b,e,v,n,t,s)
6
+ {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
7
+ n.callMethod.apply(n,arguments):n.queue.push(arguments)};
8
+ if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
9
+ n.queue=[];t=b.createElement(e);t.async=!0;
10
+ t.src=v;s=b.getElementsByTagName(e)[0];
11
+ s.parentNode.insertBefore(t,s)}(window, document,'script','${FBEVENTS_URL}');
12
+ fbq('init', '${pixelId.replace(/'/g, "\\'")}');
13
+ fbq('track', 'PageView');
14
+ window.__ks_pixel_ids = window.__ks_pixel_ids || [];
15
+ if (window.__ks_pixel_ids.indexOf('${pixelId.replace(/'/g, "\\'")}') === -1) {
16
+ window.__ks_pixel_ids.push('${pixelId.replace(/'/g, "\\'")}');
17
+ }
18
+ `;
19
+ function MetaPixel({ pixelId }) {
20
+ const raw = typeof pixelId === "string" ? pixelId.trim() : "";
21
+ const id = raw && raw !== "null" && /^\d+$/.test(raw) ? raw : "";
22
+ if (!id) {
23
+ return null;
24
+ }
25
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
26
+ Script,
27
+ {
28
+ id: "meta-pixel",
29
+ strategy: "afterInteractive",
30
+ dangerouslySetInnerHTML: { __html: PIXEL_SCRIPT(id) }
31
+ }
32
+ ), /* @__PURE__ */ React.createElement("noscript", null, /* @__PURE__ */ React.createElement(
33
+ "img",
34
+ {
35
+ height: 1,
36
+ width: 1,
37
+ style: { display: "none" },
38
+ src: `https://www.facebook.com/tr?id=${id}&ev=PageView&noscript=1`,
39
+ alt: ""
40
+ }
41
+ )));
42
+ }
43
+
44
+ // src/tracking/MetaPixelTracker.tsx
45
+ import { useEffect } from "react";
46
+ import { usePathname } from "next/navigation";
47
+
48
+ // src/tracking/firePixelEvent.ts
49
+ var STORAGE_KEY = "ks_pud";
50
+ async function sha256Hex(str) {
51
+ const buffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
52
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
53
+ }
54
+ function getFbq() {
55
+ if (typeof window === "undefined") return void 0;
56
+ return window.fbq;
57
+ }
58
+ function getRegisteredPixelIds() {
59
+ var _a;
60
+ return (_a = window.__ks_pixel_ids) != null ? _a : [];
61
+ }
62
+ function applyStoredUserData(fbq) {
63
+ try {
64
+ const raw = sessionStorage.getItem(STORAGE_KEY);
65
+ if (!raw) return;
66
+ const hashed = JSON.parse(raw);
67
+ if (Object.keys(hashed).length === 0) return;
68
+ getRegisteredPixelIds().forEach((id) => fbq("init", id, hashed));
69
+ } catch (e) {
70
+ }
71
+ }
72
+ async function setPixelUserData(userData) {
73
+ const hashed = {};
74
+ if (userData.email) {
75
+ hashed.em = await sha256Hex(userData.email.trim().toLowerCase());
76
+ }
77
+ if (userData.phone) {
78
+ const digits = userData.phone.replace(/\D/g, "");
79
+ if (digits) hashed.ph = await sha256Hex(digits);
80
+ }
81
+ if (Object.keys(hashed).length === 0) return;
82
+ try {
83
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(hashed));
84
+ } catch (e) {
85
+ }
86
+ const fbq = getFbq();
87
+ if (fbq) {
88
+ getRegisteredPixelIds().forEach((id) => fbq("init", id, hashed));
89
+ }
90
+ }
91
+ function firePixelEvent(event, params) {
92
+ const fbq = getFbq();
93
+ if (!fbq) {
94
+ console.debug("[MetaPixel] skipped \u2014 fbq not loaded", { event });
95
+ return;
96
+ }
97
+ applyStoredUserData(fbq);
98
+ const normalized = {};
99
+ if (params == null ? void 0 : params.contentName) normalized.content_name = params.contentName;
100
+ if (params == null ? void 0 : params.contentCategory) normalized.content_category = params.contentCategory;
101
+ console.debug("[MetaPixel]", event, normalized);
102
+ fbq("track", event, Object.keys(normalized).length > 0 ? normalized : void 0);
103
+ }
104
+
105
+ // src/tracking/MetaPixelTracker.tsx
106
+ function slugToTitle(slug) {
107
+ return slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
108
+ }
109
+ var ROUTE_RULES = [
110
+ {
111
+ pattern: /^\/services\/(.+)$/,
112
+ getParams: ([, slug]) => ({ contentName: slugToTitle(slug), contentCategory: "Service" })
113
+ },
114
+ {
115
+ pattern: /^\/locations\/(.+)$/,
116
+ getParams: ([, slug]) => ({ contentName: slugToTitle(slug), contentCategory: "Location" })
117
+ },
118
+ {
119
+ pattern: /^\/services$/,
120
+ getParams: () => ({ contentName: "Services", contentCategory: "Services" })
121
+ },
122
+ {
123
+ pattern: /^\/locations$/,
124
+ getParams: () => ({ contentName: "Locations", contentCategory: "Locations" })
125
+ },
126
+ {
127
+ pattern: /^\/portal$/,
128
+ getParams: () => ({ contentName: "Member Portal", contentCategory: "Pricing" })
129
+ },
130
+ {
131
+ pattern: /^\/service-menu$/,
132
+ getParams: () => ({ contentName: "Service Menu", contentCategory: "Pricing" })
133
+ },
134
+ {
135
+ pattern: /^\/faq$/,
136
+ getParams: () => ({ contentName: "FAQ", contentCategory: "FAQ" })
137
+ },
138
+ {
139
+ pattern: /^\/contact$/,
140
+ getParams: () => ({ contentName: "Contact", contentCategory: "Contact" })
141
+ }
142
+ ];
143
+ function MetaPixelTracker({ bookingUrl }) {
144
+ const pathname = usePathname();
145
+ useEffect(() => {
146
+ for (const rule of ROUTE_RULES) {
147
+ const match = pathname.match(rule.pattern);
148
+ if (match) {
149
+ const { contentName, contentCategory } = rule.getParams(match);
150
+ firePixelEvent("ViewContent", { contentName, contentCategory });
151
+ break;
152
+ }
153
+ }
154
+ }, [pathname]);
155
+ useEffect(() => {
156
+ if (!bookingUrl) return;
157
+ const handleClick = (e) => {
158
+ var _a;
159
+ const anchor = e.target.closest("a");
160
+ if ((_a = anchor == null ? void 0 : anchor.href) == null ? void 0 : _a.startsWith(bookingUrl)) {
161
+ firePixelEvent("InitiateCheckout");
162
+ }
163
+ };
164
+ document.addEventListener("click", handleClick);
165
+ return () => document.removeEventListener("click", handleClick);
166
+ }, [bookingUrl]);
167
+ return null;
168
+ }
169
+ export {
170
+ MetaPixel,
171
+ MetaPixelTracker,
172
+ firePixelEvent,
173
+ setPixelUserData
174
+ };
175
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tracking/MetaPixel.tsx","../../src/tracking/MetaPixelTracker.tsx","../../src/tracking/firePixelEvent.ts"],"sourcesContent":["'use client';\n\nimport Script from 'next/script';\n\nconst FBEVENTS_URL = 'https://connect.facebook.net/en_US/fbevents.js';\n\nconst PIXEL_SCRIPT = (pixelId: string) => `\n !function(f,b,e,v,n,t,s)\n {if(f.fbq)return;n=f.fbq=function(){n.callMethod?\n n.callMethod.apply(n,arguments):n.queue.push(arguments)};\n if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';\n n.queue=[];t=b.createElement(e);t.async=!0;\n t.src=v;s=b.getElementsByTagName(e)[0];\n s.parentNode.insertBefore(t,s)}(window, document,'script','${FBEVENTS_URL}');\n fbq('init', '${pixelId.replace(/'/g, \"\\\\'\")}');\n fbq('track', 'PageView');\n window.__ks_pixel_ids = window.__ks_pixel_ids || [];\n if (window.__ks_pixel_ids.indexOf('${pixelId.replace(/'/g, \"\\\\'\")}') === -1) {\n window.__ks_pixel_ids.push('${pixelId.replace(/'/g, \"\\\\'\")}');\n }\n`;\n\nexport type MetaPixelProps = {\n /** Meta Pixel ID. When null/undefined, nothing is rendered. */\n pixelId: string | null | undefined;\n};\n\n/**\n * Renders the Meta (Facebook) Pixel base code: loads fbevents.js, initializes\n * the pixel, and tracks PageView. Use in the root layout when ads config\n * provides a meta_pixel_id.\n */\nexport function MetaPixel({ pixelId }: MetaPixelProps) {\n const raw = typeof pixelId === 'string' ? pixelId.trim() : '';\n const id = raw && raw !== 'null' && /^\\d+$/.test(raw) ? raw : '';\n if (!id) {\n return null;\n }\n\n return (\n <>\n <Script\n id=\"meta-pixel\"\n strategy=\"afterInteractive\"\n dangerouslySetInnerHTML={{ __html: PIXEL_SCRIPT(id) }}\n />\n <noscript>\n {/* eslint-disable-next-line @next/next/no-img-element -- 1x1 tracking pixel for no-JS fallback; next/image not applicable in noscript */}\n <img\n height={1}\n width={1}\n style={{ display: 'none' }}\n src={`https://www.facebook.com/tr?id=${id}&ev=PageView&noscript=1`}\n alt=\"\"\n />\n </noscript>\n </>\n );\n}\n","'use client';\n\nimport { useEffect } from 'react';\nimport { usePathname } from 'next/navigation';\nimport { firePixelEvent } from './firePixelEvent';\n\ntype RouteRule = {\n pattern: RegExp;\n getParams: (match: RegExpMatchArray) => { contentName: string; contentCategory: string };\n};\n\nfunction slugToTitle(slug: string): string {\n return slug\n .split('-')\n .map((w) => w.charAt(0).toUpperCase() + w.slice(1))\n .join(' ');\n}\n\n// Checked in order — first match wins. More specific patterns come first.\nconst ROUTE_RULES: RouteRule[] = [\n {\n pattern: /^\\/services\\/(.+)$/,\n getParams: ([, slug]) => ({ contentName: slugToTitle(slug), contentCategory: 'Service' }),\n },\n {\n pattern: /^\\/locations\\/(.+)$/,\n getParams: ([, slug]) => ({ contentName: slugToTitle(slug), contentCategory: 'Location' }),\n },\n {\n pattern: /^\\/services$/,\n getParams: () => ({ contentName: 'Services', contentCategory: 'Services' }),\n },\n {\n pattern: /^\\/locations$/,\n getParams: () => ({ contentName: 'Locations', contentCategory: 'Locations' }),\n },\n {\n pattern: /^\\/portal$/,\n getParams: () => ({ contentName: 'Member Portal', contentCategory: 'Pricing' }),\n },\n {\n pattern: /^\\/service-menu$/,\n getParams: () => ({ contentName: 'Service Menu', contentCategory: 'Pricing' }),\n },\n {\n pattern: /^\\/faq$/,\n getParams: () => ({ contentName: 'FAQ', contentCategory: 'FAQ' }),\n },\n {\n pattern: /^\\/contact$/,\n getParams: () => ({ contentName: 'Contact', contentCategory: 'Contact' }),\n },\n];\n\ntype Props = {\n /** External booking URL. When set, fires InitiateCheckout on any click targeting that URL. */\n bookingUrl?: string | null;\n};\n\n/**\n * Single client-side tracker placed once in KeystoneRootLayout.\n * - Fires ViewContent on every route change for known page patterns.\n * - Fires InitiateCheckout whenever a visitor clicks a link to the external booking URL.\n *\n * Portal booking tab tracking is handled directly inside PortalPage via PortalTabTracker\n * since intent is only established when the Booking tab is explicitly opened.\n */\nexport function MetaPixelTracker({ bookingUrl }: Props) {\n const pathname = usePathname();\n\n useEffect(() => {\n for (const rule of ROUTE_RULES) {\n const match = pathname.match(rule.pattern);\n if (match) {\n const { contentName, contentCategory } = rule.getParams(match);\n firePixelEvent('ViewContent', { contentName, contentCategory });\n break;\n }\n }\n }, [pathname]);\n\n useEffect(() => {\n if (!bookingUrl) return;\n\n const handleClick = (e: MouseEvent) => {\n const anchor = (e.target as Element).closest('a');\n if (anchor?.href?.startsWith(bookingUrl)) {\n firePixelEvent('InitiateCheckout');\n }\n };\n\n document.addEventListener('click', handleClick);\n return () => document.removeEventListener('click', handleClick);\n }, [bookingUrl]);\n\n return null;\n}\n","type FbqFn = (method: string, ...args: unknown[]) => void;\n\nexport type PixelEvent = 'PageView' | 'ViewContent' | 'InitiateCheckout' | 'Lead';\n\nexport interface PixelEventParams {\n contentName?: string;\n contentCategory?: string;\n}\n\n/** Raw (unhashed) user identifiers for advanced matching. */\nexport interface PixelUserData {\n email?: string | null;\n phone?: string | null;\n}\n\n// Hashed user data stored in sessionStorage for the duration of the browsing session.\n// Keyed by a short namespace to avoid collisions.\nconst STORAGE_KEY = 'ks_pud';\n\nasync function sha256Hex(str: string): Promise<string> {\n const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));\n return Array.from(new Uint8Array(buffer))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\nfunction getFbq(): FbqFn | undefined {\n if (typeof window === 'undefined') return undefined;\n return (window as Window & { fbq?: FbqFn }).fbq;\n}\n\n/** Read the pixel IDs registered by MetaPixel via the inline init script. */\nfunction getRegisteredPixelIds(): string[] {\n return (window as Window & { __ks_pixel_ids?: string[] }).__ks_pixel_ids ?? [];\n}\n\n/** Apply stored hashed user data to every configured Meta Pixel. */\nfunction applyStoredUserData(fbq: FbqFn): void {\n try {\n const raw = sessionStorage.getItem(STORAGE_KEY);\n if (!raw) return;\n const hashed = JSON.parse(raw) as Record<string, string>;\n if (Object.keys(hashed).length === 0) return;\n getRegisteredPixelIds().forEach((id) => fbq('init', id, hashed));\n } catch {\n // sessionStorage unavailable or JSON malformed — safe to ignore\n }\n}\n\n/**\n * Hash and store user identifiers so they are automatically included in all\n * subsequent pixel events for this browser session. Call this as soon as\n * identity is known (e.g. contact form submission, portal login step 1).\n */\nexport async function setPixelUserData(userData: PixelUserData): Promise<void> {\n const hashed: Record<string, string> = {};\n\n if (userData.email) {\n hashed.em = await sha256Hex(userData.email.trim().toLowerCase());\n }\n if (userData.phone) {\n const digits = userData.phone.replace(/\\D/g, '');\n if (digits) hashed.ph = await sha256Hex(digits);\n }\n\n if (Object.keys(hashed).length === 0) return;\n\n try {\n sessionStorage.setItem(STORAGE_KEY, JSON.stringify(hashed));\n } catch {\n // sessionStorage unavailable — still apply to fbq for this page load\n }\n\n const fbq = getFbq();\n if (fbq) {\n getRegisteredPixelIds().forEach((id) => fbq('init', id, hashed));\n }\n}\n\n/**\n * Single entry point for all client-side Meta Pixel event fires.\n * Automatically applies any stored user identity before firing so that Meta\n * can match events to known users across the entire session.\n * Silently no-ops if fbq is not loaded (pixel not configured for this site).\n */\nexport function firePixelEvent(event: PixelEvent, params?: PixelEventParams): void {\n const fbq = getFbq();\n if (!fbq) {\n console.debug('[MetaPixel] skipped — fbq not loaded', { event });\n return;\n }\n\n // Re-apply stored identity before every event so user data is included\n // even on events that fire after a client-side navigation (PageView, ViewContent, etc.)\n applyStoredUserData(fbq);\n\n const normalized: Record<string, string> = {};\n if (params?.contentName) normalized.content_name = params.contentName;\n if (params?.contentCategory) normalized.content_category = params.contentCategory;\n\n console.debug('[MetaPixel]', event, normalized);\n fbq('track', event, Object.keys(normalized).length > 0 ? normalized : undefined);\n}\n"],"mappings":"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","names":[]}
@@ -1,8 +1,9 @@
1
1
  import { Theme } from '../themes/index.js';
2
2
  export { B as BlogPost, a as BlogPostAuthor, b as BlogPostParams, c as BlogPostResponse, d as BlogPostTag } from '../blog-post-vWzW8yFb.js';
3
- export { C as CompanyInformation, a as CompanyInformationResponse, F as FormDefinition, b as FormFieldDefinition, c as FormFieldItem, d as FormType, O as OfferPublic, P as Package, e as PackageItem, S as Service, f as ServiceItem, g as ServiceParams, h as ServiceResponse } from '../package-ByCAZw9u.js';
3
+ export { C as CompanyInformation, a as CompanyInformationResponse, O as OfferPublic, P as Package, b as PackageItem, S as Service, c as ServiceItem, d as ServiceParams, e as ServiceResponse } from '../package-DeHKpQp7.js';
4
4
  import { P as PhotoAttachment } from '../photos-CmBdWiuZ.js';
5
5
  export { a as Photo } from '../photos-CmBdWiuZ.js';
6
+ export { F as FormDefinition, a as FormFieldDefinition, b as FormFieldItem, c as FormFieldOption, d as FormType } from '../form-C94A_PX_.js';
6
7
  export { a as WebsitePhoto, W as WebsitePhotos, b as WebsitePhotosResponse } from '../website-photos-Cl1YqAno.js';
7
8
 
8
9
  interface NavItem {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-design-bootstrap",
3
- "version": "1.0.63",
3
+ "version": "1.0.65",
4
4
  "description": "Keystone Design Bootstrap - Sections, Elements, and Theme System for customer websites",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -13,6 +13,7 @@
13
13
  "./hooks": "./src/lib/hooks/index.ts",
14
14
  "./contexts": "./src/contexts/index.ts",
15
15
  "./tracking": "./src/tracking/index.ts",
16
+ "./components/DynamicFormFields": "./src/design_system/components/DynamicFormFields.tsx",
16
17
  "./lib/server-api": "./src/lib/server-api.ts",
17
18
  "./lib/cta-urls": "./src/lib/cta-urls.ts",
18
19
  "./lib/component-registry": "./src/lib/component-registry.ts",
@@ -29,6 +29,62 @@ function allFieldsFlat(fields: FormDefinition['fields']): FormFieldDefinition[]
29
29
  return out;
30
30
  }
31
31
 
32
+ // ─── CheckboxGroup ────────────────────────────────────────────────────────────
33
+
34
+ interface CheckboxGroupProps {
35
+ field: FormFieldDefinition;
36
+ }
37
+
38
+ function CheckboxGroup({ field }: CheckboxGroupProps) {
39
+ const [selected, setSelected] = useState<string[]>([]);
40
+ const options = field.options ?? [];
41
+
42
+ const toggle = (value: string) => {
43
+ setSelected((prev) =>
44
+ prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]
45
+ );
46
+ };
47
+
48
+ return (
49
+ <fieldset>
50
+ <legend className="mb-2 font-body text-sm font-medium text-fg-primary">
51
+ {field.label}
52
+ {field.required && <span className="ml-1 text-error-500">*</span>}
53
+ </legend>
54
+
55
+ {/* Hidden input carries the comma-separated value on submit */}
56
+ <input
57
+ type="hidden"
58
+ name={field.name}
59
+ value={selected.join(',')}
60
+ />
61
+
62
+ <div className="grid grid-cols-2 gap-x-8 gap-y-3">
63
+ {options.map((opt) => {
64
+ const id = `cbg-${field.name}-${opt.value}`;
65
+ const checked = selected.includes(opt.value);
66
+ return (
67
+ <div key={opt.value} className="flex items-center gap-3">
68
+ <input
69
+ type="checkbox"
70
+ id={id}
71
+ checked={checked}
72
+ onChange={() => toggle(opt.value)}
73
+ className="h-4 w-4 shrink-0 rounded border-secondary focus:ring-focus-ring"
74
+ />
75
+ <label htmlFor={id} className="font-body text-sm text-tertiary cursor-pointer">
76
+ {opt.label}
77
+ </label>
78
+ </div>
79
+ );
80
+ })}
81
+ </div>
82
+ </fieldset>
83
+ );
84
+ }
85
+
86
+ // ─── renderField ─────────────────────────────────────────────────────────────
87
+
32
88
  function renderField(
33
89
  field: FormFieldDefinition,
34
90
  index: number,
@@ -44,6 +100,10 @@ function renderField(
44
100
  const name = field.name ?? `field-${index}`;
45
101
  const type = (field.type ?? 'text').toString().toLowerCase();
46
102
 
103
+ if (type === 'checkbox_group') {
104
+ return <CheckboxGroup key={name} field={field} />;
105
+ }
106
+
47
107
  if (field.type === 'hidden') {
48
108
  const val = field.value ?? '';
49
109
  return <input key={name} type="hidden" name={name} value={val} />;
@@ -55,7 +115,7 @@ function renderField(
55
115
  let labelWithCompany = companyNameClean
56
116
  ? labelRaw.replace(/\{\{company_name\}\}/gi, companyNameClean)
57
117
  : labelRaw;
58
- // Inject ToS/Privacy links as markdown so they render as links (then whole label is rendered as markdown)
118
+ // Inject ToS/Privacy links as markdown so they render as links
59
119
  if (name === 'tos_privacy_consent' && privacyPolicyUrl && termsOfServiceUrl) {
60
120
  labelWithCompany = labelWithCompany
61
121
  .replace(/\*\*Terms of Service\*\*/gi, `**[Terms of Service](${termsOfServiceUrl})**`)
@@ -93,19 +153,13 @@ function renderField(
93
153
  );
94
154
  }
95
155
 
96
- if (field.type === 'tel' && showCountryCode) {
97
- const countryOptions = countries.map((c) => ({
98
- label: c.phoneCode.startsWith('+') ? c.phoneCode : `+${c.phoneCode}`,
99
- value: c.code,
100
- }));
156
+ if (field.type === 'tel') {
101
157
  const country = countries.find((c) => c.code === selectedCountryPhone);
102
158
  const nationalMask = getNationalMask(country);
103
159
  const value = phoneValues[name] ?? '';
104
160
  const placeholder = nationalMask ? nationalMask.replace(/#/g, '0') : (field.placeholder ?? '');
105
161
 
106
162
  const handlePhoneChange = (valueOrEvent: unknown) => {
107
- // Some input implementations call onChange with an event, others with a string value.
108
- // Normalize defensively to avoid runtime errors.
109
163
  const raw =
110
164
  typeof valueOrEvent === 'string'
111
165
  ? valueOrEvent
@@ -122,23 +176,43 @@ function renderField(
122
176
  setPhoneValues((prev) => ({ ...prev, [name]: formatted }));
123
177
  };
124
178
 
125
- return (
126
- <InputGroup
127
- key={name}
128
- label={field.label}
129
- isRequired={Boolean(field.required)}
130
- size="md"
131
- leadingAddon={
132
- <NativeSelect
133
- aria-label="Country code"
134
- value={selectedCountryPhone}
135
- onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
136
- onCountryChange(e.currentTarget.value)
137
- }
138
- options={countryOptions}
179
+ if (showCountryCode) {
180
+ const countryOptions = countries.map((c) => ({
181
+ label: c.phoneCode.startsWith('+') ? c.phoneCode : `+${c.phoneCode}`,
182
+ value: c.code,
183
+ }));
184
+ return (
185
+ <InputGroup
186
+ key={name}
187
+ label={field.label}
188
+ isRequired={Boolean(field.required)}
189
+ size="md"
190
+ leadingAddon={
191
+ <NativeSelect
192
+ aria-label="Country code"
193
+ value={selectedCountryPhone}
194
+ onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
195
+ onCountryChange(e.currentTarget.value)
196
+ }
197
+ options={countryOptions}
198
+ />
199
+ }
200
+ >
201
+ <InputBase
202
+ type="tel"
203
+ name={name}
204
+ value={value}
205
+ onChange={handlePhoneChange}
206
+ placeholder={placeholder}
207
+ size="md"
139
208
  />
140
- }
141
- >
209
+ </InputGroup>
210
+ );
211
+ }
212
+
213
+ // No country code selector — render a formatted phone input directly
214
+ return (
215
+ <InputGroup key={name} label={field.label} isRequired={Boolean(field.required)} size="md">
142
216
  <InputBase
143
217
  type="tel"
144
218
  name={name}
@@ -180,6 +254,8 @@ function renderField(
180
254
  );
181
255
  }
182
256
 
257
+ // ─── DynamicFormFields ────────────────────────────────────────────────────────
258
+
183
259
  export function DynamicFormFields({ form, jobSlug, privacyPolicyUrl, termsOfServiceUrl }: DynamicFormFieldsProps) {
184
260
  const [selectedCountryPhone, setSelectedCountryPhone] = useState('US');
185
261
  const [phoneValues, setPhoneValues] = useState<Record<string, string>>({});