@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 +69 -8
- package/package.json +6 -5
- package/src/auth/context/AuthContext.tsx +58 -9
- package/src/index.ts +1 -1
- package/src/layouts/AppLayout/AppLayout.tsx +17 -14
- package/src/layouts/AppLayout/components/ErrorBoundary.tsx +9 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +3 -3
- package/src/layouts/AppLayout/types/config.ts +6 -0
- package/src/snippets/Analytics/AnalyticsProvider.tsx +29 -0
- package/src/snippets/Analytics/events.ts +55 -0
- package/src/snippets/Analytics/index.ts +10 -0
- package/src/snippets/Analytics/useAnalytics.ts +150 -0
- package/src/snippets/ContactForm/ContactPage.tsx +4 -11
- package/src/snippets/index.ts +1 -0
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.
|
|
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.
|
|
89
|
-
"@djangocfg/og-image": "^1.4.
|
|
90
|
-
"@djangocfg/ui": "^1.4.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
setInitialized(true);
|
|
426
|
-
setIsLoading(false);
|
|
427
|
-
pushToDefaultAuthCallbackUrl();
|
|
476
|
+
performLogout();
|
|
428
477
|
}
|
|
429
478
|
}
|
|
430
479
|
}, [accounts, pushToDefaultAuthCallbackUrl]);
|
package/src/index.ts
CHANGED
|
@@ -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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
+
{/* Update Notifier */}
|
|
314
|
+
<UpdateNotifier enabled={showUpdateNotifier} currentVersion={packageJson.version} />
|
|
313
315
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
+
{/* Loading Progress Bar */}
|
|
317
|
+
<PageProgress />
|
|
316
318
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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="
|
|
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
|
-
{
|
|
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}
|
package/src/snippets/index.ts
CHANGED