keystone-design-bootstrap 1.0.58 → 1.0.60
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/dist/blog-post-vWzW8yFb.d.ts +50 -0
- package/dist/contexts/index.d.ts +13 -0
- package/dist/design_system/elements/index.d.ts +383 -0
- package/dist/design_system/logo/keystone-logo.d.ts +6 -0
- package/dist/design_system/sections/index.d.ts +232 -0
- package/dist/design_system/sections/index.js +25 -37
- package/dist/design_system/sections/index.js.map +1 -1
- package/dist/index.d.ts +69 -0
- package/dist/index.js +25 -37
- package/dist/index.js.map +1 -1
- package/dist/lib/component-registry.d.ts +13 -0
- package/dist/lib/hooks/index.d.ts +83 -0
- package/dist/lib/server-api.d.ts +44 -0
- package/dist/package-CB1tENyG.d.ts +148 -0
- package/dist/photos-CmBdWiuZ.d.ts +27 -0
- package/dist/themes/index.d.ts +16 -0
- package/dist/types/index.d.ts +312 -0
- package/dist/utils/cx.d.ts +15 -0
- package/dist/utils/gradient-placeholder.d.ts +8 -0
- package/dist/utils/is-react-component.d.ts +21 -0
- package/dist/utils/markdown-toc.d.ts +14 -0
- package/dist/utils/phone-helpers.d.ts +24 -0
- package/dist/utils/photo-helpers.d.ts +38 -0
- package/dist/website-photos-Cl1YqAno.d.ts +21 -0
- package/package.json +1 -1
- package/src/design_system/portal/LoginForm.tsx +11 -11
- package/src/design_system/portal/MessageComposer.tsx +2 -2
- package/src/design_system/portal/PortalPage.tsx +121 -105
- package/src/design_system/portal/PortalTabTracker.tsx +24 -0
- package/src/design_system/portal/RowThumbnail.tsx +3 -3
- package/src/design_system/sections/contact-section-form.aman.tsx +2 -2
- package/src/design_system/sections/contact-section-form.balance.tsx +2 -2
- package/src/design_system/sections/contact-section-form.barelux.tsx +2 -2
- package/src/design_system/sections/contact-section-form.tsx +2 -2
- package/src/design_system/sections/header-navigation.aman.tsx +1 -4
- package/src/design_system/sections/header-navigation.balance.tsx +1 -4
- package/src/design_system/sections/header-navigation.barelux.tsx +1 -4
- package/src/design_system/sections/header-navigation.tsx +1 -8
- package/src/index.ts +1 -1
- package/src/lib/cta-urls.ts +15 -38
- package/src/next/layouts/root-layout.tsx +6 -7
- package/src/styles/style-overrides.aman.css +6 -0
- package/src/styles/style-overrides.barelux.css +6 -0
- package/src/styles/theme.css +6 -0
- package/src/tracking/MetaPixelTracker.tsx +17 -12
- package/src/tracking/firePixelEvent.ts +26 -0
- package/src/tracking/index.ts +2 -6
- package/src/types/api/company-information.ts +2 -0
- package/src/tracking/BookingCtaTracker.tsx +0 -32
- package/src/tracking/ViewContentTracker.tsx +0 -21
- package/src/tracking/trackInitiateCheckout.ts +0 -16
- package/src/tracking/trackMetaLead.ts +0 -14
- package/src/tracking/trackViewContent.ts +0 -19
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import React, { useRef, useState } from 'react';
|
|
4
4
|
import { Form, Button } from '../elements';
|
|
5
5
|
import { DynamicFormFields } from '../components/DynamicFormFields';
|
|
6
|
-
import {
|
|
6
|
+
import { firePixelEvent } from '../../tracking/firePixelEvent';
|
|
7
7
|
import type { FormDefinition } from '../../types/api/form';
|
|
8
8
|
import { useFormDefinitions } from '../../next/contexts/form-definitions';
|
|
9
9
|
|
|
@@ -61,7 +61,7 @@ export const ContactSectionForm = ({
|
|
|
61
61
|
setStatusMessage(result.message || successMessage);
|
|
62
62
|
formRef.current?.reset();
|
|
63
63
|
onSuccess?.();
|
|
64
|
-
|
|
64
|
+
firePixelEvent('Lead');
|
|
65
65
|
setTimeout(() => setSubmitStatus('idle'), 5000);
|
|
66
66
|
} else {
|
|
67
67
|
setSubmitStatus('error');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import React, { useRef, useState } from 'react';
|
|
4
4
|
import { Form, Button } from '../elements';
|
|
5
5
|
import { DynamicFormFields } from '../components/DynamicFormFields';
|
|
6
|
-
import {
|
|
6
|
+
import { firePixelEvent } from '../../tracking/firePixelEvent';
|
|
7
7
|
import type { FormDefinition } from '../../types/api/form';
|
|
8
8
|
import { useFormDefinitions } from '../../next/contexts/form-definitions';
|
|
9
9
|
|
|
@@ -61,7 +61,7 @@ export const ContactSectionForm = ({
|
|
|
61
61
|
setStatusMessage(result.message || successMessage);
|
|
62
62
|
formRef.current?.reset();
|
|
63
63
|
onSuccess?.();
|
|
64
|
-
|
|
64
|
+
firePixelEvent('Lead');
|
|
65
65
|
setTimeout(() => setSubmitStatus('idle'), 5000);
|
|
66
66
|
} else {
|
|
67
67
|
setSubmitStatus('error');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import React, { useRef, useState } from 'react';
|
|
4
4
|
import { Form, Button } from '../elements';
|
|
5
5
|
import { DynamicFormFields } from '../components/DynamicFormFields';
|
|
6
|
-
import {
|
|
6
|
+
import { firePixelEvent } from '../../tracking/firePixelEvent';
|
|
7
7
|
import type { FormDefinition } from '../../types/api/form';
|
|
8
8
|
import { useFormDefinitions } from '../../next/contexts/form-definitions';
|
|
9
9
|
|
|
@@ -69,7 +69,7 @@ export const ContactSectionForm = ({
|
|
|
69
69
|
setStatusMessage(result.message || successMessage);
|
|
70
70
|
formRef.current?.reset();
|
|
71
71
|
onSuccess?.();
|
|
72
|
-
|
|
72
|
+
firePixelEvent('Lead');
|
|
73
73
|
setTimeout(() => setSubmitStatus('idle'), 5000);
|
|
74
74
|
} else {
|
|
75
75
|
setSubmitStatus('error');
|
|
@@ -56,10 +56,7 @@ export function HeaderNavigation({
|
|
|
56
56
|
|
|
57
57
|
// Use navigation from config or override
|
|
58
58
|
const navigation = navigationOverride || config?.navigation?.header || [];
|
|
59
|
-
const ctaUrls = resolveCtaUrls(companyInformation
|
|
60
|
-
primaryHrefOverride: props?.primaryHrefOverride,
|
|
61
|
-
secondaryHrefOverride: props?.secondaryHrefOverride,
|
|
62
|
-
});
|
|
59
|
+
const ctaUrls = resolveCtaUrls(companyInformation);
|
|
63
60
|
|
|
64
61
|
// Cancel any pending close timeout
|
|
65
62
|
const cancelCloseTimeout = useCallback(() => {
|
|
@@ -35,10 +35,7 @@ export function HeaderNavigation({
|
|
|
35
35
|
const companyName = logoTextOverride || companyInformation?.company_name || props?.logo?.text || '';
|
|
36
36
|
|
|
37
37
|
const navigation = navigationOverride || config?.navigation?.header || [];
|
|
38
|
-
const ctaUrls = resolveCtaUrls(companyInformation
|
|
39
|
-
primaryHrefOverride: props?.primaryHrefOverride,
|
|
40
|
-
secondaryHrefOverride: props?.secondaryHrefOverride,
|
|
41
|
-
});
|
|
38
|
+
const ctaUrls = resolveCtaUrls(companyInformation);
|
|
42
39
|
|
|
43
40
|
const cancelCloseTimeout = useCallback(() => {
|
|
44
41
|
if (closeTimeoutRef.current) {
|
|
@@ -41,10 +41,7 @@ export function HeaderNavigation({
|
|
|
41
41
|
|
|
42
42
|
// Use navigation from config or override
|
|
43
43
|
const navigation = navigationOverride || config?.navigation?.header || [];
|
|
44
|
-
const ctaUrls = resolveCtaUrls(companyInformation
|
|
45
|
-
primaryHrefOverride: props?.primaryHrefOverride,
|
|
46
|
-
secondaryHrefOverride: props?.secondaryHrefOverride,
|
|
47
|
-
});
|
|
44
|
+
const ctaUrls = resolveCtaUrls(companyInformation);
|
|
48
45
|
|
|
49
46
|
// Cancel any pending close timeout
|
|
50
47
|
const cancelCloseTimeout = useCallback(() => {
|
|
@@ -27,10 +27,6 @@ export interface HeaderProps {
|
|
|
27
27
|
secondary_href?: string;
|
|
28
28
|
secondary_target?: '_blank' | '_self';
|
|
29
29
|
};
|
|
30
|
-
/** When set, overrides the default primary CTA URL (booking or /contact). */
|
|
31
|
-
primaryHrefOverride?: string | null;
|
|
32
|
-
/** When set, overrides the default secondary CTA URL (/contact). */
|
|
33
|
-
secondaryHrefOverride?: string | null;
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
export interface HeaderComponentProps {
|
|
@@ -102,10 +98,7 @@ export function HeaderNavigation({
|
|
|
102
98
|
const logoImage = logoImageOverride || getLogoUrl(websitePhotos) || props?.logo?.image;
|
|
103
99
|
const logoText = logoTextOverride || companyInformation?.company_name || props?.logo?.text || '';
|
|
104
100
|
const cta_button = props?.cta_button;
|
|
105
|
-
const ctaUrls = resolveCtaUrls(companyInformation
|
|
106
|
-
primaryHrefOverride: props?.primaryHrefOverride,
|
|
107
|
-
secondaryHrefOverride: props?.secondaryHrefOverride,
|
|
108
|
-
});
|
|
101
|
+
const ctaUrls = resolveCtaUrls(companyInformation);
|
|
109
102
|
|
|
110
103
|
const logo = {
|
|
111
104
|
text: logoText || '',
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,6 @@ export * from './design_system/sections';
|
|
|
7
7
|
export * from './design_system/elements';
|
|
8
8
|
export * from './lib/actions';
|
|
9
9
|
export { resolveCtaUrls, isExternalCtaUrl } from './lib/cta-urls';
|
|
10
|
-
export type { ResolvedCtaUrls
|
|
10
|
+
export type { ResolvedCtaUrls } from './lib/cta-urls';
|
|
11
11
|
export * from './contexts';
|
|
12
12
|
export { ChatWidget } from './design_system/components/ChatWidget';
|
package/src/lib/cta-urls.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Resolves the primary and secondary CTA hrefs used across the site.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order for primaryHref:
|
|
5
|
+
* 1. portal_url — set by backend when account has consumer portal enabled
|
|
6
|
+
* 2. external_management_url — booking platform URL from the API
|
|
7
|
+
* 3. /contact — final fallback
|
|
8
|
+
*
|
|
9
|
+
* No per-site configuration needed. Enable the portal in the admin console and
|
|
10
|
+
* every CTA across the site automatically routes to the portal.
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
import type { CompanyInformation } from '../types/api/company-information';
|
|
@@ -13,49 +19,20 @@ export function isExternalCtaUrl(href: string): boolean {
|
|
|
13
19
|
return href.startsWith('http://') || href.startsWith('https://');
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
export interface CtaUrlOverrides {
|
|
17
|
-
primaryHrefOverride?: string | null;
|
|
18
|
-
secondaryHrefOverride?: string | null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface CtaButtonLike {
|
|
22
|
-
href?: string | null;
|
|
23
|
-
secondary_href?: string | null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
export interface ResolvedCtaUrls {
|
|
27
23
|
primaryHref: string;
|
|
28
24
|
secondaryHref: string;
|
|
29
25
|
hasSecondary: boolean;
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
/**
|
|
33
|
-
* Resolve primary and secondary CTA hrefs from company info and optional overrides.
|
|
34
|
-
* - primaryHref = external_management_url (if set) else /contact
|
|
35
|
-
* - secondaryHref = /contact
|
|
36
|
-
* Overrides win when provided.
|
|
37
|
-
*/
|
|
38
28
|
export function resolveCtaUrls(
|
|
39
|
-
companyInformation?: CompanyInformation | null
|
|
40
|
-
_ctaButton?: CtaButtonLike | null,
|
|
41
|
-
overrides?: CtaUrlOverrides | null
|
|
29
|
+
companyInformation?: CompanyInformation | null
|
|
42
30
|
): ResolvedCtaUrls {
|
|
31
|
+
const portalUrl = companyInformation?.portal_url?.trim() || null;
|
|
43
32
|
const externalUrl = companyInformation?.external_management_url?.trim() || null;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
const primaryHref
|
|
47
|
-
overrides?.primaryHrefOverride !== undefined && overrides.primaryHrefOverride !== null
|
|
48
|
-
? overrides.primaryHrefOverride
|
|
49
|
-
: (externalUrl || CONTACT_PATH);
|
|
50
|
-
|
|
51
|
-
const secondaryHref =
|
|
52
|
-
overrides?.secondaryHrefOverride !== undefined && overrides.secondaryHrefOverride !== null
|
|
53
|
-
? overrides.secondaryHrefOverride
|
|
54
|
-
: CONTACT_PATH;
|
|
33
|
+
const primaryHref = portalUrl ?? externalUrl ?? CONTACT_PATH;
|
|
34
|
+
const secondaryHref = CONTACT_PATH;
|
|
35
|
+
const hasSecondary = primaryHref !== CONTACT_PATH;
|
|
55
36
|
|
|
56
|
-
return {
|
|
57
|
-
primaryHref,
|
|
58
|
-
secondaryHref,
|
|
59
|
-
hasSecondary,
|
|
60
|
-
};
|
|
37
|
+
return { primaryHref, secondaryHref, hasSecondary };
|
|
61
38
|
}
|
|
@@ -24,10 +24,10 @@ export type KeystoneRootLayoutHeaderOverrides = {
|
|
|
24
24
|
logoHref?: string;
|
|
25
25
|
/** Overrides the title/company name shown in the center of the nav bar (otherwise from business_info) */
|
|
26
26
|
logoText?: string;
|
|
27
|
+
/** Overrides the primary CTA button label (default: "Contact Us") */
|
|
27
28
|
ctaLabel?: string;
|
|
28
|
-
|
|
29
|
+
/** Overrides the secondary CTA button label (default: "Book Now" when a booking/portal URL is configured) */
|
|
29
30
|
secondaryLabel?: string;
|
|
30
|
-
secondaryHref?: string;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export type KeystoneRootLayoutOptions = {
|
|
@@ -137,7 +137,9 @@ export async function KeystoneRootLayout(props: {
|
|
|
137
137
|
const theme = config.site.theme;
|
|
138
138
|
|
|
139
139
|
const ci = companyInformation as CompanyInformation | null;
|
|
140
|
-
const externalManagementUrl = ci?.external_management_url ||
|
|
140
|
+
const externalManagementUrl = ci?.external_management_url?.trim() || null;
|
|
141
|
+
const portalUrl = ci?.portal_url?.trim() || null;
|
|
142
|
+
const bookingHref = portalUrl ?? externalManagementUrl ?? null;
|
|
141
143
|
const chatEnabled = Boolean(ci?.chat_enabled);
|
|
142
144
|
|
|
143
145
|
const headerOverrides = options?.headerOverrides;
|
|
@@ -148,10 +150,7 @@ export async function KeystoneRootLayout(props: {
|
|
|
148
150
|
},
|
|
149
151
|
cta_button: {
|
|
150
152
|
label: headerOverrides?.ctaLabel || 'Contact Us',
|
|
151
|
-
|
|
152
|
-
secondary_label:
|
|
153
|
-
headerOverrides?.secondaryLabel ?? (externalManagementUrl ? 'Book Now' : undefined),
|
|
154
|
-
secondary_href: headerOverrides?.secondaryHref ?? externalManagementUrl,
|
|
153
|
+
secondary_label: headerOverrides?.secondaryLabel ?? (bookingHref ? 'Book Now' : undefined),
|
|
155
154
|
},
|
|
156
155
|
};
|
|
157
156
|
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
/* Border colors */
|
|
41
41
|
--color-border-primary: #E5E2D9;
|
|
42
42
|
--color-border-secondary: #D5D2C9;
|
|
43
|
+
|
|
44
|
+
/* Radius — minimal/sharp aesthetic */
|
|
45
|
+
--radius-component: 0.25rem;
|
|
46
|
+
--radius-interactive: 0.25rem;
|
|
47
|
+
--radius-input: 0.25rem;
|
|
48
|
+
--radius-badge: 0.25rem;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/* Body text uses Inter */
|
|
@@ -40,6 +40,12 @@
|
|
|
40
40
|
/* Border colors */
|
|
41
41
|
--color-border-primary: #EEEAE7;
|
|
42
42
|
--color-border-secondary: #E5E1DE;
|
|
43
|
+
|
|
44
|
+
/* Radius — soft/rounded aesthetic */
|
|
45
|
+
--radius-component: 0.75rem;
|
|
46
|
+
--radius-interactive: 0.5rem;
|
|
47
|
+
--radius-input: 0.5rem;
|
|
48
|
+
--radius-badge: 9999px;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/* Body text uses Poppins */
|
package/src/styles/theme.css
CHANGED
|
@@ -54,6 +54,12 @@
|
|
|
54
54
|
--radius-3xl: 1.5rem;
|
|
55
55
|
--radius-full: 9999px;
|
|
56
56
|
|
|
57
|
+
/* Semantic radius tokens — override per theme to get consistent rounding everywhere */
|
|
58
|
+
--radius-component: 0.5rem; /* cards, panels, containers */
|
|
59
|
+
--radius-interactive: 0.375rem; /* buttons, tabs */
|
|
60
|
+
--radius-input: 0.375rem; /* form inputs */
|
|
61
|
+
--radius-badge: 9999px; /* pill badges / tags */
|
|
62
|
+
|
|
57
63
|
/* ==================== SHADOWS ==================== */
|
|
58
64
|
--shadow-xs: 0px 1px 2px rgba(10, 13, 18, 0.05);
|
|
59
65
|
--shadow-sm: 0px 1px 3px rgba(10, 13, 18, 0.1), 0px 1px 2px -1px rgba(10, 13, 18, 0.1);
|
|
@@ -2,14 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
4
|
import { usePathname } from 'next/navigation';
|
|
5
|
-
import {
|
|
6
|
-
import { trackInitiateCheckout } from './trackInitiateCheckout';
|
|
5
|
+
import { firePixelEvent } from './firePixelEvent';
|
|
7
6
|
|
|
8
7
|
type RouteRule = {
|
|
9
8
|
pattern: RegExp;
|
|
10
9
|
getParams: (match: RegExpMatchArray) => { contentName: string; contentCategory: string };
|
|
11
10
|
};
|
|
12
11
|
|
|
12
|
+
function slugToTitle(slug: string): string {
|
|
13
|
+
return slug
|
|
14
|
+
.split('-')
|
|
15
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
16
|
+
.join(' ');
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
// Checked in order — first match wins. More specific patterns come first.
|
|
14
20
|
const ROUTE_RULES: RouteRule[] = [
|
|
15
21
|
{
|
|
@@ -28,6 +34,10 @@ const ROUTE_RULES: RouteRule[] = [
|
|
|
28
34
|
pattern: /^\/locations$/,
|
|
29
35
|
getParams: () => ({ contentName: 'Locations', contentCategory: 'Locations' }),
|
|
30
36
|
},
|
|
37
|
+
{
|
|
38
|
+
pattern: /^\/portal$/,
|
|
39
|
+
getParams: () => ({ contentName: 'Member Portal', contentCategory: 'Pricing' }),
|
|
40
|
+
},
|
|
31
41
|
{
|
|
32
42
|
pattern: /^\/service-menu$/,
|
|
33
43
|
getParams: () => ({ contentName: 'Service Menu', contentCategory: 'Pricing' }),
|
|
@@ -42,13 +52,6 @@ const ROUTE_RULES: RouteRule[] = [
|
|
|
42
52
|
},
|
|
43
53
|
];
|
|
44
54
|
|
|
45
|
-
function slugToTitle(slug: string): string {
|
|
46
|
-
return slug
|
|
47
|
-
.split('-')
|
|
48
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
49
|
-
.join(' ');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
55
|
type Props = {
|
|
53
56
|
/** External booking URL. When set, fires InitiateCheckout on any click targeting that URL. */
|
|
54
57
|
bookingUrl?: string | null;
|
|
@@ -58,6 +61,9 @@ type Props = {
|
|
|
58
61
|
* Single client-side tracker placed once in KeystoneRootLayout.
|
|
59
62
|
* - Fires ViewContent on every route change for known page patterns.
|
|
60
63
|
* - Fires InitiateCheckout whenever a visitor clicks a link to the external booking URL.
|
|
64
|
+
*
|
|
65
|
+
* Portal booking tab tracking is handled directly inside PortalPage via PortalTabTracker
|
|
66
|
+
* since intent is only established when the Booking tab is explicitly opened.
|
|
61
67
|
*/
|
|
62
68
|
export function MetaPixelTracker({ bookingUrl }: Props) {
|
|
63
69
|
const pathname = usePathname();
|
|
@@ -67,7 +73,7 @@ export function MetaPixelTracker({ bookingUrl }: Props) {
|
|
|
67
73
|
const match = pathname.match(rule.pattern);
|
|
68
74
|
if (match) {
|
|
69
75
|
const { contentName, contentCategory } = rule.getParams(match);
|
|
70
|
-
|
|
76
|
+
firePixelEvent('ViewContent', { contentName, contentCategory });
|
|
71
77
|
break;
|
|
72
78
|
}
|
|
73
79
|
}
|
|
@@ -79,8 +85,7 @@ export function MetaPixelTracker({ bookingUrl }: Props) {
|
|
|
79
85
|
const handleClick = (e: MouseEvent) => {
|
|
80
86
|
const anchor = (e.target as Element).closest('a');
|
|
81
87
|
if (anchor?.href?.startsWith(bookingUrl)) {
|
|
82
|
-
|
|
83
|
-
trackInitiateCheckout();
|
|
88
|
+
firePixelEvent('InitiateCheckout');
|
|
84
89
|
}
|
|
85
90
|
};
|
|
86
91
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type FbqFn = (method: string, eventName: string, params?: Record<string, string>) => void;
|
|
2
|
+
|
|
3
|
+
export type PixelEvent = 'PageView' | 'ViewContent' | 'InitiateCheckout' | 'Lead';
|
|
4
|
+
|
|
5
|
+
export interface PixelEventParams {
|
|
6
|
+
contentName?: string;
|
|
7
|
+
contentCategory?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Single entry point for all client-side Meta Pixel event fires.
|
|
12
|
+
* Silently no-ops if fbq is not loaded (pixel not configured for this site).
|
|
13
|
+
*/
|
|
14
|
+
export function firePixelEvent(event: PixelEvent, params?: PixelEventParams): void {
|
|
15
|
+
if (typeof window === 'undefined') return;
|
|
16
|
+
const fbq = (window as Window & { fbq?: FbqFn }).fbq;
|
|
17
|
+
if (!fbq) {
|
|
18
|
+
console.debug('[MetaPixel] skipped — fbq not loaded', { event });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const normalized: Record<string, string> = {};
|
|
22
|
+
if (params?.contentName) normalized.content_name = params.contentName;
|
|
23
|
+
if (params?.contentCategory) normalized.content_category = params.contentCategory;
|
|
24
|
+
console.debug('[MetaPixel]', event, normalized);
|
|
25
|
+
fbq('track', event, Object.keys(normalized).length > 0 ? normalized : undefined);
|
|
26
|
+
}
|
package/src/tracking/index.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
export { MetaPixel } from './MetaPixel';
|
|
2
2
|
export type { MetaPixelProps } from './MetaPixel';
|
|
3
|
-
export { trackMetaLead } from './trackMetaLead';
|
|
4
|
-
export { trackViewContent } from './trackViewContent';
|
|
5
|
-
export { trackInitiateCheckout } from './trackInitiateCheckout';
|
|
6
3
|
export { MetaPixelTracker } from './MetaPixelTracker';
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
export { BookingCtaTracker } from './BookingCtaTracker';
|
|
4
|
+
export { firePixelEvent } from './firePixelEvent';
|
|
5
|
+
export type { PixelEvent, PixelEventParams } from './firePixelEvent';
|
|
@@ -36,6 +36,8 @@ export interface CompanyInformation {
|
|
|
36
36
|
sales_email?: string;
|
|
37
37
|
business_hours?: string;
|
|
38
38
|
external_management_url?: string;
|
|
39
|
+
/** Member portal URL for this account. When set, used as the primary CTA instead of external_management_url. */
|
|
40
|
+
portal_url?: string | null;
|
|
39
41
|
created_at: string;
|
|
40
42
|
updated_at: string;
|
|
41
43
|
photo_attachments?: PhotoAttachment[];
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect } from 'react';
|
|
4
|
-
import { trackInitiateCheckout } from './trackInitiateCheckout';
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
bookingUrl: string | null | undefined;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Drop into the root layout to fire Meta Pixel InitiateCheckout whenever a visitor
|
|
12
|
-
* clicks any anchor whose href points to the external booking URL.
|
|
13
|
-
* Uses document-level click delegation — no changes needed to individual buttons.
|
|
14
|
-
*/
|
|
15
|
-
export function BookingCtaTracker({ bookingUrl }: Props) {
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (!bookingUrl) return;
|
|
18
|
-
|
|
19
|
-
const handleClick = (e: MouseEvent) => {
|
|
20
|
-
const anchor = (e.target as Element).closest('a');
|
|
21
|
-
if (!anchor) return;
|
|
22
|
-
if (anchor.href && anchor.href.startsWith(bookingUrl)) {
|
|
23
|
-
trackInitiateCheckout();
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
document.addEventListener('click', handleClick);
|
|
28
|
-
return () => document.removeEventListener('click', handleClick);
|
|
29
|
-
}, [bookingUrl]);
|
|
30
|
-
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useEffect } from 'react';
|
|
4
|
-
import { trackViewContent } from './trackViewContent';
|
|
5
|
-
|
|
6
|
-
type Props = {
|
|
7
|
-
contentName?: string;
|
|
8
|
-
contentCategory?: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Drop into any server-rendered page to fire the Meta Pixel ViewContent event on mount.
|
|
13
|
-
* Renders nothing — purely a tracking side-effect component.
|
|
14
|
-
*/
|
|
15
|
-
export function ViewContentTracker({ contentName, contentCategory }: Props) {
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
trackViewContent(contentName, contentCategory);
|
|
18
|
-
}, [contentName, contentCategory]);
|
|
19
|
-
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
type FbqFn = (method: string, eventName: string, params?: object) => void;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Fires the client-side Meta Pixel InitiateCheckout event.
|
|
5
|
-
* Call this when a visitor clicks a booking / scheduling button.
|
|
6
|
-
*/
|
|
7
|
-
export function trackInitiateCheckout(): void {
|
|
8
|
-
if (typeof window === 'undefined') return;
|
|
9
|
-
const fbq = (window as Window & { fbq?: FbqFn }).fbq;
|
|
10
|
-
if (!fbq) {
|
|
11
|
-
console.debug('[MetaPixel] InitiateCheckout skipped — fbq not loaded');
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
console.debug('[MetaPixel] InitiateCheckout');
|
|
15
|
-
fbq('track', 'InitiateCheckout');
|
|
16
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fires the client-side Meta Pixel Lead event with the same eventID as server CAPI
|
|
3
|
-
* so Meta can deduplicate browser + server events.
|
|
4
|
-
*/
|
|
5
|
-
export function trackMetaLead(eventId: string | undefined): void {
|
|
6
|
-
if (typeof window === 'undefined' || !eventId) return;
|
|
7
|
-
const fbq = (window as Window & { fbq?: (method: string, eventName: string, params?: object, options?: { eventID?: string }) => void }).fbq;
|
|
8
|
-
if (!fbq) {
|
|
9
|
-
console.debug('[MetaPixel] Lead skipped — fbq not loaded', { eventId });
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
console.debug('[MetaPixel] Lead', { eventId });
|
|
13
|
-
fbq('track', 'Lead', {}, { eventID: eventId });
|
|
14
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
type FbqFn = (method: string, eventName: string, params?: object) => void;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Fires the client-side Meta Pixel ViewContent event.
|
|
5
|
-
* Call this on service/offer detail pages so Meta knows which content is most engaging.
|
|
6
|
-
*/
|
|
7
|
-
export function trackViewContent(contentName?: string, contentCategory?: string): void {
|
|
8
|
-
if (typeof window === 'undefined') return;
|
|
9
|
-
const fbq = (window as Window & { fbq?: FbqFn }).fbq;
|
|
10
|
-
if (!fbq) {
|
|
11
|
-
console.debug('[MetaPixel] ViewContent skipped — fbq not loaded', { contentName, contentCategory });
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
const params: Record<string, string> = {};
|
|
15
|
-
if (contentName) params.content_name = contentName;
|
|
16
|
-
if (contentCategory) params.content_category = contentCategory;
|
|
17
|
-
console.debug('[MetaPixel] ViewContent', params);
|
|
18
|
-
fbq('track', 'ViewContent', params);
|
|
19
|
-
}
|