@djangocfg/layouts 1.4.21 → 1.4.23

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @djangocfg/layouts
2
2
 
3
- Pre-built layouts, auth, and snippets for Next.js + Tailwind CSS.
3
+ Pre-built layouts, auth, analytics, and snippets for Next.js + Tailwind CSS.
4
4
 
5
5
  **Part of [DjangoCFG](https://djangocfg.com)** — modern Django framework for production-ready SaaS applications.
6
6
 
@@ -45,6 +45,61 @@ import { AuthDialog } from '@djangocfg/layouts/snippets';
45
45
  | `useAuthGuard` | Route protection hook |
46
46
  | `authMiddleware` | Next.js middleware |
47
47
 
48
+ ## Analytics
49
+
50
+ Google Analytics integration via `react-ga4`. Auto-tracks pageviews and user sessions.
51
+
52
+ ### Setup
53
+
54
+ Add tracking ID to your config:
55
+
56
+ ```tsx
57
+ // appLayoutConfig.ts
58
+ export const appLayoutConfig: AppLayoutConfig = {
59
+ // ...
60
+ analytics: {
61
+ googleTrackingId: 'G-XXXXXXXXXX',
62
+ },
63
+ };
64
+ ```
65
+
66
+ Analytics is automatically initialized by `AppLayout`. Works only in production (`NODE_ENV === 'production'`).
67
+
68
+ ### Usage
69
+
70
+ ```tsx
71
+ import { useAnalytics, Analytics, AnalyticsEvent, AnalyticsCategory } from '@djangocfg/layouts';
72
+
73
+ // In React components - auto-tracks pageviews
74
+ const { event, isEnabled } = useAnalytics();
75
+
76
+ event(AnalyticsEvent.THEME_CHANGE, {
77
+ category: AnalyticsCategory.ENGAGEMENT,
78
+ label: 'dark',
79
+ });
80
+
81
+ // Outside React (utilities, handlers)
82
+ Analytics.event('button_click', { category: 'engagement', label: 'signup' });
83
+ Analytics.setUser('user-123');
84
+ ```
85
+
86
+ ### Predefined Events
87
+
88
+ | Category | Events |
89
+ |----------|--------|
90
+ | **Auth** | `AUTH_OTP_REQUEST`, `AUTH_LOGIN_SUCCESS`, `AUTH_OTP_VERIFY_FAIL`, `AUTH_LOGOUT`, `AUTH_SESSION_EXPIRED`, `AUTH_TOKEN_REFRESH` |
91
+ | **Error** | `ERROR_BOUNDARY`, `ERROR_API`, `ERROR_VALIDATION`, `ERROR_NETWORK` |
92
+ | **Navigation** | `NAV_ADMIN_ENTER`, `NAV_DASHBOARD_ENTER`, `NAV_PAGE_VIEW` |
93
+ | **Engagement** | `THEME_CHANGE`, `SIDEBAR_TOGGLE`, `MOBILE_MENU_OPEN` |
94
+
95
+ ### Auto-tracking
96
+
97
+ Built-in tracking for:
98
+ - **Page views** - on every route change
99
+ - **User ID** - automatically set when user is authenticated
100
+ - **Auth events** - login, logout, OTP, session expiry
101
+ - **Errors** - React ErrorBoundary errors
102
+
48
103
  ## Snippets
49
104
 
50
105
  ```tsx
@@ -60,6 +115,7 @@ import { ContactPage, VideoPlayer, Breadcrumbs } from '@djangocfg/layouts/snippe
60
115
  | `VideoPlayer` | Vidstack video player |
61
116
  | `Breadcrumbs` | Navigation breadcrumbs |
62
117
  | `Chat` | Chat widget |
118
+ | `AnalyticsProvider` | Analytics wrapper component |
63
119
 
64
120
  ### ContactPage
65
121
 
@@ -75,25 +131,29 @@ import { ContactPage, VideoPlayer, Breadcrumbs } from '@djangocfg/layouts/snippe
75
131
  />
76
132
  ```
77
133
 
78
- **Defaults:**
79
- - API: `http://localhost:8000` (dev) / `https://api.reforms.ai` (prod)
80
- - Email: `markolofsen@gmail.com`
81
- - Calendly: `https://calendly.com/markolofsen/meeting`
82
-
83
134
  **Features:**
84
135
  - localStorage draft saving
85
136
  - Success state with icon
86
137
  - Zod validation from `@djangocfg/api`
87
138
 
139
+ ## Validation
140
+
141
+ Error tracking and validation utilities.
142
+
143
+ ```tsx
144
+ import { ValidationErrorConfig, CORSErrorConfig, NetworkErrorConfig } from '@djangocfg/layouts';
145
+ ```
146
+
88
147
  ## Exports
89
148
 
90
149
  | Path | Content |
91
150
  |------|---------|
92
- | `@djangocfg/layouts` | Main exports |
151
+ | `@djangocfg/layouts` | Main exports (all modules) |
93
152
  | `@djangocfg/layouts/layouts` | Layout components |
94
153
  | `@djangocfg/layouts/auth` | Auth context & hooks |
95
- | `@djangocfg/layouts/snippets` | Reusable components |
154
+ | `@djangocfg/layouts/snippets` | Reusable components + Analytics |
96
155
  | `@djangocfg/layouts/utils` | Utilities |
156
+ | `@djangocfg/layouts/types` | TypeScript types |
97
157
  | `@djangocfg/layouts/styles` | CSS |
98
158
 
99
159
  ## Requirements
@@ -101,3 +161,4 @@ import { ContactPage, VideoPlayer, Breadcrumbs } from '@djangocfg/layouts/snippe
101
161
  - Next.js >= 15
102
162
  - React >= 19
103
163
  - Tailwind CSS >= 4
164
+ - react-ga4 (bundled)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.4.21",
3
+ "version": "1.4.23",
4
4
  "description": "Pre-built dashboard layouts, authentication pages, and admin templates for Next.js applications with Tailwind CSS",
5
5
  "keywords": [
6
6
  "layouts",
@@ -85,9 +85,9 @@
85
85
  "check": "tsc --noEmit"
86
86
  },
87
87
  "peerDependencies": {
88
- "@djangocfg/api": "^1.4.21",
89
- "@djangocfg/og-image": "^1.4.21",
90
- "@djangocfg/ui": "^1.4.21",
88
+ "@djangocfg/api": "^1.4.23",
89
+ "@djangocfg/og-image": "^1.4.23",
90
+ "@djangocfg/ui": "^1.4.23",
91
91
  "@hookform/resolvers": "^5.2.0",
92
92
  "consola": "^3.4.2",
93
93
  "lucide-react": "^0.468.0",
@@ -105,10 +105,11 @@
105
105
  "dependencies": {
106
106
  "@vidstack/react": "^0.6.15",
107
107
  "maverick.js": "0.37.0",
108
+ "react-ga4": "^2.1.0",
108
109
  "vidstack": "0.6.15"
109
110
  },
110
111
  "devDependencies": {
111
- "@djangocfg/typescript-config": "^1.4.21",
112
+ "@djangocfg/typescript-config": "^1.4.23",
112
113
  "@types/node": "^24.7.2",
113
114
  "@types/react": "19.2.2",
114
115
  "@types/react-dom": "19.2.1",
@@ -9,6 +9,7 @@ import { useLocalStorage } from '@djangocfg/ui/hooks';
9
9
  import { getCachedProfile, clearProfileCache } from '../hooks/useProfileCache';
10
10
 
11
11
  import { authLogger } from '../../utils/logger';
12
+ import { Analytics, AnalyticsEvent, AnalyticsCategory } from '../../snippets/Analytics';
12
13
  import type { AuthConfig, AuthContextType, AuthProviderProps, UserProfile } from './types';
13
14
 
14
15
  // Default routes
@@ -287,6 +288,13 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
287
288
  });
288
289
 
289
290
  const channelName = channel === 'phone' ? 'phone number' : 'email address';
291
+
292
+ // Track OTP request
293
+ Analytics.event(AnalyticsEvent.AUTH_OTP_REQUEST, {
294
+ category: AnalyticsCategory.AUTH,
295
+ label: channel || 'email',
296
+ });
297
+
290
298
  return {
291
299
  success: true,
292
300
  message: result.message || `OTP code sent to your ${channelName}`,
@@ -336,6 +344,17 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
336
344
  // Small delay to ensure profile state is updated
337
345
  await new Promise(resolve => setTimeout(resolve, 200));
338
346
 
347
+ // Track successful login
348
+ Analytics.event(AnalyticsEvent.AUTH_LOGIN_SUCCESS, {
349
+ category: AnalyticsCategory.AUTH,
350
+ label: channel || 'email',
351
+ });
352
+
353
+ // Set user ID for future tracking
354
+ if (result.user?.id) {
355
+ Analytics.setUser(String(result.user.id));
356
+ }
357
+
339
358
  // Handle redirect logic here
340
359
  const defaultCallback = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
341
360
 
@@ -353,6 +372,13 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
353
372
  };
354
373
  } catch (error) {
355
374
  authLogger.error('Verify OTP error:', error);
375
+
376
+ // Track failed verification
377
+ Analytics.event(AnalyticsEvent.AUTH_OTP_VERIFY_FAIL, {
378
+ category: AnalyticsCategory.AUTH,
379
+ label: channel || 'email',
380
+ });
381
+
356
382
  return {
357
383
  success: false,
358
384
  message: 'Failed to verify OTP',
@@ -367,6 +393,12 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
367
393
  const refreshTokenValue = api.getRefreshToken();
368
394
  if (!refreshTokenValue) {
369
395
  clearAuthState('refreshToken:noToken');
396
+
397
+ // Track session expired
398
+ Analytics.event(AnalyticsEvent.AUTH_SESSION_EXPIRED, {
399
+ category: AnalyticsCategory.AUTH,
400
+ });
401
+
370
402
  return {
371
403
  success: false,
372
404
  message: 'No refresh token available',
@@ -374,7 +406,12 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
374
406
  }
375
407
 
376
408
  await accounts.refreshToken(refreshTokenValue);
377
-
409
+
410
+ // Track successful refresh
411
+ Analytics.event(AnalyticsEvent.AUTH_TOKEN_REFRESH, {
412
+ category: AnalyticsCategory.AUTH,
413
+ });
414
+
378
415
  return {
379
416
  success: true,
380
417
  message: 'Token refreshed',
@@ -382,6 +419,12 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
382
419
  } catch (error) {
383
420
  authLogger.error('Refresh token error:', error);
384
421
  clearAuthState('refreshToken:error');
422
+
423
+ // Track refresh failure
424
+ Analytics.event(AnalyticsEvent.AUTH_TOKEN_REFRESH_FAIL, {
425
+ category: AnalyticsCategory.AUTH,
426
+ });
427
+
385
428
  return {
386
429
  success: false,
387
430
  message: 'Error refreshing token',
@@ -402,6 +445,18 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
402
445
  }, [setRedirectUrl]);
403
446
 
404
447
  const logout = useCallback(async (): Promise<void> => {
448
+ const performLogout = () => {
449
+ // Track logout
450
+ Analytics.event(AnalyticsEvent.AUTH_LOGOUT, {
451
+ category: AnalyticsCategory.AUTH,
452
+ });
453
+
454
+ accounts.logout(); // Clear tokens and profile
455
+ setInitialized(true);
456
+ setIsLoading(false);
457
+ pushToDefaultAuthCallbackUrl();
458
+ };
459
+
405
460
  // Use config.onConfirm if provided, otherwise use a simple confirm
406
461
  if (configRef.current?.onConfirm) {
407
462
  const { confirmed } = await configRef.current.onConfirm({
@@ -412,19 +467,13 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
412
467
  color: 'error',
413
468
  });
414
469
  if (confirmed) {
415
- accounts.logout(); // Clear tokens and profile
416
- setInitialized(true);
417
- setIsLoading(false);
418
- pushToDefaultAuthCallbackUrl();
470
+ performLogout();
419
471
  }
420
472
  } else {
421
473
  // Fallback to browser confirm
422
474
  const confirmed = window.confirm('Are you sure you want to logout?');
423
475
  if (confirmed) {
424
- accounts.logout(); // Clear tokens and profile
425
- setInitialized(true);
426
- setIsLoading(false);
427
- pushToDefaultAuthCallbackUrl();
476
+ performLogout();
428
477
  }
429
478
  }
430
479
  }, [accounts, pushToDefaultAuthCallbackUrl]);
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@ export * from './layouts';
13
13
  // Types
14
14
  export * from './types';
15
15
 
16
- // Snippets - Reusable UI components
16
+ // Snippets - Reusable UI components (includes Analytics)
17
17
  export * from './snippets';
18
18
 
19
19
  // Validation error tracking
@@ -30,6 +30,7 @@ import dynamic from 'next/dynamic';
30
30
  import { AppContextProvider } from './context';
31
31
  import { CoreProviders } from './providers';
32
32
  import { Seo, PageProgress, ErrorBoundary, UpdateNotifier } from './components';
33
+ import { AnalyticsProvider } from '../../snippets/Analytics';
33
34
  import { PublicLayout } from './layouts/PublicLayout';
34
35
  import { PrivateLayout } from './layouts/PrivateLayout';
35
36
  import { AuthLayout } from './layouts/AuthLayout';
@@ -301,23 +302,25 @@ export function AppLayout({ children, config, component, pageProps, fontFamily,
301
302
 
302
303
  const appContent = (
303
304
  <AppContextProvider config={config} showUpdateNotifier={showUpdateNotifier}>
304
- {/* SEO Meta Tags */}
305
- <Seo
306
- pageConfig={finalPageConfig}
307
- icons={config.app.icons}
308
- siteUrl={config.app.siteUrl}
309
- />
305
+ <AnalyticsProvider>
306
+ {/* SEO Meta Tags */}
307
+ <Seo
308
+ pageConfig={finalPageConfig}
309
+ icons={config.app.icons}
310
+ siteUrl={config.app.siteUrl}
311
+ />
310
312
 
311
- {/* Update Notifier */}
312
- <UpdateNotifier enabled={showUpdateNotifier} currentVersion={packageJson.version} />
313
+ {/* Update Notifier */}
314
+ <UpdateNotifier enabled={showUpdateNotifier} currentVersion={packageJson.version} />
313
315
 
314
- {/* Loading Progress Bar */}
315
- <PageProgress />
316
+ {/* Loading Progress Bar */}
317
+ <PageProgress />
316
318
 
317
- {/* Smart Layout Router */}
318
- <LayoutRouter component={component} config={config}>
319
- {children}
320
- </LayoutRouter>
319
+ {/* Smart Layout Router */}
320
+ <LayoutRouter component={component} config={config}>
321
+ {children}
322
+ </LayoutRouter>
323
+ </AnalyticsProvider>
321
324
  </AppContextProvider>
322
325
  );
323
326
 
@@ -12,6 +12,7 @@ import React, { Component, ReactNode } from 'react';
12
12
  import { ErrorLayout } from '../../ErrorLayout';
13
13
  import { Bug } from 'lucide-react';
14
14
  import logger from '../../../utils/logger';
15
+ import { Analytics, AnalyticsEvent, AnalyticsCategory } from '../../../snippets/Analytics';
15
16
 
16
17
  interface ErrorBoundaryProps {
17
18
  children: ReactNode;
@@ -50,6 +51,14 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
50
51
  logger.error('ErrorBoundary caught error:', error);
51
52
  logger.error('Error info:', errorInfo);
52
53
 
54
+ // Track error in analytics
55
+ Analytics.event(AnalyticsEvent.ERROR_BOUNDARY, {
56
+ category: AnalyticsCategory.ERROR,
57
+ label: error.message,
58
+ error_name: error.name,
59
+ component_stack: errorInfo.componentStack?.slice(0, 500), // Limit size
60
+ });
61
+
53
62
  // Call optional callback
54
63
  this.props.onError?.(error, errorInfo);
55
64
 
@@ -147,10 +147,10 @@ export function Footer() {
147
147
  )}
148
148
  </div>
149
149
 
150
- {/* Right Column - Footer Menu Sections */}
151
- <div className="grid grid-cols-2 md:grid-cols-4 gap-8 flex-1">
150
+ {/* Right Column - Footer Menu Sections (max 4 columns, rest wraps) */}
151
+ <div className="flex flex-wrap gap-8 flex-1">
152
152
  {footer.menuSections.map((section) => (
153
- <div key={section.title}>
153
+ <div key={section.title} className="flex-1 basis-[calc(25%-1.5rem)] min-w-[140px] max-w-[200px]">
154
154
  <h3 className="text-sm font-semibold text-foreground mb-3">
155
155
  {section.title}
156
156
  </h3>
@@ -70,4 +70,10 @@ export interface AppLayoutConfig {
70
70
  /** Enable phone authentication (default: false) */
71
71
  enablePhoneAuth?: boolean;
72
72
  };
73
+
74
+ /** Analytics configuration */
75
+ analytics?: {
76
+ /** Google Analytics Tracking ID (e.g., 'G-XXXXXXXXXX' or 'UA-XXXXXXXXX-X') */
77
+ googleTrackingId?: string;
78
+ };
73
79
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * AnalyticsProvider Component
3
+ *
4
+ * Initializes Google Analytics and auto-tracks pageviews.
5
+ * Must be placed inside AppContextProvider and AuthProvider.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import { ReactNode } from 'react';
11
+ import { useAnalytics } from './useAnalytics';
12
+
13
+ interface AnalyticsProviderProps {
14
+ children: ReactNode;
15
+ }
16
+
17
+ /**
18
+ * Analytics Provider that initializes tracking
19
+ * Automatically:
20
+ * - Initializes GA4 with tracking ID from config
21
+ * - Sets user ID when authenticated
22
+ * - Tracks page views on route changes
23
+ */
24
+ export function AnalyticsProvider({ children }: AnalyticsProviderProps) {
25
+ useAnalytics(); // Initialize and auto-track
26
+ return <>{children}</>;
27
+ }
28
+
29
+ export default AnalyticsProvider;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Analytics Events Constants
3
+ *
4
+ * Predefined event names and categories for consistent tracking
5
+ * across the entire application.
6
+ */
7
+
8
+ /**
9
+ * Event Categories
10
+ */
11
+ export const AnalyticsCategory = {
12
+ AUTH: 'auth',
13
+ ERROR: 'error',
14
+ NAVIGATION: 'navigation',
15
+ ENGAGEMENT: 'engagement',
16
+ USER: 'user',
17
+ } as const;
18
+
19
+ /**
20
+ * Predefined Event Names
21
+ */
22
+ export const AnalyticsEvent = {
23
+ // Auth Events
24
+ AUTH_OTP_REQUEST: 'auth_otp_request',
25
+ AUTH_OTP_VERIFY_SUCCESS: 'auth_otp_verify_success',
26
+ AUTH_OTP_VERIFY_FAIL: 'auth_otp_verify_fail',
27
+ AUTH_LOGIN_SUCCESS: 'auth_login_success',
28
+ AUTH_LOGOUT: 'auth_logout',
29
+ AUTH_SESSION_EXPIRED: 'auth_session_expired',
30
+ AUTH_TOKEN_REFRESH: 'auth_token_refresh',
31
+ AUTH_TOKEN_REFRESH_FAIL: 'auth_token_refresh_fail',
32
+
33
+ // Error Events
34
+ ERROR_BOUNDARY: 'error_boundary',
35
+ ERROR_API: 'error_api',
36
+ ERROR_VALIDATION: 'error_validation',
37
+ ERROR_NETWORK: 'error_network',
38
+
39
+ // Navigation Events
40
+ NAV_ADMIN_ENTER: 'nav_admin_enter',
41
+ NAV_DASHBOARD_ENTER: 'nav_dashboard_enter',
42
+ NAV_PAGE_VIEW: 'nav_page_view',
43
+
44
+ // Engagement Events
45
+ THEME_CHANGE: 'theme_change',
46
+ SIDEBAR_TOGGLE: 'sidebar_toggle',
47
+ MOBILE_MENU_OPEN: 'mobile_menu_open',
48
+
49
+ // User Events
50
+ USER_PROFILE_VIEW: 'user_profile_view',
51
+ USER_PROFILE_UPDATE: 'user_profile_update',
52
+ } as const;
53
+
54
+ export type AnalyticsCategoryType = typeof AnalyticsCategory[keyof typeof AnalyticsCategory];
55
+ export type AnalyticsEventType = typeof AnalyticsEvent[keyof typeof AnalyticsEvent];
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Analytics Module
3
+ *
4
+ * Google Analytics integration with react-ga4
5
+ */
6
+
7
+ export { useAnalytics, Analytics } from './useAnalytics';
8
+ export { AnalyticsProvider } from './AnalyticsProvider';
9
+ export { AnalyticsEvent, AnalyticsCategory } from './events';
10
+ export type { AnalyticsEventType, AnalyticsCategoryType } from './events';
@@ -0,0 +1,150 @@
1
+ /**
2
+ * useAnalytics Hook
3
+ *
4
+ * Provides Google Analytics tracking via react-ga4
5
+ * Automatically tracks page views on route changes
6
+ * Only works in production mode
7
+ */
8
+
9
+ 'use client';
10
+
11
+ import { useEffect } from 'react';
12
+ import { useRouter } from 'next/router';
13
+ import ReactGA from 'react-ga4';
14
+ import { useAppContext } from '../../layouts/AppLayout/context';
15
+ import { useAuth } from '../../auth';
16
+
17
+ // Check if we're in production
18
+ const isProduction = process.env.NODE_ENV === 'production';
19
+
20
+ // Tracking state
21
+ let isInitialized = false;
22
+ let currentTrackingId: string | undefined;
23
+
24
+ /**
25
+ * Analytics utility object for standalone usage (outside React components)
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { Analytics } from '@djangocfg/layouts';
30
+ *
31
+ * // In an event handler or utility function
32
+ * Analytics.event('button_click', { category: 'engagement', label: 'signup' });
33
+ * ```
34
+ */
35
+ export const Analytics = {
36
+ /**
37
+ * Initialize Google Analytics (called automatically by useAnalytics hook)
38
+ */
39
+ init: (trackingId: string) => {
40
+ if (!isProduction || !trackingId || isInitialized) return;
41
+ ReactGA.initialize(trackingId);
42
+ isInitialized = true;
43
+ currentTrackingId = trackingId;
44
+ },
45
+
46
+ /**
47
+ * Check if Analytics is enabled and initialized
48
+ */
49
+ isEnabled: () => isProduction && isInitialized,
50
+
51
+ /**
52
+ * Track a page view
53
+ */
54
+ pageview: (path: string) => {
55
+ if (!Analytics.isEnabled()) return;
56
+ ReactGA.send({ hitType: 'pageview', page: path });
57
+ },
58
+
59
+ /**
60
+ * Track a custom event
61
+ * @param name - Event name (action)
62
+ * @param params - Optional event parameters
63
+ */
64
+ event: (name: string, params: Record<string, any> = {}) => {
65
+ if (!Analytics.isEnabled()) return;
66
+ ReactGA.event(name, params);
67
+ },
68
+
69
+ /**
70
+ * Set user ID for tracking
71
+ */
72
+ setUser: (userId: string) => {
73
+ if (!Analytics.isEnabled()) return;
74
+ ReactGA.set({ user_id: userId });
75
+ },
76
+
77
+ /**
78
+ * Set custom dimensions/metrics
79
+ */
80
+ set: (fieldsObject: Record<string, any>) => {
81
+ if (!Analytics.isEnabled()) return;
82
+ ReactGA.set(fieldsObject);
83
+ },
84
+ };
85
+
86
+ /**
87
+ * Hook for Google Analytics tracking via react-ga4
88
+ *
89
+ * Automatically initializes GA and tracks page views on route changes
90
+ * Only works in production mode (NODE_ENV === 'production')
91
+ *
92
+ * @example
93
+ * ```tsx
94
+ * // Just call the hook - it auto-tracks pageviews
95
+ * useAnalytics();
96
+ *
97
+ * // Or use the returned methods for custom tracking
98
+ * const { event, isEnabled } = useAnalytics();
99
+ * event('button_click', { category: 'engagement', label: 'signup' });
100
+ * ```
101
+ */
102
+ export function useAnalytics() {
103
+ const router = useRouter();
104
+ const { config } = useAppContext();
105
+ const { user, isAuthenticated } = useAuth();
106
+
107
+ const trackingId = config.analytics?.googleTrackingId;
108
+ const isEnabled = isProduction && Boolean(trackingId);
109
+
110
+ // Initialize GA4
111
+ useEffect(() => {
112
+ if (!isEnabled || !trackingId) return;
113
+ Analytics.init(trackingId);
114
+ }, [isEnabled, trackingId]);
115
+
116
+ // Auto-set user ID when authenticated
117
+ useEffect(() => {
118
+ if (!isEnabled || !isAuthenticated || !user?.id) return;
119
+ Analytics.setUser(String(user.id));
120
+ }, [isEnabled, isAuthenticated, user?.id]);
121
+
122
+ // Auto-track page views on route change
123
+ useEffect(() => {
124
+ if (!isEnabled) return;
125
+
126
+ // Track initial page view
127
+ Analytics.pageview(window.location.pathname + window.location.search);
128
+
129
+ // Track on route change
130
+ const handleRouteChange = (url: string) => {
131
+ Analytics.pageview(url);
132
+ };
133
+
134
+ router.events.on('routeChangeComplete', handleRouteChange);
135
+ return () => {
136
+ router.events.off('routeChangeComplete', handleRouteChange);
137
+ };
138
+ }, [router.events, isEnabled]);
139
+
140
+ return {
141
+ isEnabled,
142
+ trackingId,
143
+ pageview: Analytics.pageview,
144
+ event: Analytics.event,
145
+ setUser: Analytics.setUser,
146
+ set: Analytics.set,
147
+ };
148
+ }
149
+
150
+ export default useAnalytics;
@@ -98,27 +98,20 @@ export function ContactPage({
98
98
  return (
99
99
  <div className={className}>
100
100
  {/* Header */}
101
- <div className="text-center mb-12">
102
- <h1 className="text-4xl md:text-5xl font-bold mb-4">
103
- {typeof title === 'string' ? (
104
- title
105
- ) : (
106
- title
107
- )}
101
+ <div className="text-center mb-8 md:mb-12">
102
+ <h1 className="text-3xl sm:text-4xl md:text-5xl font-bold mb-4">
103
+ {title}
108
104
  </h1>
109
- <p className="text-lg text-muted-foreground max-w-2xl mx-auto">
105
+ <p className="text-base sm:text-lg text-muted-foreground max-w-2xl mx-auto px-4">
110
106
  {subtitle}
111
107
  </p>
112
108
  </div>
113
109
 
114
110
  {/* Content Grid */}
115
111
  <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
116
- {/* Contact Form */}
117
112
  <div className="lg:col-span-2">
118
113
  <ContactForm apiUrl={apiUrl} onSuccess={onSuccess} />
119
114
  </div>
120
-
121
- {/* Contact Info */}
122
115
  <div>
123
116
  <ContactInfo
124
117
  details={contactDetails}
@@ -9,3 +9,4 @@ export * from './Breadcrumbs';
9
9
  export * from './AuthDialog';
10
10
  export * from './VideoPlayer';
11
11
  export * from './ContactForm';
12
+ export * from './Analytics';