@zoyth/simple-site-framework 1.0.0
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/LICENSE +21 -0
- package/README.md +572 -0
- package/bin/create-simple-site.js +390 -0
- package/bin/simple-site.js +664 -0
- package/dist/client.js +135 -0
- package/dist/client.js.map +1 -0
- package/dist/client.mjs +107 -0
- package/dist/client.mjs.map +1 -0
- package/dist/components/index.d.mts +3936 -0
- package/dist/components/index.d.ts +3936 -0
- package/dist/components/index.js +38265 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +38173 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/config/index.d.mts +298 -0
- package/dist/config/index.d.ts +298 -0
- package/dist/config/index.js +19 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.mjs +1 -0
- package/dist/config/index.mjs.map +1 -0
- package/dist/index.d.mts +2184 -0
- package/dist/index.d.ts +2184 -0
- package/dist/index.js +1713 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1605 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/i18n/index.js +665 -0
- package/dist/lib/i18n/index.js.map +1 -0
- package/dist/lib/i18n/index.mjs +621 -0
- package/dist/lib/i18n/index.mjs.map +1 -0
- package/docs/DOCUMENTATION-STRUCTURE.md +1156 -0
- package/docs/EXPORTS.md +125 -0
- package/docs/PERFORMANCE.md +757 -0
- package/docs/POLICY-PAGES.md +867 -0
- package/docs/ROADMAP.md +334 -0
- package/docs/SEO.md +455 -0
- package/docs/SITEMAP.md +708 -0
- package/docs/STRUCTURED-DATA.md +671 -0
- package/docs/accessibility/common-patterns.md +529 -0
- package/docs/accessibility/keyboard-navigation.md +263 -0
- package/docs/accessibility/overview.md +122 -0
- package/docs/accessibility/screen-readers.md +311 -0
- package/docs/accessibility/wcag-compliance.md +159 -0
- package/docs/api/README.md +164 -0
- package/docs/api/components/Accessibility.md +356 -0
- package/docs/api/components/Button.md +240 -0
- package/docs/api/components/HeroSection.md +306 -0
- package/docs/architecture/decisions.md +449 -0
- package/docs/components/AnalyticsTracker.md +58 -0
- package/docs/components/AnimatedCounter.md +48 -0
- package/docs/components/AnimatedSection.md +56 -0
- package/docs/components/BlogCard.md +42 -0
- package/docs/components/Checkbox.md +56 -0
- package/docs/components/CodeBlock.md +52 -0
- package/docs/components/ComparisonTable.md +40 -0
- package/docs/components/ComponentDemo.md +38 -0
- package/docs/components/CountdownTimer.md +51 -0
- package/docs/components/ExitIntentModal.md +56 -0
- package/docs/components/FAQAccordion.md +66 -0
- package/docs/components/FeaturesGrid.md +55 -0
- package/docs/components/FileUpload.md +54 -0
- package/docs/components/I18nMetaTags.md +55 -0
- package/docs/components/Icon.md +53 -0
- package/docs/components/LazySection.md +46 -0
- package/docs/components/LiveProof.md +53 -0
- package/docs/components/LoadingSpinner.md +46 -0
- package/docs/components/MultiStepForm.md +48 -0
- package/docs/components/PolicyLayout.md +55 -0
- package/docs/components/PricingTable.md +49 -0
- package/docs/components/Radio.md +59 -0
- package/docs/components/SEOMetaTags.md +58 -0
- package/docs/components/ScriptInjector.md +50 -0
- package/docs/components/Select.md +72 -0
- package/docs/components/Skeleton.md +47 -0
- package/docs/components/StatsSection.md +48 -0
- package/docs/components/StickyBar.md +62 -0
- package/docs/components/StructuredData.md +99 -0
- package/docs/components/StyleGuide.md +46 -0
- package/docs/components/TableOfContents.md +47 -0
- package/docs/components/TestimonialCarousel.md +42 -0
- package/docs/components/Timeline.md +51 -0
- package/docs/components/Toast.md +59 -0
- package/docs/components/TrackedLink.md +62 -0
- package/docs/components/TrustBadges.md +44 -0
- package/docs/components/conversion/MobileCTA.md +363 -0
- package/docs/components/forms/ContactForm.md +75 -0
- package/docs/components/forms/FormField.md +74 -0
- package/docs/components/layout/Footer.md +601 -0
- package/docs/components/layout/Header.md +549 -0
- package/docs/components/layout/LanguageSelector.md +54 -0
- package/docs/components/layout/LanguageSwitcher.md +24 -0
- package/docs/components/overview.md +447 -0
- package/docs/components/sections/AboutSection.md +48 -0
- package/docs/components/sections/CTASection.md +596 -0
- package/docs/components/sections/CaseStudySection.md +47 -0
- package/docs/components/sections/ContactSection.md +599 -0
- package/docs/components/sections/FeatureSection.md +44 -0
- package/docs/components/sections/HeroSection.md +404 -0
- package/docs/components/sections/LogosSection.md +47 -0
- package/docs/components/sections/PersonalTaxesSection.md +23 -0
- package/docs/components/sections/RecruitingSection.md +23 -0
- package/docs/components/sections/SecurePortalSection.md +23 -0
- package/docs/components/sections/ServicePageLayout.md +52 -0
- package/docs/components/sections/ServicesSection.md +49 -0
- package/docs/components/sections/TestimonialSection.md +44 -0
- package/docs/components/sections/WhyChooseUsSection.md +54 -0
- package/docs/components/ui/Breadcrumb.md +70 -0
- package/docs/components/ui/Button.md +514 -0
- package/docs/components/ui/Card.md +501 -0
- package/docs/components/ui/Input.md +54 -0
- package/docs/components/ui/MobileLinks.md +43 -0
- package/docs/components/ui/Modal.md +60 -0
- package/docs/components/ui/Tabs.md +62 -0
- package/docs/components/ui/Textarea.md +52 -0
- package/docs/core-concepts/configuration-driven.md +552 -0
- package/docs/core-concepts/overview.md +351 -0
- package/docs/features/accessibility/README.md +73 -0
- package/docs/features/accessibility/aria-support.md +177 -0
- package/docs/features/accessibility/color-contrast.md +155 -0
- package/docs/features/accessibility/focus-management.md +187 -0
- package/docs/features/accessibility/testing.md +196 -0
- package/docs/features/analytics/README.md +51 -0
- package/docs/features/analytics/ab-testing.md +171 -0
- package/docs/features/analytics/conversion-tracking.md +207 -0
- package/docs/features/analytics/custom-events.md +219 -0
- package/docs/features/analytics/privacy.md +198 -0
- package/docs/features/analytics/setup.md +114 -0
- package/docs/features/analytics/tracking-events.md +224 -0
- package/docs/features/i18n/README.md +51 -0
- package/docs/features/i18n/best-practices.md +273 -0
- package/docs/features/i18n/configuration.md +84 -0
- package/docs/features/i18n/formatting.md +133 -0
- package/docs/features/i18n/locale-detection.md +122 -0
- package/docs/features/i18n/routing.md +99 -0
- package/docs/features/i18n/rtl-support.md +191 -0
- package/docs/features/i18n/translations.md +129 -0
- package/docs/features/internationalization.md +595 -0
- package/docs/features/performance/README.md +77 -0
- package/docs/features/performance/bundle-size.md +134 -0
- package/docs/features/performance/caching.md +131 -0
- package/docs/features/performance/code-splitting.md +121 -0
- package/docs/features/performance/image-optimization.md +110 -0
- package/docs/features/performance/lazy-loading.md +92 -0
- package/docs/features/performance/monitoring.md +148 -0
- package/docs/features/seo/README.md +51 -0
- package/docs/features/seo/best-practices.md +184 -0
- package/docs/features/seo/canonical-urls.md +182 -0
- package/docs/features/seo/meta-tags.md +126 -0
- package/docs/features/seo/open-graph.md +166 -0
- package/docs/features/seo/robots-txt.md +146 -0
- package/docs/features/seo/sitemaps.md +162 -0
- package/docs/features/seo/structured-data.md +166 -0
- package/docs/getting-started/installation.md +292 -0
- package/docs/getting-started/introduction.md +195 -0
- package/docs/getting-started/quick-start.md +460 -0
- package/docs/guides/analytics-setup.md +616 -0
- package/docs/i18n/CONFIGURATION.md +353 -0
- package/docs/i18n/EXAMPLES.md +402 -0
- package/docs/i18n/MIGRATION.md +260 -0
- package/docs/i18n/SEO.md +392 -0
- package/docs/i18n/STATIC-GENERATION-FIX.md +71 -0
- package/docs/migration/changelog.md +136 -0
- package/docs/migration/overview.md +233 -0
- package/docs/recipes/adding-animations.md +475 -0
- package/docs/recipes/forms-with-validation.md +393 -0
- package/package.json +152 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
"use client";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/client.ts
|
|
23
|
+
var client_exports = {};
|
|
24
|
+
__export(client_exports, {
|
|
25
|
+
AnalyticsTracker: () => AnalyticsTracker,
|
|
26
|
+
getABTestVariant: () => getABTestVariant,
|
|
27
|
+
trackABTestEvent: () => trackABTestEvent
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(client_exports);
|
|
30
|
+
|
|
31
|
+
// src/components/AnalyticsTracker.tsx
|
|
32
|
+
var import_react = require("react");
|
|
33
|
+
var import_navigation = require("next/navigation");
|
|
34
|
+
|
|
35
|
+
// src/lib/analytics.ts
|
|
36
|
+
function pushToDataLayer(data) {
|
|
37
|
+
if (typeof window === "undefined") return;
|
|
38
|
+
window.dataLayer = window.dataLayer || [];
|
|
39
|
+
window.dataLayer.push(data);
|
|
40
|
+
if (process.env.NODE_ENV === "development") {
|
|
41
|
+
console.log("[Analytics]", data);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function trackPageView(pagePath, pageTitle) {
|
|
45
|
+
pushToDataLayer({
|
|
46
|
+
event: "page_view",
|
|
47
|
+
page_path: pagePath,
|
|
48
|
+
page_title: pageTitle
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function trackABTestEvent(testId, variant, eventName, metadata) {
|
|
52
|
+
pushToDataLayer({
|
|
53
|
+
event: "ab_test_event",
|
|
54
|
+
event_category: "ab_test",
|
|
55
|
+
event_label: `${testId}_${variant}_${eventName}`,
|
|
56
|
+
test_id: testId,
|
|
57
|
+
variant,
|
|
58
|
+
test_event: eventName,
|
|
59
|
+
...metadata
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function trackScrollDepth(percentage, pagePath) {
|
|
63
|
+
pushToDataLayer({
|
|
64
|
+
event: "scroll_depth",
|
|
65
|
+
event_category: "engagement",
|
|
66
|
+
event_label: `${percentage}%`,
|
|
67
|
+
scroll_percentage: percentage,
|
|
68
|
+
page_path: pagePath
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/components/AnalyticsTracker.tsx
|
|
73
|
+
function AnalyticsTracker() {
|
|
74
|
+
if (typeof window === "undefined" && typeof document === "undefined") {
|
|
75
|
+
if (process.env.NODE_ENV !== "production") {
|
|
76
|
+
console.error(
|
|
77
|
+
`AnalyticsTracker: This component requires browser APIs and cannot run in edge runtime or SSR. Import from "simple-site-framework/client" and ensure it's only used in client components.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const pathname = (0, import_navigation.usePathname)();
|
|
83
|
+
const scrollTracked = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
84
|
+
(0, import_react.useEffect)(() => {
|
|
85
|
+
if (pathname) {
|
|
86
|
+
trackPageView(pathname, document.title);
|
|
87
|
+
scrollTracked.current.clear();
|
|
88
|
+
}
|
|
89
|
+
}, [pathname]);
|
|
90
|
+
(0, import_react.useEffect)(() => {
|
|
91
|
+
const handleScroll = () => {
|
|
92
|
+
const windowHeight = window.innerHeight;
|
|
93
|
+
const documentHeight = document.documentElement.scrollHeight;
|
|
94
|
+
const scrollTop = window.scrollY;
|
|
95
|
+
const scrollPercent = scrollTop / (documentHeight - windowHeight) * 100;
|
|
96
|
+
const milestones = [25, 50, 75, 100];
|
|
97
|
+
for (const milestone of milestones) {
|
|
98
|
+
if (scrollPercent >= milestone && !scrollTracked.current.has(milestone)) {
|
|
99
|
+
scrollTracked.current.add(milestone);
|
|
100
|
+
trackScrollDepth(milestone, pathname || "/");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
105
|
+
handleScroll();
|
|
106
|
+
return () => {
|
|
107
|
+
window.removeEventListener("scroll", handleScroll);
|
|
108
|
+
};
|
|
109
|
+
}, [pathname]);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/lib/ab-test.ts
|
|
114
|
+
function getABTestVariant(config) {
|
|
115
|
+
if (typeof window === "undefined") {
|
|
116
|
+
return "A";
|
|
117
|
+
}
|
|
118
|
+
const storageKey = `ab-test-${config.testId}`;
|
|
119
|
+
const stored = localStorage.getItem(storageKey);
|
|
120
|
+
if (stored === "A" || stored === "B") {
|
|
121
|
+
return stored;
|
|
122
|
+
}
|
|
123
|
+
const totalWeight = config.variants.A.weight + config.variants.B.weight;
|
|
124
|
+
const random = Math.random() * totalWeight;
|
|
125
|
+
const variant = random < config.variants.A.weight ? "A" : "B";
|
|
126
|
+
localStorage.setItem(storageKey, variant);
|
|
127
|
+
return variant;
|
|
128
|
+
}
|
|
129
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
130
|
+
0 && (module.exports = {
|
|
131
|
+
AnalyticsTracker,
|
|
132
|
+
getABTestVariant,
|
|
133
|
+
trackABTestEvent
|
|
134
|
+
});
|
|
135
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/components/AnalyticsTracker.tsx","../src/lib/analytics.ts","../src/lib/ab-test.ts"],"sourcesContent":["// ABOUTME: Client-only exports for browser/React Server Components\n// ABOUTME: Import from 'simple-site-framework/client' for client components\n\n'use client';\n\n// Client-only components that use browser APIs or React hooks\nexport { AnalyticsTracker } from './components/AnalyticsTracker';\n\n// A/B testing utilities (client-only - use localStorage and window)\nexport { getABTestVariant } from './lib/ab-test';\nexport type { ABTestVariant, ABTestConfig } from './lib/ab-test';\n\n// Analytics utilities\nexport { trackABTestEvent } from './lib/analytics';\n\n// Re-export type for convenience\nexport type { AnalyticsEvent } from './lib/analytics';\n","// ABOUTME: Client-side analytics tracker for page views and scroll depth\n// ABOUTME: Automatically tracks page views and scroll milestones\n'use client';\n\nimport { useEffect, useRef } from 'react';\nimport { usePathname } from 'next/navigation';\nimport { trackPageView, trackScrollDepth } from '../lib/analytics';\n\n/**\n * AnalyticsTracker - Client-only component for tracking page views and scroll depth\n *\n * ⚠️ Edge Runtime Warning:\n * This component uses browser-only APIs (window, document) and Next.js client hooks (usePathname).\n * Do NOT import from main index in middleware or edge functions.\n * Use: import { AnalyticsTracker } from 'simple-site-framework/client'\n */\nexport function AnalyticsTracker() {\n // Edge-safe check: fail fast with clear error if used in wrong context\n if (typeof window === 'undefined' && typeof document === 'undefined') {\n if (process.env.NODE_ENV !== 'production') {\n console.error(\n 'AnalyticsTracker: This component requires browser APIs and cannot run in edge runtime or SSR. ' +\n 'Import from \"simple-site-framework/client\" and ensure it\\'s only used in client components.'\n );\n }\n return null;\n }\n\n const pathname = usePathname();\n const scrollTracked = useRef<Set<number>>(new Set());\n\n // Track page views on route change\n useEffect(() => {\n if (pathname) {\n trackPageView(pathname, document.title);\n // Reset scroll tracking for new page\n scrollTracked.current.clear();\n }\n }, [pathname]);\n\n // Track scroll depth\n useEffect(() => {\n const handleScroll = () => {\n const windowHeight = window.innerHeight;\n const documentHeight = document.documentElement.scrollHeight;\n const scrollTop = window.scrollY;\n const scrollPercent = (scrollTop / (documentHeight - windowHeight)) * 100;\n\n const milestones = [25, 50, 75, 100] as const;\n\n for (const milestone of milestones) {\n if (scrollPercent >= milestone && !scrollTracked.current.has(milestone)) {\n scrollTracked.current.add(milestone);\n trackScrollDepth(milestone, pathname || '/');\n }\n }\n };\n\n // Add scroll listener\n window.addEventListener('scroll', handleScroll, { passive: true });\n\n // Check immediately in case page loads already scrolled\n handleScroll();\n\n return () => {\n window.removeEventListener('scroll', handleScroll);\n };\n }, [pathname]);\n\n return null;\n}\n","// ABOUTME: Comprehensive analytics tracking utility for GA4 via GTM\n// ABOUTME: Provides type-safe event tracking for conversions, CTAs, forms, and user interactions\n\nimport type { AnalyticsEvent } from '../types/analytics';\n\ndeclare global {\n interface Window {\n dataLayer: unknown[];\n }\n}\n\n// Re-export types for convenience\nexport type * from '../types/analytics';\n\n/**\n * Push event to Google Tag Manager dataLayer\n */\nfunction pushToDataLayer(data: AnalyticsEvent) {\n if (typeof window === 'undefined') return;\n\n window.dataLayer = window.dataLayer || [];\n window.dataLayer.push(data);\n\n // Log in development\n if (process.env.NODE_ENV === 'development') {\n console.log('[Analytics]', data);\n }\n}\n\n/**\n * Generic event tracking function\n * Use this for custom events not covered by specific tracking functions\n *\n * @param eventName - Name of the event\n * @param properties - Additional event properties\n *\n * @example\n * ```typescript\n * trackEvent('button_click', {\n * button_text: 'Sign Up',\n * button_location: 'hero',\n * button_variant: 'primary'\n * });\n * ```\n */\nexport function trackEvent(eventName: string, properties?: Record<string, unknown>) {\n pushToDataLayer({\n event: eventName,\n ...properties,\n });\n}\n\n/**\n * Track CTA click events\n * @param ctaLocation - Where the CTA appears (e.g., 'hero', 'pricing', 'mobile_sticky')\n * @param ctaText - The text on the CTA button\n * @param ctaType - Type of CTA (e.g., 'signup', 'trial', 'contact')\n */\nexport function trackCTAClick(\n ctaLocation: string,\n ctaText: string,\n ctaType: 'signup' | 'trial' | 'contact' | 'download' | 'other' = 'other'\n) {\n pushToDataLayer({\n event: 'cta_click',\n event_category: 'cta',\n event_label: `${ctaLocation}_${ctaType}`,\n cta_location: ctaLocation,\n cta_text: ctaText,\n cta_type: ctaType,\n });\n}\n\n/**\n * Track form interactions\n * @param formName - Identifier for the form\n * @param action - Form action (start, submit, error, abandon)\n */\nexport function trackFormEvent(\n formName: string,\n action: 'start' | 'submit' | 'error' | 'abandon',\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: `form_${action}`,\n event_category: 'form',\n event_label: formName,\n form_name: formName,\n form_action: action,\n ...metadata,\n });\n}\n\n/**\n * Track page view (for SPAs and custom tracking)\n * @param pagePath - The page path\n * @param pageTitle - The page title\n */\nexport function trackPageView(pagePath: string, pageTitle: string) {\n pushToDataLayer({\n event: 'page_view',\n page_path: pagePath,\n page_title: pageTitle,\n });\n}\n\n/**\n * Track pricing page interactions\n * @param action - Type of interaction\n */\nexport function trackPricingEvent(\n action: 'view' | 'calculate' | 'plan_select' | 'currency_change',\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: 'pricing_interaction',\n event_category: 'engagement',\n event_label: action,\n pricing_action: action,\n ...metadata,\n });\n}\n\n/**\n * Track feature page engagement\n * @param featureName - Name of the feature being viewed\n * @param action - Type of engagement\n */\nexport function trackFeatureEngagement(\n featureName: string,\n action: 'view' | 'video_play' | 'video_complete' | 'cta_click',\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: 'feature_engagement',\n event_category: 'engagement',\n event_label: `${featureName}_${action}`,\n feature_name: featureName,\n feature_action: action,\n ...metadata,\n });\n}\n\n/**\n * Track resource downloads\n * @param resourceName - Name of the resource\n * @param resourceType - Type of resource (pdf, video, etc.)\n */\nexport function trackResourceDownload(\n resourceName: string,\n resourceType: string\n) {\n pushToDataLayer({\n event: 'resource_download',\n event_category: 'engagement',\n event_label: resourceName,\n resource_name: resourceName,\n resource_type: resourceType,\n });\n}\n\n/**\n * Track video interactions\n * @param videoTitle - Title of the video\n * @param action - Video action\n */\nexport function trackVideoEvent(\n videoTitle: string,\n action: 'play' | 'pause' | 'complete' | '25%' | '50%' | '75%',\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: 'video_interaction',\n event_category: 'engagement',\n event_label: `${videoTitle}_${action}`,\n video_title: videoTitle,\n video_action: action,\n ...metadata,\n });\n}\n\n/**\n * Track navigation events\n * @param linkText - Text of the link clicked\n * @param linkUrl - URL of the link\n * @param linkLocation - Where the link appears (header, footer, content)\n */\nexport function trackNavigation(\n linkText: string,\n linkUrl: string,\n linkLocation: 'header' | 'footer' | 'content' | 'mobile'\n) {\n pushToDataLayer({\n event: 'navigation_click',\n event_category: 'navigation',\n event_label: linkText,\n link_text: linkText,\n link_url: linkUrl,\n link_location: linkLocation,\n });\n}\n\n/**\n * Track A/B test variant assignment and events\n * @param testId - ID of the A/B test\n * @param variant - Variant assigned (A or B)\n * @param eventName - Name of the event\n */\nexport function trackABTestEvent(\n testId: string,\n variant: 'A' | 'B',\n eventName: string,\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: 'ab_test_event',\n event_category: 'ab_test',\n event_label: `${testId}_${variant}_${eventName}`,\n test_id: testId,\n variant: variant,\n test_event: eventName,\n ...metadata,\n });\n}\n\n/**\n * Track conversion events (trial signups, purchases, etc.)\n * @param conversionType - Type of conversion\n * @param value - Monetary value (optional)\n */\nexport function trackConversion(\n conversionType: 'trial_signup' | 'contact' | 'newsletter' | 'other',\n value?: number,\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: 'conversion',\n event_category: 'conversion',\n event_label: conversionType,\n conversion_type: conversionType,\n value: value,\n ...metadata,\n });\n}\n\n/**\n * Track scroll depth\n * @param percentage - Scroll depth percentage (25, 50, 75, 100)\n * @param pagePath - The page path\n */\nexport function trackScrollDepth(\n percentage: 25 | 50 | 75 | 100,\n pagePath: string\n) {\n pushToDataLayer({\n event: 'scroll_depth',\n event_category: 'engagement',\n event_label: `${percentage}%`,\n scroll_percentage: percentage,\n page_path: pagePath,\n });\n}\n\n/**\n * Track search events\n * @param searchTerm - The search term\n * @param resultCount - Number of results (optional)\n */\nexport function trackSearch(searchTerm: string, resultCount?: number) {\n pushToDataLayer({\n event: 'search',\n event_category: 'engagement',\n event_label: searchTerm,\n search_term: searchTerm,\n result_count: resultCount,\n });\n}\n\n/**\n * Track outbound link clicks\n * @param url - The external URL\n * @param linkText - Text of the link\n */\nexport function trackOutboundLink(url: string, linkText: string) {\n pushToDataLayer({\n event: 'outbound_link',\n event_category: 'navigation',\n event_label: url,\n outbound_url: url,\n link_text: linkText,\n });\n}\n\n/**\n * Track error events\n * @param errorType - Type of error\n * @param errorMessage - Error message or description\n */\nexport function trackError(errorType: string, errorMessage: string) {\n pushToDataLayer({\n event: 'error',\n event_category: 'engagement',\n event_label: errorType,\n error_type: errorType,\n error_message: errorMessage,\n });\n}\n","// ABOUTME: A/B testing utility for CTA variations\n// ABOUTME: Uses localStorage to persist user's assigned variant\n// ABOUTME: Integrated with analytics tracking\n\n'use client';\n\nimport { trackABTestEvent as analyticsTrackABTestEvent } from './analytics';\n\nexport type ABTestVariant = 'A' | 'B';\n\nexport interface ABTestConfig {\n testId: string;\n variants: {\n A: { weight: number };\n B: { weight: number };\n };\n}\n\n/**\n * Get the assigned variant for a test, or assign one if not already assigned\n * Uses localStorage to persist the assignment across sessions\n */\nexport function getABTestVariant(config: ABTestConfig): ABTestVariant {\n if (typeof window === 'undefined') {\n // Server-side: return default variant\n return 'A';\n }\n\n const storageKey = `ab-test-${config.testId}`;\n const stored = localStorage.getItem(storageKey);\n\n if (stored === 'A' || stored === 'B') {\n return stored;\n }\n\n // Assign new variant based on weights\n const totalWeight = config.variants.A.weight + config.variants.B.weight;\n const random = Math.random() * totalWeight;\n const variant: ABTestVariant = random < config.variants.A.weight ? 'A' : 'B';\n\n localStorage.setItem(storageKey, variant);\n return variant;\n}\n\n/**\n * Track A/B test event with integrated analytics\n * @deprecated Import trackABTestEvent directly from analytics instead\n */\nfunction trackABTestEvent(\n testId: string,\n variant: ABTestVariant,\n eventName: string,\n metadata?: Record<string, unknown>\n) {\n if (typeof window === 'undefined') return;\n\n // Track via analytics library\n analyticsTrackABTestEvent(testId, variant, eventName, metadata);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,mBAAkC;AAClC,wBAA4B;;;ACY5B,SAAS,gBAAgB,MAAsB;AAC7C,MAAI,OAAO,WAAW,YAAa;AAEnC,SAAO,YAAY,OAAO,aAAa,CAAC;AACxC,SAAO,UAAU,KAAK,IAAI;AAG1B,MAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,YAAQ,IAAI,eAAe,IAAI;AAAA,EACjC;AACF;AAuEO,SAAS,cAAc,UAAkB,WAAmB;AACjE,kBAAgB;AAAA,IACd,OAAO;AAAA,IACP,WAAW;AAAA,IACX,YAAY;AAAA,EACd,CAAC;AACH;AAwGO,SAAS,iBACd,QACA,SACA,WACA,UACA;AACA,kBAAgB;AAAA,IACd,OAAO;AAAA,IACP,gBAAgB;AAAA,IAChB,aAAa,GAAG,MAAM,IAAI,OAAO,IAAI,SAAS;AAAA,IAC9C,SAAS;AAAA,IACT;AAAA,IACA,YAAY;AAAA,IACZ,GAAG;AAAA,EACL,CAAC;AACH;AA2BO,SAAS,iBACd,YACA,UACA;AACA,kBAAgB;AAAA,IACd,OAAO;AAAA,IACP,gBAAgB;AAAA,IAChB,aAAa,GAAG,UAAU;AAAA,IAC1B,mBAAmB;AAAA,IACnB,WAAW;AAAA,EACb,CAAC;AACH;;;ADrPO,SAAS,mBAAmB;AAEjC,MAAI,OAAO,WAAW,eAAe,OAAO,aAAa,aAAa;AACpE,QAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,cAAQ;AAAA,QACN;AAAA,MAEF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,eAAW,+BAAY;AAC7B,QAAM,oBAAgB,qBAAoB,oBAAI,IAAI,CAAC;AAGnD,8BAAU,MAAM;AACd,QAAI,UAAU;AACZ,oBAAc,UAAU,SAAS,KAAK;AAEtC,oBAAc,QAAQ,MAAM;AAAA,IAC9B;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAGb,8BAAU,MAAM;AACd,UAAM,eAAe,MAAM;AACzB,YAAM,eAAe,OAAO;AAC5B,YAAM,iBAAiB,SAAS,gBAAgB;AAChD,YAAM,YAAY,OAAO;AACzB,YAAM,gBAAiB,aAAa,iBAAiB,gBAAiB;AAEtE,YAAM,aAAa,CAAC,IAAI,IAAI,IAAI,GAAG;AAEnC,iBAAW,aAAa,YAAY;AAClC,YAAI,iBAAiB,aAAa,CAAC,cAAc,QAAQ,IAAI,SAAS,GAAG;AACvE,wBAAc,QAAQ,IAAI,SAAS;AACnC,2BAAiB,WAAW,YAAY,GAAG;AAAA,QAC7C;AAAA,MACF;AAAA,IACF;AAGA,WAAO,iBAAiB,UAAU,cAAc,EAAE,SAAS,KAAK,CAAC;AAGjE,iBAAa;AAEb,WAAO,MAAM;AACX,aAAO,oBAAoB,UAAU,YAAY;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAEb,SAAO;AACT;;;AEhDO,SAAS,iBAAiB,QAAqC;AACpE,MAAI,OAAO,WAAW,aAAa;AAEjC,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,WAAW,OAAO,MAAM;AAC3C,QAAM,SAAS,aAAa,QAAQ,UAAU;AAE9C,MAAI,WAAW,OAAO,WAAW,KAAK;AACpC,WAAO;AAAA,EACT;AAGA,QAAM,cAAc,OAAO,SAAS,EAAE,SAAS,OAAO,SAAS,EAAE;AACjE,QAAM,SAAS,KAAK,OAAO,IAAI;AAC/B,QAAM,UAAyB,SAAS,OAAO,SAAS,EAAE,SAAS,MAAM;AAEzE,eAAa,QAAQ,YAAY,OAAO;AACxC,SAAO;AACT;","names":[]}
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
// src/components/AnalyticsTracker.tsx
|
|
5
|
+
import { useEffect, useRef } from "react";
|
|
6
|
+
import { usePathname } from "next/navigation";
|
|
7
|
+
|
|
8
|
+
// src/lib/analytics.ts
|
|
9
|
+
function pushToDataLayer(data) {
|
|
10
|
+
if (typeof window === "undefined") return;
|
|
11
|
+
window.dataLayer = window.dataLayer || [];
|
|
12
|
+
window.dataLayer.push(data);
|
|
13
|
+
if (process.env.NODE_ENV === "development") {
|
|
14
|
+
console.log("[Analytics]", data);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function trackPageView(pagePath, pageTitle) {
|
|
18
|
+
pushToDataLayer({
|
|
19
|
+
event: "page_view",
|
|
20
|
+
page_path: pagePath,
|
|
21
|
+
page_title: pageTitle
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function trackABTestEvent(testId, variant, eventName, metadata) {
|
|
25
|
+
pushToDataLayer({
|
|
26
|
+
event: "ab_test_event",
|
|
27
|
+
event_category: "ab_test",
|
|
28
|
+
event_label: `${testId}_${variant}_${eventName}`,
|
|
29
|
+
test_id: testId,
|
|
30
|
+
variant,
|
|
31
|
+
test_event: eventName,
|
|
32
|
+
...metadata
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function trackScrollDepth(percentage, pagePath) {
|
|
36
|
+
pushToDataLayer({
|
|
37
|
+
event: "scroll_depth",
|
|
38
|
+
event_category: "engagement",
|
|
39
|
+
event_label: `${percentage}%`,
|
|
40
|
+
scroll_percentage: percentage,
|
|
41
|
+
page_path: pagePath
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/components/AnalyticsTracker.tsx
|
|
46
|
+
function AnalyticsTracker() {
|
|
47
|
+
if (typeof window === "undefined" && typeof document === "undefined") {
|
|
48
|
+
if (process.env.NODE_ENV !== "production") {
|
|
49
|
+
console.error(
|
|
50
|
+
`AnalyticsTracker: This component requires browser APIs and cannot run in edge runtime or SSR. Import from "simple-site-framework/client" and ensure it's only used in client components.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const pathname = usePathname();
|
|
56
|
+
const scrollTracked = useRef(/* @__PURE__ */ new Set());
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (pathname) {
|
|
59
|
+
trackPageView(pathname, document.title);
|
|
60
|
+
scrollTracked.current.clear();
|
|
61
|
+
}
|
|
62
|
+
}, [pathname]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const handleScroll = () => {
|
|
65
|
+
const windowHeight = window.innerHeight;
|
|
66
|
+
const documentHeight = document.documentElement.scrollHeight;
|
|
67
|
+
const scrollTop = window.scrollY;
|
|
68
|
+
const scrollPercent = scrollTop / (documentHeight - windowHeight) * 100;
|
|
69
|
+
const milestones = [25, 50, 75, 100];
|
|
70
|
+
for (const milestone of milestones) {
|
|
71
|
+
if (scrollPercent >= milestone && !scrollTracked.current.has(milestone)) {
|
|
72
|
+
scrollTracked.current.add(milestone);
|
|
73
|
+
trackScrollDepth(milestone, pathname || "/");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
78
|
+
handleScroll();
|
|
79
|
+
return () => {
|
|
80
|
+
window.removeEventListener("scroll", handleScroll);
|
|
81
|
+
};
|
|
82
|
+
}, [pathname]);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/lib/ab-test.ts
|
|
87
|
+
function getABTestVariant(config) {
|
|
88
|
+
if (typeof window === "undefined") {
|
|
89
|
+
return "A";
|
|
90
|
+
}
|
|
91
|
+
const storageKey = `ab-test-${config.testId}`;
|
|
92
|
+
const stored = localStorage.getItem(storageKey);
|
|
93
|
+
if (stored === "A" || stored === "B") {
|
|
94
|
+
return stored;
|
|
95
|
+
}
|
|
96
|
+
const totalWeight = config.variants.A.weight + config.variants.B.weight;
|
|
97
|
+
const random = Math.random() * totalWeight;
|
|
98
|
+
const variant = random < config.variants.A.weight ? "A" : "B";
|
|
99
|
+
localStorage.setItem(storageKey, variant);
|
|
100
|
+
return variant;
|
|
101
|
+
}
|
|
102
|
+
export {
|
|
103
|
+
AnalyticsTracker,
|
|
104
|
+
getABTestVariant,
|
|
105
|
+
trackABTestEvent
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=client.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/AnalyticsTracker.tsx","../src/lib/analytics.ts","../src/lib/ab-test.ts"],"sourcesContent":["// ABOUTME: Client-side analytics tracker for page views and scroll depth\n// ABOUTME: Automatically tracks page views and scroll milestones\n'use client';\n\nimport { useEffect, useRef } from 'react';\nimport { usePathname } from 'next/navigation';\nimport { trackPageView, trackScrollDepth } from '../lib/analytics';\n\n/**\n * AnalyticsTracker - Client-only component for tracking page views and scroll depth\n *\n * ⚠️ Edge Runtime Warning:\n * This component uses browser-only APIs (window, document) and Next.js client hooks (usePathname).\n * Do NOT import from main index in middleware or edge functions.\n * Use: import { AnalyticsTracker } from 'simple-site-framework/client'\n */\nexport function AnalyticsTracker() {\n // Edge-safe check: fail fast with clear error if used in wrong context\n if (typeof window === 'undefined' && typeof document === 'undefined') {\n if (process.env.NODE_ENV !== 'production') {\n console.error(\n 'AnalyticsTracker: This component requires browser APIs and cannot run in edge runtime or SSR. ' +\n 'Import from \"simple-site-framework/client\" and ensure it\\'s only used in client components.'\n );\n }\n return null;\n }\n\n const pathname = usePathname();\n const scrollTracked = useRef<Set<number>>(new Set());\n\n // Track page views on route change\n useEffect(() => {\n if (pathname) {\n trackPageView(pathname, document.title);\n // Reset scroll tracking for new page\n scrollTracked.current.clear();\n }\n }, [pathname]);\n\n // Track scroll depth\n useEffect(() => {\n const handleScroll = () => {\n const windowHeight = window.innerHeight;\n const documentHeight = document.documentElement.scrollHeight;\n const scrollTop = window.scrollY;\n const scrollPercent = (scrollTop / (documentHeight - windowHeight)) * 100;\n\n const milestones = [25, 50, 75, 100] as const;\n\n for (const milestone of milestones) {\n if (scrollPercent >= milestone && !scrollTracked.current.has(milestone)) {\n scrollTracked.current.add(milestone);\n trackScrollDepth(milestone, pathname || '/');\n }\n }\n };\n\n // Add scroll listener\n window.addEventListener('scroll', handleScroll, { passive: true });\n\n // Check immediately in case page loads already scrolled\n handleScroll();\n\n return () => {\n window.removeEventListener('scroll', handleScroll);\n };\n }, [pathname]);\n\n return null;\n}\n","// ABOUTME: Comprehensive analytics tracking utility for GA4 via GTM\n// ABOUTME: Provides type-safe event tracking for conversions, CTAs, forms, and user interactions\n\nimport type { AnalyticsEvent } from '../types/analytics';\n\ndeclare global {\n interface Window {\n dataLayer: unknown[];\n }\n}\n\n// Re-export types for convenience\nexport type * from '../types/analytics';\n\n/**\n * Push event to Google Tag Manager dataLayer\n */\nfunction pushToDataLayer(data: AnalyticsEvent) {\n if (typeof window === 'undefined') return;\n\n window.dataLayer = window.dataLayer || [];\n window.dataLayer.push(data);\n\n // Log in development\n if (process.env.NODE_ENV === 'development') {\n console.log('[Analytics]', data);\n }\n}\n\n/**\n * Generic event tracking function\n * Use this for custom events not covered by specific tracking functions\n *\n * @param eventName - Name of the event\n * @param properties - Additional event properties\n *\n * @example\n * ```typescript\n * trackEvent('button_click', {\n * button_text: 'Sign Up',\n * button_location: 'hero',\n * button_variant: 'primary'\n * });\n * ```\n */\nexport function trackEvent(eventName: string, properties?: Record<string, unknown>) {\n pushToDataLayer({\n event: eventName,\n ...properties,\n });\n}\n\n/**\n * Track CTA click events\n * @param ctaLocation - Where the CTA appears (e.g., 'hero', 'pricing', 'mobile_sticky')\n * @param ctaText - The text on the CTA button\n * @param ctaType - Type of CTA (e.g., 'signup', 'trial', 'contact')\n */\nexport function trackCTAClick(\n ctaLocation: string,\n ctaText: string,\n ctaType: 'signup' | 'trial' | 'contact' | 'download' | 'other' = 'other'\n) {\n pushToDataLayer({\n event: 'cta_click',\n event_category: 'cta',\n event_label: `${ctaLocation}_${ctaType}`,\n cta_location: ctaLocation,\n cta_text: ctaText,\n cta_type: ctaType,\n });\n}\n\n/**\n * Track form interactions\n * @param formName - Identifier for the form\n * @param action - Form action (start, submit, error, abandon)\n */\nexport function trackFormEvent(\n formName: string,\n action: 'start' | 'submit' | 'error' | 'abandon',\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: `form_${action}`,\n event_category: 'form',\n event_label: formName,\n form_name: formName,\n form_action: action,\n ...metadata,\n });\n}\n\n/**\n * Track page view (for SPAs and custom tracking)\n * @param pagePath - The page path\n * @param pageTitle - The page title\n */\nexport function trackPageView(pagePath: string, pageTitle: string) {\n pushToDataLayer({\n event: 'page_view',\n page_path: pagePath,\n page_title: pageTitle,\n });\n}\n\n/**\n * Track pricing page interactions\n * @param action - Type of interaction\n */\nexport function trackPricingEvent(\n action: 'view' | 'calculate' | 'plan_select' | 'currency_change',\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: 'pricing_interaction',\n event_category: 'engagement',\n event_label: action,\n pricing_action: action,\n ...metadata,\n });\n}\n\n/**\n * Track feature page engagement\n * @param featureName - Name of the feature being viewed\n * @param action - Type of engagement\n */\nexport function trackFeatureEngagement(\n featureName: string,\n action: 'view' | 'video_play' | 'video_complete' | 'cta_click',\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: 'feature_engagement',\n event_category: 'engagement',\n event_label: `${featureName}_${action}`,\n feature_name: featureName,\n feature_action: action,\n ...metadata,\n });\n}\n\n/**\n * Track resource downloads\n * @param resourceName - Name of the resource\n * @param resourceType - Type of resource (pdf, video, etc.)\n */\nexport function trackResourceDownload(\n resourceName: string,\n resourceType: string\n) {\n pushToDataLayer({\n event: 'resource_download',\n event_category: 'engagement',\n event_label: resourceName,\n resource_name: resourceName,\n resource_type: resourceType,\n });\n}\n\n/**\n * Track video interactions\n * @param videoTitle - Title of the video\n * @param action - Video action\n */\nexport function trackVideoEvent(\n videoTitle: string,\n action: 'play' | 'pause' | 'complete' | '25%' | '50%' | '75%',\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: 'video_interaction',\n event_category: 'engagement',\n event_label: `${videoTitle}_${action}`,\n video_title: videoTitle,\n video_action: action,\n ...metadata,\n });\n}\n\n/**\n * Track navigation events\n * @param linkText - Text of the link clicked\n * @param linkUrl - URL of the link\n * @param linkLocation - Where the link appears (header, footer, content)\n */\nexport function trackNavigation(\n linkText: string,\n linkUrl: string,\n linkLocation: 'header' | 'footer' | 'content' | 'mobile'\n) {\n pushToDataLayer({\n event: 'navigation_click',\n event_category: 'navigation',\n event_label: linkText,\n link_text: linkText,\n link_url: linkUrl,\n link_location: linkLocation,\n });\n}\n\n/**\n * Track A/B test variant assignment and events\n * @param testId - ID of the A/B test\n * @param variant - Variant assigned (A or B)\n * @param eventName - Name of the event\n */\nexport function trackABTestEvent(\n testId: string,\n variant: 'A' | 'B',\n eventName: string,\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: 'ab_test_event',\n event_category: 'ab_test',\n event_label: `${testId}_${variant}_${eventName}`,\n test_id: testId,\n variant: variant,\n test_event: eventName,\n ...metadata,\n });\n}\n\n/**\n * Track conversion events (trial signups, purchases, etc.)\n * @param conversionType - Type of conversion\n * @param value - Monetary value (optional)\n */\nexport function trackConversion(\n conversionType: 'trial_signup' | 'contact' | 'newsletter' | 'other',\n value?: number,\n metadata?: Record<string, unknown>\n) {\n pushToDataLayer({\n event: 'conversion',\n event_category: 'conversion',\n event_label: conversionType,\n conversion_type: conversionType,\n value: value,\n ...metadata,\n });\n}\n\n/**\n * Track scroll depth\n * @param percentage - Scroll depth percentage (25, 50, 75, 100)\n * @param pagePath - The page path\n */\nexport function trackScrollDepth(\n percentage: 25 | 50 | 75 | 100,\n pagePath: string\n) {\n pushToDataLayer({\n event: 'scroll_depth',\n event_category: 'engagement',\n event_label: `${percentage}%`,\n scroll_percentage: percentage,\n page_path: pagePath,\n });\n}\n\n/**\n * Track search events\n * @param searchTerm - The search term\n * @param resultCount - Number of results (optional)\n */\nexport function trackSearch(searchTerm: string, resultCount?: number) {\n pushToDataLayer({\n event: 'search',\n event_category: 'engagement',\n event_label: searchTerm,\n search_term: searchTerm,\n result_count: resultCount,\n });\n}\n\n/**\n * Track outbound link clicks\n * @param url - The external URL\n * @param linkText - Text of the link\n */\nexport function trackOutboundLink(url: string, linkText: string) {\n pushToDataLayer({\n event: 'outbound_link',\n event_category: 'navigation',\n event_label: url,\n outbound_url: url,\n link_text: linkText,\n });\n}\n\n/**\n * Track error events\n * @param errorType - Type of error\n * @param errorMessage - Error message or description\n */\nexport function trackError(errorType: string, errorMessage: string) {\n pushToDataLayer({\n event: 'error',\n event_category: 'engagement',\n event_label: errorType,\n error_type: errorType,\n error_message: errorMessage,\n });\n}\n","// ABOUTME: A/B testing utility for CTA variations\n// ABOUTME: Uses localStorage to persist user's assigned variant\n// ABOUTME: Integrated with analytics tracking\n\n'use client';\n\nimport { trackABTestEvent as analyticsTrackABTestEvent } from './analytics';\n\nexport type ABTestVariant = 'A' | 'B';\n\nexport interface ABTestConfig {\n testId: string;\n variants: {\n A: { weight: number };\n B: { weight: number };\n };\n}\n\n/**\n * Get the assigned variant for a test, or assign one if not already assigned\n * Uses localStorage to persist the assignment across sessions\n */\nexport function getABTestVariant(config: ABTestConfig): ABTestVariant {\n if (typeof window === 'undefined') {\n // Server-side: return default variant\n return 'A';\n }\n\n const storageKey = `ab-test-${config.testId}`;\n const stored = localStorage.getItem(storageKey);\n\n if (stored === 'A' || stored === 'B') {\n return stored;\n }\n\n // Assign new variant based on weights\n const totalWeight = config.variants.A.weight + config.variants.B.weight;\n const random = Math.random() * totalWeight;\n const variant: ABTestVariant = random < config.variants.A.weight ? 'A' : 'B';\n\n localStorage.setItem(storageKey, variant);\n return variant;\n}\n\n/**\n * Track A/B test event with integrated analytics\n * @deprecated Import trackABTestEvent directly from analytics instead\n */\nfunction trackABTestEvent(\n testId: string,\n variant: ABTestVariant,\n eventName: string,\n metadata?: Record<string, unknown>\n) {\n if (typeof window === 'undefined') return;\n\n // Track via analytics library\n analyticsTrackABTestEvent(testId, variant, eventName, metadata);\n}\n"],"mappings":";;;;AAIA,SAAS,WAAW,cAAc;AAClC,SAAS,mBAAmB;;;ACY5B,SAAS,gBAAgB,MAAsB;AAC7C,MAAI,OAAO,WAAW,YAAa;AAEnC,SAAO,YAAY,OAAO,aAAa,CAAC;AACxC,SAAO,UAAU,KAAK,IAAI;AAG1B,MAAI,QAAQ,IAAI,aAAa,eAAe;AAC1C,YAAQ,IAAI,eAAe,IAAI;AAAA,EACjC;AACF;AAuEO,SAAS,cAAc,UAAkB,WAAmB;AACjE,kBAAgB;AAAA,IACd,OAAO;AAAA,IACP,WAAW;AAAA,IACX,YAAY;AAAA,EACd,CAAC;AACH;AAwGO,SAAS,iBACd,QACA,SACA,WACA,UACA;AACA,kBAAgB;AAAA,IACd,OAAO;AAAA,IACP,gBAAgB;AAAA,IAChB,aAAa,GAAG,MAAM,IAAI,OAAO,IAAI,SAAS;AAAA,IAC9C,SAAS;AAAA,IACT;AAAA,IACA,YAAY;AAAA,IACZ,GAAG;AAAA,EACL,CAAC;AACH;AA2BO,SAAS,iBACd,YACA,UACA;AACA,kBAAgB;AAAA,IACd,OAAO;AAAA,IACP,gBAAgB;AAAA,IAChB,aAAa,GAAG,UAAU;AAAA,IAC1B,mBAAmB;AAAA,IACnB,WAAW;AAAA,EACb,CAAC;AACH;;;ADrPO,SAAS,mBAAmB;AAEjC,MAAI,OAAO,WAAW,eAAe,OAAO,aAAa,aAAa;AACpE,QAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,cAAQ;AAAA,QACN;AAAA,MAEF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,YAAY;AAC7B,QAAM,gBAAgB,OAAoB,oBAAI,IAAI,CAAC;AAGnD,YAAU,MAAM;AACd,QAAI,UAAU;AACZ,oBAAc,UAAU,SAAS,KAAK;AAEtC,oBAAc,QAAQ,MAAM;AAAA,IAC9B;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAGb,YAAU,MAAM;AACd,UAAM,eAAe,MAAM;AACzB,YAAM,eAAe,OAAO;AAC5B,YAAM,iBAAiB,SAAS,gBAAgB;AAChD,YAAM,YAAY,OAAO;AACzB,YAAM,gBAAiB,aAAa,iBAAiB,gBAAiB;AAEtE,YAAM,aAAa,CAAC,IAAI,IAAI,IAAI,GAAG;AAEnC,iBAAW,aAAa,YAAY;AAClC,YAAI,iBAAiB,aAAa,CAAC,cAAc,QAAQ,IAAI,SAAS,GAAG;AACvE,wBAAc,QAAQ,IAAI,SAAS;AACnC,2BAAiB,WAAW,YAAY,GAAG;AAAA,QAC7C;AAAA,MACF;AAAA,IACF;AAGA,WAAO,iBAAiB,UAAU,cAAc,EAAE,SAAS,KAAK,CAAC;AAGjE,iBAAa;AAEb,WAAO,MAAM;AACX,aAAO,oBAAoB,UAAU,YAAY;AAAA,IACnD;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAEb,SAAO;AACT;;;AEhDO,SAAS,iBAAiB,QAAqC;AACpE,MAAI,OAAO,WAAW,aAAa;AAEjC,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,WAAW,OAAO,MAAM;AAC3C,QAAM,SAAS,aAAa,QAAQ,UAAU;AAE9C,MAAI,WAAW,OAAO,WAAW,KAAK;AACpC,WAAO;AAAA,EACT;AAGA,QAAM,cAAc,OAAO,SAAS,EAAE,SAAS,OAAO,SAAS,EAAE;AACjE,QAAM,SAAS,KAAK,OAAO,IAAI;AAC/B,QAAM,UAAyB,SAAS,OAAO,SAAS,EAAE,SAAS,MAAM;AAEzE,eAAa,QAAQ,YAAY,OAAO;AACxC,SAAO;AACT;","names":[]}
|