keystone-design-bootstrap 1.0.68 → 1.0.70

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 (34) hide show
  1. package/README.md +74 -132
  2. package/dist/design_system/sections/index.js +110 -60
  3. package/dist/design_system/sections/index.js.map +1 -1
  4. package/dist/index.js +117 -61
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/server-api.d.ts +9 -1
  7. package/dist/lib/server-api.js +11 -0
  8. package/dist/lib/server-api.js.map +1 -1
  9. package/dist/tracking/index.d.ts +134 -5
  10. package/dist/tracking/index.js +123 -0
  11. package/dist/tracking/index.js.map +1 -1
  12. package/package.json +2 -1
  13. package/src/design_system/components/ChatWidget.tsx +6 -7
  14. package/src/design_system/portal/LoginForm.tsx +21 -2
  15. package/src/design_system/portal/PortalPage.tsx +5 -5
  16. package/src/design_system/portal/PortalTabTracker.tsx +10 -2
  17. package/src/design_system/sections/contact-section-form.aman.tsx +6 -1
  18. package/src/design_system/sections/contact-section-form.balance.tsx +6 -1
  19. package/src/design_system/sections/contact-section-form.barelux.tsx +6 -1
  20. package/src/design_system/sections/contact-section-form.tsx +6 -1
  21. package/src/design_system/sections/email-signup-section.tsx +6 -1
  22. package/src/design_system/sections/header-navigation.aman.tsx +6 -1
  23. package/src/design_system/sections/header-navigation.balance.tsx +6 -1
  24. package/src/design_system/sections/header-navigation.barelux.tsx +6 -1
  25. package/src/design_system/sections/header-navigation.tsx +6 -1
  26. package/src/design_system/sections/job-application-form.aman.tsx +6 -1
  27. package/src/design_system/sections/job-application-form.barelux.tsx +6 -1
  28. package/src/design_system/sections/job-application-form.tsx +6 -1
  29. package/src/lib/server-api.ts +18 -0
  30. package/src/next/layouts/root-layout.tsx +78 -33
  31. package/src/tracking/KeystoneAnalyticsTracker.tsx +41 -0
  32. package/src/tracking/PostHogProvider.tsx +128 -0
  33. package/src/tracking/captureEvent.ts +140 -0
  34. package/src/tracking/index.ts +5 -0
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ import posthog from 'posthog-js';
4
+ import { PostHogProvider as PHProvider } from 'posthog-js/react';
5
+ import { useEffect, Suspense } from 'react';
6
+ import { usePathname, useSearchParams } from 'next/navigation';
7
+
8
+ const DEFAULT_HOST = 'https://us.i.posthog.com';
9
+
10
+ export type PostHogProviderProps = {
11
+ apiKey: string;
12
+ apiHost?: string;
13
+ /** Keystone account ID — attached to every event as a super property. */
14
+ accountId?: number;
15
+ /** Keystone account name (company_name) — attached to every event as a super property. */
16
+ accountName?: string;
17
+ children: React.ReactNode;
18
+ };
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Page name resolution
22
+ // ---------------------------------------------------------------------------
23
+
24
+ type PageInfo = { page_name: string; page_slug?: string };
25
+
26
+ function resolvePageInfo(pathname: string): PageInfo {
27
+ if (pathname === '/') return { page_name: 'home' };
28
+
29
+ const patterns: Array<[RegExp, (m: RegExpMatchArray) => PageInfo]> = [
30
+ [/^\/services\/(.+)$/, ([, slug]) => ({ page_name: 'service_detail', page_slug: slug })],
31
+ [/^\/locations\/(.+)$/, ([, slug]) => ({ page_name: 'location_detail', page_slug: slug })],
32
+ [/^\/blog\/(.+)$/, ([, slug]) => ({ page_name: 'blog_post', page_slug: slug })],
33
+ [/^\/jobs\/(.+)$/, ([, slug]) => ({ page_name: 'job_detail', page_slug: slug })],
34
+ [/^\/packages\/(.+)$/, ([, slug]) => ({ page_name: 'package_detail', page_slug: slug })],
35
+ ];
36
+
37
+ for (const [pattern, resolve] of patterns) {
38
+ const match = pathname.match(pattern);
39
+ if (match) return resolve(match);
40
+ }
41
+
42
+ const staticNames: Record<string, string> = {
43
+ '/services': 'services',
44
+ '/locations': 'locations',
45
+ '/contact': 'contact',
46
+ '/about': 'about',
47
+ '/blog': 'blog',
48
+ '/portal': 'portal',
49
+ '/gallery': 'gallery',
50
+ '/team': 'team',
51
+ '/faq': 'faq',
52
+ '/reviews': 'reviews',
53
+ '/jobs': 'jobs',
54
+ '/packages': 'packages',
55
+ '/service-menu': 'service_menu',
56
+ '/privacy-policy': 'privacy_policy',
57
+ '/terms': 'terms_of_service',
58
+ };
59
+
60
+ return { page_name: staticNames[pathname] ?? 'unknown' };
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Pageview tracker — must be wrapped in <Suspense> (useSearchParams)
65
+ // ---------------------------------------------------------------------------
66
+
67
+ function PostHogPageviewTracker() {
68
+ const pathname = usePathname();
69
+ const searchParams = useSearchParams();
70
+
71
+ useEffect(() => {
72
+ if (!pathname) return;
73
+
74
+ const search = searchParams?.toString();
75
+ const url = window.location.origin + pathname + (search ? `?${search}` : '');
76
+ const { page_name, page_slug } = resolvePageInfo(pathname);
77
+
78
+ posthog.capture('$pageview', {
79
+ $current_url: url,
80
+ page_name,
81
+ page_path: pathname,
82
+ ...(page_slug && { page_slug }),
83
+ });
84
+ }, [pathname, searchParams]);
85
+
86
+ return null;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Provider
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Initialises PostHog, registers account-level super properties, and fires
95
+ * an enriched `$pageview` on every App Router navigation.
96
+ *
97
+ * Super properties attached to every event automatically:
98
+ * - account_id (Keystone account ID)
99
+ * - account_name (company_name)
100
+ * - site_domain (window.location.hostname)
101
+ *
102
+ * Mount once in the root layout body. One project key covers all customer
103
+ * sites — filter by account_name or site_domain in the PostHog dashboard.
104
+ */
105
+ export function PostHogProvider({ apiKey, apiHost, accountId, accountName, children }: PostHogProviderProps) {
106
+ useEffect(() => {
107
+ posthog.init(apiKey, {
108
+ api_host: apiHost ?? DEFAULT_HOST,
109
+ person_profiles: 'identified_only',
110
+ capture_pageview: false,
111
+ });
112
+
113
+ posthog.register({
114
+ ...(accountId !== undefined && { account_id: accountId }),
115
+ ...(accountName && { account_name: accountName }),
116
+ site_domain: window.location.hostname,
117
+ });
118
+ }, [apiKey, apiHost, accountId, accountName]);
119
+
120
+ return (
121
+ <PHProvider client={posthog}>
122
+ <Suspense fallback={null}>
123
+ <PostHogPageviewTracker />
124
+ </Suspense>
125
+ {children}
126
+ </PHProvider>
127
+ );
128
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * PostHog event capture — Keystone customer sites.
3
+ *
4
+ * ## Naming convention
5
+ * All events use snake_case `object_action` format.
6
+ * Properties use snake_case as well.
7
+ *
8
+ * ## Event taxonomy
9
+ * Add new events here: define the name in `KsEventName` and its required
10
+ * properties in `KsEventProperties`. Every callsite is then type-checked.
11
+ *
12
+ * ## Usage
13
+ *
14
+ * import { captureEvent } from 'keystone-design-bootstrap/tracking';
15
+ *
16
+ * captureEvent('form_submitted', { form_type: 'lead' });
17
+ * captureEvent('booking_cta_clicked', { source_path: '/services/massage', booking_url: url });
18
+ *
19
+ * All calls are safe no-ops when PostHog has not been initialised (e.g. no
20
+ * POSTHOG_API_KEY configured on the server).
21
+ *
22
+ * ## Super properties
23
+ * account_id, account_name, and site_domain are registered as super properties
24
+ * by PostHogProvider and are automatically attached to every event — you do not
25
+ * need to include them in individual captureEvent calls.
26
+ */
27
+
28
+ import posthog from 'posthog-js';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Event taxonomy
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export type KsEventName =
35
+ // Navigation
36
+ | 'page_viewed'
37
+ // Booking / conversion
38
+ | 'booking_cta_clicked'
39
+ // Forms
40
+ | 'form_submitted'
41
+ | 'form_failed'
42
+ // Chat widget
43
+ | 'chat_opened'
44
+ | 'chat_message_sent'
45
+ | 'chat_message_failed'
46
+ // Member portal — tab navigation
47
+ | 'portal_tab_viewed'
48
+ // Member portal — authentication flow
49
+ | 'portal_login_started'
50
+ | 'portal_login_identified'
51
+ | 'portal_login_completed'
52
+ | 'portal_login_failed';
53
+
54
+ export type KsEventProperties = {
55
+ /** Fired on every page navigation. $pageview is also fired for PostHog web analytics. */
56
+ page_viewed: {
57
+ page_name: string;
58
+ page_path: string;
59
+ page_slug?: string;
60
+ };
61
+
62
+ /** Fired when a visitor clicks any CTA that links to the external booking URL. */
63
+ booking_cta_clicked: {
64
+ source_path: string;
65
+ booking_url: string;
66
+ };
67
+
68
+ /** Fired when a Keystone form is successfully submitted. */
69
+ form_submitted: {
70
+ /** One of: lead | job_application | marketing_list_signup */
71
+ form_type: string;
72
+ /** Server-generated event ID for CAPI deduplication (when present). */
73
+ event_id?: string;
74
+ };
75
+
76
+ /** Fired when a form submission fails (validation error or network error). */
77
+ form_failed: {
78
+ form_type: string;
79
+ error: string;
80
+ };
81
+
82
+ /** Fired when the chat widget is first opened by the visitor. */
83
+ chat_opened: Record<string, never>;
84
+
85
+ /** Fired when a chat message is successfully sent. */
86
+ chat_message_sent: {
87
+ /** Whether the visitor is authenticated (contactId present). */
88
+ is_authenticated: boolean;
89
+ };
90
+
91
+ /** Fired when a chat message fails to send. */
92
+ chat_message_failed: {
93
+ error: string;
94
+ };
95
+
96
+ /** Fired when a member portal tab is opened. */
97
+ portal_tab_viewed: {
98
+ tab: string;
99
+ };
100
+
101
+ /** Fired when the portal login modal is opened / login flow starts. */
102
+ portal_login_started: Record<string, never>;
103
+
104
+ /**
105
+ * Fired after the identifier step resolves — we know whether the user
106
+ * already has an account.
107
+ */
108
+ portal_login_identified: {
109
+ method: 'email' | 'phone' | 'email_and_phone';
110
+ user_exists: boolean;
111
+ };
112
+
113
+ /** Fired after the user successfully signs in or creates an account. */
114
+ portal_login_completed: {
115
+ flow: 'signin' | 'signup';
116
+ };
117
+
118
+ /** Fired when any step of the login flow returns an error. */
119
+ portal_login_failed: {
120
+ step: 'identifier' | 'signin' | 'signup';
121
+ reason: string;
122
+ };
123
+ };
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Capture helper
127
+ // ---------------------------------------------------------------------------
128
+
129
+ /**
130
+ * Captures a typed Keystone analytics event via PostHog.
131
+ * Safe no-op when PostHog has not been initialised.
132
+ */
133
+ export function captureEvent<E extends KsEventName>(
134
+ event: E,
135
+ ...args: KsEventProperties[E] extends Record<string, never>
136
+ ? []
137
+ : [properties: KsEventProperties[E]]
138
+ ): void {
139
+ posthog.capture(event, args[0] as Record<string, unknown>);
140
+ }
@@ -23,3 +23,8 @@ export type { MetaPixelProps } from './MetaPixel';
23
23
  export { MetaPixelTracker } from './MetaPixelTracker';
24
24
  export { firePixelEvent, setPixelUserData } from './firePixelEvent';
25
25
  export type { PixelEvent, PixelEventParams, PixelUserData } from './firePixelEvent';
26
+ export { PostHogProvider } from './PostHogProvider';
27
+ export type { PostHogProviderProps } from './PostHogProvider';
28
+ export { KeystoneAnalyticsTracker } from './KeystoneAnalyticsTracker';
29
+ export { captureEvent } from './captureEvent';
30
+ export type { KsEventName, KsEventProperties } from './captureEvent';