@djangocfg/layouts 2.1.470 → 2.1.472
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.472",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -89,11 +89,12 @@
|
|
|
89
89
|
"check": "tsc --noEmit"
|
|
90
90
|
},
|
|
91
91
|
"peerDependencies": {
|
|
92
|
-
"@djangocfg/
|
|
93
|
-
"@djangocfg/
|
|
94
|
-
"@djangocfg/
|
|
95
|
-
"@djangocfg/
|
|
96
|
-
"@djangocfg/
|
|
92
|
+
"@djangocfg/analytics": "workspace:*",
|
|
93
|
+
"@djangocfg/api": "^2.1.472",
|
|
94
|
+
"@djangocfg/centrifugo": "^2.1.472",
|
|
95
|
+
"@djangocfg/devtools": "^2.1.472",
|
|
96
|
+
"@djangocfg/i18n": "^2.1.472",
|
|
97
|
+
"@djangocfg/ui-core": "^2.1.472",
|
|
97
98
|
"@hookform/resolvers": "^5.2.2",
|
|
98
99
|
"consola": "^3.4.2",
|
|
99
100
|
"lucide-react": "^0.545.0",
|
|
@@ -120,17 +121,17 @@
|
|
|
120
121
|
"libphonenumber-js": "^1.13.8",
|
|
121
122
|
"nextjs-toploader": "^3.9.17",
|
|
122
123
|
"qrcode.react": "^4.2.0",
|
|
123
|
-
"react-ga4": "^2.1.0",
|
|
124
124
|
"uuid": "^11.1.1"
|
|
125
125
|
},
|
|
126
126
|
"devDependencies": {
|
|
127
|
-
"@djangocfg/
|
|
128
|
-
"@djangocfg/
|
|
129
|
-
"@djangocfg/
|
|
130
|
-
"@djangocfg/
|
|
131
|
-
"@djangocfg/
|
|
132
|
-
"@djangocfg/
|
|
133
|
-
"@djangocfg/ui-
|
|
127
|
+
"@djangocfg/analytics": "workspace:*",
|
|
128
|
+
"@djangocfg/api": "^2.1.472",
|
|
129
|
+
"@djangocfg/centrifugo": "^2.1.472",
|
|
130
|
+
"@djangocfg/devtools": "^2.1.472",
|
|
131
|
+
"@djangocfg/i18n": "^2.1.472",
|
|
132
|
+
"@djangocfg/typescript-config": "^2.1.472",
|
|
133
|
+
"@djangocfg/ui-core": "^2.1.472",
|
|
134
|
+
"@djangocfg/ui-tools": "^2.1.472",
|
|
134
135
|
"@types/node": "^25.9.5",
|
|
135
136
|
"@types/react": "19.2.15",
|
|
136
137
|
"@types/react-dom": "19.2.3",
|
|
@@ -1,34 +1,120 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* The layouts adapter for `@djangocfg/analytics`.
|
|
5
|
+
*
|
|
6
|
+
* BaseApp mounts this inside AuthProvider, which is what lets it attach the
|
|
7
|
+
* signed-in user's id — the thing hosted analytics (Plausible, Umami, GA4
|
|
8
|
+
* without extra plumbing) structurally cannot do.
|
|
3
9
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
10
|
+
* Transports are composed here:
|
|
11
|
+
* - `djangocfg` — always on. Same-origin, self-hosted, no third party.
|
|
12
|
+
* - `ga4` — only when a tracking id is configured.
|
|
13
|
+
*
|
|
14
|
+
* Note the behavior change from the old implementation: that one hard-gated on
|
|
15
|
+
* `isProd`, so it was completely silent in development and you could not tell
|
|
16
|
+
* whether tracking worked until you shipped. Self-hosting removes the reason for
|
|
17
|
+
* that gate — your dev traffic goes to your own database.
|
|
6
18
|
*/
|
|
7
19
|
|
|
8
|
-
|
|
20
|
+
import { ReactNode, useEffect, useMemo } from 'react';
|
|
9
21
|
|
|
10
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
AnalyticsProvider as BaseAnalyticsProvider,
|
|
24
|
+
PageviewTracker,
|
|
25
|
+
djangocfg,
|
|
26
|
+
ga4,
|
|
27
|
+
useAnalyticsClient,
|
|
28
|
+
type AnalyticsTransport,
|
|
29
|
+
} from '@djangocfg/analytics';
|
|
30
|
+
import { setAnalyticsSink, useAuth } from '@djangocfg/api/auth';
|
|
31
|
+
import { useLocaleOptional } from '@djangocfg/i18n';
|
|
11
32
|
|
|
12
|
-
import {
|
|
33
|
+
import { usePathnameWithoutLocale } from '../../hooks/usePathnameWithoutLocale';
|
|
13
34
|
|
|
14
35
|
interface AnalyticsProviderProps {
|
|
15
36
|
children: ReactNode;
|
|
16
|
-
/**
|
|
37
|
+
/** GA4 measurement ID. Omit to run first-party only. */
|
|
17
38
|
trackingId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Templated route (`/[locale]/blog/[slug]`). Next does not expose it to client
|
|
41
|
+
* components, so an app can pass it for exact grouping. Optional: without it
|
|
42
|
+
* the locale-stripped pathname is used, which already prevents the worst
|
|
43
|
+
* failure (see below).
|
|
44
|
+
*/
|
|
45
|
+
route?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function AnalyticsProvider({
|
|
49
|
+
children,
|
|
50
|
+
trackingId,
|
|
51
|
+
route,
|
|
52
|
+
}: AnalyticsProviderProps) {
|
|
53
|
+
const { user } = useAuth();
|
|
54
|
+
|
|
55
|
+
// The locale is READ here rather than passed in by every app.
|
|
56
|
+
//
|
|
57
|
+
// It matters: without it, `/en/pricing` and `/ru/pricing` are counted as two
|
|
58
|
+
// different pages, and the top-pages report is meaningless on a
|
|
59
|
+
// locale-prefixed site. `useLocaleOptional` returns undefined (rather than
|
|
60
|
+
// throwing) in an app with no i18n, so this stays safe for both.
|
|
61
|
+
// `?? undefined` is load-bearing: useLocaleOptional returns `null` with no
|
|
62
|
+
// I18nProvider, and this package builds with `strict: false`, so a raw null
|
|
63
|
+
// would sail through the `locale?: string` prop and end up in the payload.
|
|
64
|
+
const locale = useLocaleOptional() ?? undefined;
|
|
65
|
+
|
|
66
|
+
// The locale-stripped path is the fallback "route". Not as good as the real
|
|
67
|
+
// templated route (`/blog/[slug]` still fragments per slug), but it fixes the
|
|
68
|
+
// locale split with zero work from the app.
|
|
69
|
+
const strippedPath = usePathnameWithoutLocale(locale);
|
|
70
|
+
|
|
71
|
+
const transports = useMemo<AnalyticsTransport[]>(() => {
|
|
72
|
+
const list: AnalyticsTransport[] = [djangocfg()];
|
|
73
|
+
if (trackingId) list.push(ga4({ trackingId }));
|
|
74
|
+
return list;
|
|
75
|
+
}, [trackingId]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<BaseAnalyticsProvider
|
|
79
|
+
transports={transports}
|
|
80
|
+
// An opaque id — never an email. Goes back to null on logout, or the next
|
|
81
|
+
// anonymous session would inherit it.
|
|
82
|
+
userId={user?.id != null ? String(user.id) : null}
|
|
83
|
+
>
|
|
84
|
+
<AuthEventBridge />
|
|
85
|
+
<PageviewTracker route={route ?? strippedPath} locale={locale} />
|
|
86
|
+
{children}
|
|
87
|
+
</BaseAnalyticsProvider>
|
|
88
|
+
);
|
|
18
89
|
}
|
|
19
90
|
|
|
20
91
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
92
|
+
* Routes `@djangocfg/api`'s auth events into the analytics client.
|
|
93
|
+
*
|
|
94
|
+
* `api` cannot import `analytics` (analytics reads auth state — that would be a
|
|
95
|
+
* cycle), so `api` exposes a sink and we install the real implementation here,
|
|
96
|
+
* where both packages are already in scope.
|
|
97
|
+
*
|
|
98
|
+
* Without this bridge, every auth event — AUTH_LOGIN_SUCCESS, AUTH_OAUTH_FAIL,
|
|
99
|
+
* AUTH_SESSION_EXPIRED — hits api's built-in no-op stub and is silently
|
|
100
|
+
* discarded. That was the status quo before this package existed.
|
|
26
101
|
*/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
102
|
+
function AuthEventBridge() {
|
|
103
|
+
const client = useAnalyticsClient();
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!client) return;
|
|
107
|
+
|
|
108
|
+
setAnalyticsSink({
|
|
109
|
+
event: (name, params) => client.trackEvent(name, params),
|
|
110
|
+
setUser: (userId) => client.identify(userId),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Uninstall on unmount, or a torn-down client keeps receiving events.
|
|
114
|
+
return () => setAnalyticsSink(null);
|
|
115
|
+
}, [client]);
|
|
116
|
+
|
|
117
|
+
return null;
|
|
32
118
|
}
|
|
33
119
|
|
|
34
120
|
export default AnalyticsProvider;
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Analytics
|
|
2
|
+
* Analytics — MOVED to `@djangocfg/analytics`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* This is a back-compat re-export so existing imports keep working. New code
|
|
5
|
+
* should import from `@djangocfg/analytics` directly.
|
|
6
|
+
*
|
|
7
|
+
* Why it moved: this module used to own its own route listener. Adding a second
|
|
8
|
+
* destination (the self-hosted first-party backend) would have meant a SECOND
|
|
9
|
+
* listener, double-firing pageviews with a drifting event taxonomy. The new
|
|
10
|
+
* package has one listener that fans out to N transports — GA4 is now just one
|
|
11
|
+
* of them, alongside the django-cfg ingest endpoint.
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
|
-
export {
|
|
14
|
+
export { AnalyticsCategory, AnalyticsEvent, useAnalytics } from '@djangocfg/analytics';
|
|
15
|
+
|
|
16
|
+
export type { AnalyticsCategoryType, AnalyticsEventType } from '@djangocfg/analytics';
|
|
17
|
+
|
|
8
18
|
export { AnalyticsProvider } from './AnalyticsProvider';
|
|
9
|
-
export { AnalyticsEvent, AnalyticsCategory } from './events';
|
|
10
|
-
export type { AnalyticsEventType, AnalyticsCategoryType } from './events';
|
|
11
19
|
export type { AnalyticsConfig } from './types';
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
// OAuth Events
|
|
34
|
-
AUTH_OAUTH_START: 'auth_oauth_start',
|
|
35
|
-
AUTH_OAUTH_SUCCESS: 'auth_oauth_success',
|
|
36
|
-
AUTH_OAUTH_FAIL: 'auth_oauth_fail',
|
|
37
|
-
|
|
38
|
-
// Error Events
|
|
39
|
-
ERROR_BOUNDARY: 'error_boundary',
|
|
40
|
-
ERROR_API: 'error_api',
|
|
41
|
-
ERROR_VALIDATION: 'error_validation',
|
|
42
|
-
ERROR_NETWORK: 'error_network',
|
|
43
|
-
|
|
44
|
-
// Navigation Events
|
|
45
|
-
NAV_ADMIN_ENTER: 'nav_admin_enter',
|
|
46
|
-
NAV_DASHBOARD_ENTER: 'nav_dashboard_enter',
|
|
47
|
-
NAV_PAGE_VIEW: 'nav_page_view',
|
|
48
|
-
|
|
49
|
-
// Engagement Events
|
|
50
|
-
THEME_CHANGE: 'theme_change',
|
|
51
|
-
SIDEBAR_TOGGLE: 'sidebar_toggle',
|
|
52
|
-
MOBILE_MENU_OPEN: 'mobile_menu_open',
|
|
53
|
-
|
|
54
|
-
// User Events
|
|
55
|
-
USER_PROFILE_VIEW: 'user_profile_view',
|
|
56
|
-
USER_PROFILE_UPDATE: 'user_profile_update',
|
|
57
|
-
} as const;
|
|
58
|
-
|
|
59
|
-
export type AnalyticsCategoryType = typeof AnalyticsCategory[keyof typeof AnalyticsCategory];
|
|
60
|
-
export type AnalyticsEventType = typeof AnalyticsEvent[keyof typeof AnalyticsEvent];
|
|
@@ -1,142 +0,0 @@
|
|
|
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 { usePathname } from 'next/navigation';
|
|
12
|
-
import { useEffect } from 'react';
|
|
13
|
-
import ReactGA from 'react-ga4';
|
|
14
|
-
|
|
15
|
-
import { useAuth } from '@djangocfg/api/auth';
|
|
16
|
-
|
|
17
|
-
// Check if we're in production
|
|
18
|
-
import { isProd as isProduction } from '@djangocfg/ui-core/lib';
|
|
19
|
-
|
|
20
|
-
// Tracking state
|
|
21
|
-
let isInitialized = false;
|
|
22
|
-
// @ts-ignore reserved for future use (e.g., analytics debugging)
|
|
23
|
-
let _currentTrackingId: string | undefined;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Analytics utility object for standalone usage (outside React components)
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```ts
|
|
30
|
-
* import { Analytics } from '@djangocfg/layouts';
|
|
31
|
-
*
|
|
32
|
-
* // In an event handler or utility function
|
|
33
|
-
* Analytics.event('button_click', { category: 'engagement', label: 'signup' });
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
export const Analytics = {
|
|
37
|
-
/**
|
|
38
|
-
* Initialize Google Analytics (called automatically by useAnalytics hook)
|
|
39
|
-
*/
|
|
40
|
-
init: (trackingId: string) => {
|
|
41
|
-
if (!isProduction || !trackingId || isInitialized) return;
|
|
42
|
-
ReactGA.initialize(trackingId);
|
|
43
|
-
isInitialized = true;
|
|
44
|
-
_currentTrackingId = trackingId;
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Check if Analytics is enabled and initialized
|
|
49
|
-
*/
|
|
50
|
-
isEnabled: () => isProduction && isInitialized,
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Track a page view
|
|
54
|
-
*/
|
|
55
|
-
pageview: (path: string) => {
|
|
56
|
-
if (!Analytics.isEnabled()) return;
|
|
57
|
-
ReactGA.send({ hitType: 'pageview', page: path });
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Track a custom event
|
|
62
|
-
* @param name - Event name (action)
|
|
63
|
-
* @param params - Optional event parameters
|
|
64
|
-
*/
|
|
65
|
-
event: (name: string, params: Record<string, any> = {}) => {
|
|
66
|
-
if (!Analytics.isEnabled()) return;
|
|
67
|
-
ReactGA.event(name, params);
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Set user ID for tracking
|
|
72
|
-
*/
|
|
73
|
-
setUser: (userId: string) => {
|
|
74
|
-
if (!Analytics.isEnabled()) return;
|
|
75
|
-
ReactGA.set({ user_id: userId });
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Set custom dimensions/metrics
|
|
80
|
-
*/
|
|
81
|
-
set: (fieldsObject: Record<string, any>) => {
|
|
82
|
-
if (!Analytics.isEnabled()) return;
|
|
83
|
-
ReactGA.set(fieldsObject);
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Hook for Google Analytics tracking via react-ga4
|
|
89
|
-
*
|
|
90
|
-
* Automatically initializes GA and tracks page views on route changes
|
|
91
|
-
* Only works in production mode (NODE_ENV === 'production')
|
|
92
|
-
*
|
|
93
|
-
* @example
|
|
94
|
-
* ```tsx
|
|
95
|
-
* // Just call the hook - it auto-tracks pageviews
|
|
96
|
-
* useAnalytics();
|
|
97
|
-
*
|
|
98
|
-
* // Or use the returned methods for custom tracking
|
|
99
|
-
* const { event, isEnabled } = useAnalytics();
|
|
100
|
-
* event('button_click', { category: 'engagement', label: 'signup' });
|
|
101
|
-
* ```
|
|
102
|
-
*/
|
|
103
|
-
export function useAnalytics(trackingIdProp?: string) {
|
|
104
|
-
const pathname = usePathname();
|
|
105
|
-
const { user, isAuthenticated } = useAuth();
|
|
106
|
-
|
|
107
|
-
// Use trackingId from prop (passed from AppLayout)
|
|
108
|
-
const trackingId = trackingIdProp;
|
|
109
|
-
const isEnabled = isProduction && Boolean(trackingId);
|
|
110
|
-
|
|
111
|
-
// Initialize GA4
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
if (!isEnabled || !trackingId) return;
|
|
114
|
-
Analytics.init(trackingId);
|
|
115
|
-
}, [isEnabled, trackingId]);
|
|
116
|
-
|
|
117
|
-
// Auto-set user ID when authenticated
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
if (!isEnabled || !isAuthenticated || !user?.id) return;
|
|
120
|
-
Analytics.setUser(String(user.id));
|
|
121
|
-
}, [isEnabled, isAuthenticated, user?.id]);
|
|
122
|
-
|
|
123
|
-
// Auto-track page views on route change (App Router)
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
if (!isEnabled || typeof window === 'undefined') return;
|
|
126
|
-
|
|
127
|
-
// Track page view when pathname changes
|
|
128
|
-
const url = pathname + (window.location.search || '');
|
|
129
|
-
Analytics.pageview(url);
|
|
130
|
-
}, [pathname, isEnabled]);
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
isEnabled,
|
|
134
|
-
trackingId,
|
|
135
|
-
pageview: Analytics.pageview,
|
|
136
|
-
event: Analytics.event,
|
|
137
|
-
setUser: Analytics.setUser,
|
|
138
|
-
set: Analytics.set,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export default useAnalytics;
|