@djangocfg/layouts 2.1.48 → 2.1.50
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 +8 -8
- package/src/components/core/index.ts +0 -1
- package/src/components/errors/ErrorsTracker/components/ErrorButtons.tsx +10 -5
- package/src/components/errors/ErrorsTracker/components/ErrorToast.tsx +40 -2
- package/src/components/errors/ErrorsTracker/index.ts +4 -1
- package/src/components/errors/ErrorsTracker/providers/ErrorTrackingProvider.tsx +33 -3
- package/src/components/errors/ErrorsTracker/types.ts +51 -2
- package/src/components/errors/ErrorsTracker/utils/formatters.ts +22 -2
- package/src/index.ts +4 -0
- package/src/layouts/AppLayout/BaseApp.tsx +7 -3
- package/src/snippets/PushNotifications/components/PushPrompt.tsx +8 -1
- package/src/components/core/PageProgress.tsx +0 -127
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.50",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -92,12 +92,13 @@
|
|
|
92
92
|
"check": "tsc --noEmit"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@djangocfg/api": "^2.1.
|
|
96
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
97
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
95
|
+
"@djangocfg/api": "^2.1.50",
|
|
96
|
+
"@djangocfg/centrifugo": "^2.1.50",
|
|
97
|
+
"@djangocfg/ui-nextjs": "^2.1.50",
|
|
98
98
|
"@hookform/resolvers": "^5.2.0",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
100
|
"lucide-react": "^0.545.0",
|
|
101
|
+
"moment": "^2.30.1",
|
|
101
102
|
"next": ">=15.0.0",
|
|
102
103
|
"p-retry": "^7.0.0",
|
|
103
104
|
"react": "^19.1.0",
|
|
@@ -107,15 +108,15 @@
|
|
|
107
108
|
"swr": "^2.3.7",
|
|
108
109
|
"tailwindcss": "^4.1.14",
|
|
109
110
|
"tailwindcss-animate": "^1.0.7",
|
|
110
|
-
"zod": "^4.1.13"
|
|
111
|
-
"moment": "^2.30.1"
|
|
111
|
+
"zod": "^4.1.13"
|
|
112
112
|
},
|
|
113
113
|
"dependencies": {
|
|
114
|
+
"nextjs-toploader": "^3.9.17",
|
|
114
115
|
"react-ga4": "^2.1.0",
|
|
115
116
|
"uuid": "^11.1.0"
|
|
116
117
|
},
|
|
117
118
|
"devDependencies": {
|
|
118
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
119
|
+
"@djangocfg/typescript-config": "^2.1.50",
|
|
119
120
|
"@types/node": "^24.7.2",
|
|
120
121
|
"@types/react": "^19.1.0",
|
|
121
122
|
"@types/react-dom": "^19.1.0",
|
|
@@ -126,4 +127,3 @@
|
|
|
126
127
|
"access": "public"
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
|
-
|
|
@@ -7,6 +7,5 @@ export type { ClientOnlyProps } from './ClientOnly';
|
|
|
7
7
|
export { JsonLd } from './JsonLd';
|
|
8
8
|
export { LucideIcon } from './LucideIcon';
|
|
9
9
|
export type { LucideIconProps } from './LucideIcon';
|
|
10
|
-
export { PageProgress } from './PageProgress';
|
|
11
10
|
export { Suspense } from './Suspense';
|
|
12
11
|
|
|
@@ -13,18 +13,21 @@ import { Button, useCopy } from '@djangocfg/ui-nextjs';
|
|
|
13
13
|
|
|
14
14
|
import { generateCurlFromError } from '../utils/curl-generator';
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
formatCentrifugoErrorForClipboard,
|
|
17
|
+
formatCORSErrorForClipboard,
|
|
18
|
+
formatNetworkErrorForClipboard,
|
|
19
|
+
formatValidationErrorForClipboard
|
|
17
20
|
} from '../utils/formatters';
|
|
18
21
|
|
|
19
|
-
import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail } from '../types';
|
|
22
|
+
import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail, CentrifugoErrorDetail } from '../types';
|
|
20
23
|
export interface ErrorButtonsProps {
|
|
21
|
-
detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail;
|
|
24
|
+
detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail | CentrifugoErrorDetail;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* Format error for clipboard based on type
|
|
26
29
|
*/
|
|
27
|
-
function formatErrorForClipboard(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail): string {
|
|
30
|
+
function formatErrorForClipboard(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail | CentrifugoErrorDetail): string {
|
|
28
31
|
switch (detail.type) {
|
|
29
32
|
case 'validation':
|
|
30
33
|
return formatValidationErrorForClipboard(detail);
|
|
@@ -32,6 +35,8 @@ function formatErrorForClipboard(detail: ValidationErrorDetail | CORSErrorDetail
|
|
|
32
35
|
return formatCORSErrorForClipboard(detail);
|
|
33
36
|
case 'network':
|
|
34
37
|
return formatNetworkErrorForClipboard(detail);
|
|
38
|
+
case 'centrifugo':
|
|
39
|
+
return formatCentrifugoErrorForClipboard(detail);
|
|
35
40
|
default:
|
|
36
41
|
return JSON.stringify(detail, null, 2);
|
|
37
42
|
}
|
|
@@ -40,7 +45,7 @@ function formatErrorForClipboard(detail: ValidationErrorDetail | CORSErrorDetail
|
|
|
40
45
|
/**
|
|
41
46
|
* Check if error supports cURL generation
|
|
42
47
|
*/
|
|
43
|
-
function supportsCurl(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail): boolean {
|
|
48
|
+
function supportsCurl(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail | CentrifugoErrorDetail): boolean {
|
|
44
49
|
return detail.type === 'validation';
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -15,9 +15,11 @@ import type {
|
|
|
15
15
|
ValidationErrorDetail,
|
|
16
16
|
CORSErrorDetail,
|
|
17
17
|
NetworkErrorDetail,
|
|
18
|
+
CentrifugoErrorDetail,
|
|
18
19
|
ValidationErrorConfig,
|
|
19
20
|
CORSErrorConfig,
|
|
20
21
|
NetworkErrorConfig,
|
|
22
|
+
CentrifugoErrorConfig,
|
|
21
23
|
} from '../types';
|
|
22
24
|
/**
|
|
23
25
|
* Build validation error description
|
|
@@ -133,12 +135,46 @@ function buildNetworkDescription(
|
|
|
133
135
|
);
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Build centrifugo error description
|
|
140
|
+
*/
|
|
141
|
+
function buildCentrifugoDescription(
|
|
142
|
+
detail: CentrifugoErrorDetail,
|
|
143
|
+
config: Required<CentrifugoErrorConfig>
|
|
144
|
+
): React.ReactNode {
|
|
145
|
+
const parts: string[] = [];
|
|
146
|
+
|
|
147
|
+
// Add method info
|
|
148
|
+
if (config.showMethod) {
|
|
149
|
+
parts.push(`RPC: ${detail.method}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add error code
|
|
153
|
+
if (config.showCode && detail.code !== undefined) {
|
|
154
|
+
parts.push(`Code: ${detail.code}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="flex flex-col gap-2 text-sm">
|
|
159
|
+
{parts.length > 0 && (
|
|
160
|
+
<div className="font-mono text-xs opacity-90">
|
|
161
|
+
{parts.join(' • ')}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
<div className="opacity-90">{detail.error}</div>
|
|
166
|
+
|
|
167
|
+
<ErrorButtons detail={detail} />
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
136
172
|
/**
|
|
137
173
|
* Create toast options for any error type
|
|
138
174
|
*/
|
|
139
175
|
export function createErrorToast(
|
|
140
|
-
detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail,
|
|
141
|
-
config: Required<ValidationErrorConfig | CORSErrorConfig | NetworkErrorConfig>
|
|
176
|
+
detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail | CentrifugoErrorDetail,
|
|
177
|
+
config: Required<ValidationErrorConfig | CORSErrorConfig | NetworkErrorConfig | CentrifugoErrorConfig>
|
|
142
178
|
) {
|
|
143
179
|
let description: React.ReactNode;
|
|
144
180
|
|
|
@@ -147,6 +183,8 @@ export function createErrorToast(
|
|
|
147
183
|
description = buildValidationDescription(detail, config as Required<ValidationErrorConfig>);
|
|
148
184
|
} else if (detail.type === 'cors') {
|
|
149
185
|
description = buildCORSDescription(detail, config as Required<CORSErrorConfig>);
|
|
186
|
+
} else if (detail.type === 'centrifugo') {
|
|
187
|
+
description = buildCentrifugoDescription(detail, config as Required<CentrifugoErrorConfig>);
|
|
150
188
|
} else {
|
|
151
189
|
description = buildNetworkDescription(detail, config as Required<NetworkErrorConfig>);
|
|
152
190
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Error Tracking - Unified error tracking for all error types
|
|
3
3
|
*
|
|
4
|
-
* Single provider and hook for validation, CORS, and
|
|
4
|
+
* Single provider and hook for validation, CORS, network, and Centrifugo errors
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// Main provider and hook
|
|
@@ -14,11 +14,13 @@ export type {
|
|
|
14
14
|
ValidationErrorDetail,
|
|
15
15
|
CORSErrorDetail,
|
|
16
16
|
NetworkErrorDetail,
|
|
17
|
+
CentrifugoErrorDetail,
|
|
17
18
|
StoredError,
|
|
18
19
|
ErrorTrackingConfig,
|
|
19
20
|
ValidationErrorConfig,
|
|
20
21
|
CORSErrorConfig,
|
|
21
22
|
NetworkErrorConfig,
|
|
23
|
+
CentrifugoErrorConfig,
|
|
22
24
|
ErrorTrackingContextValue,
|
|
23
25
|
} from './types';
|
|
24
26
|
|
|
@@ -32,6 +34,7 @@ export {
|
|
|
32
34
|
formatValidationErrorForClipboard,
|
|
33
35
|
formatCORSErrorForClipboard,
|
|
34
36
|
formatNetworkErrorForClipboard,
|
|
37
|
+
formatCentrifugoErrorForClipboard,
|
|
35
38
|
formatErrorTitle,
|
|
36
39
|
extractDomain,
|
|
37
40
|
} from './utils/formatters';
|
|
@@ -34,7 +34,11 @@ import { toast } from 'sonner';
|
|
|
34
34
|
|
|
35
35
|
import { createErrorToast } from '../components/ErrorToast';
|
|
36
36
|
import {
|
|
37
|
-
|
|
37
|
+
DEFAULT_CENTRIFUGO_CONFIG,
|
|
38
|
+
DEFAULT_CORS_CONFIG,
|
|
39
|
+
DEFAULT_NETWORK_CONFIG,
|
|
40
|
+
DEFAULT_VALIDATION_CONFIG,
|
|
41
|
+
ERROR_EVENTS
|
|
38
42
|
} from '../types';
|
|
39
43
|
|
|
40
44
|
import type {
|
|
@@ -44,9 +48,11 @@ import type {
|
|
|
44
48
|
ValidationErrorConfig,
|
|
45
49
|
CORSErrorConfig,
|
|
46
50
|
NetworkErrorConfig,
|
|
51
|
+
CentrifugoErrorConfig,
|
|
47
52
|
ValidationErrorDetail,
|
|
48
53
|
CORSErrorDetail,
|
|
49
54
|
NetworkErrorDetail,
|
|
55
|
+
CentrifugoErrorDetail,
|
|
50
56
|
ErrorTrackingContextValue,
|
|
51
57
|
} from '../types';
|
|
52
58
|
const ErrorTrackingContext = createContext<ErrorTrackingContextValue | undefined>(undefined);
|
|
@@ -64,6 +70,7 @@ export interface ErrorTrackingProviderProps {
|
|
|
64
70
|
validation?: Partial<ValidationErrorConfig>;
|
|
65
71
|
cors?: Partial<CORSErrorConfig>;
|
|
66
72
|
network?: Partial<NetworkErrorConfig>;
|
|
73
|
+
centrifugo?: Partial<CentrifugoErrorConfig>;
|
|
67
74
|
onError?: (error: ErrorDetail) => boolean | void;
|
|
68
75
|
}
|
|
69
76
|
|
|
@@ -77,6 +84,7 @@ export function ErrorTrackingProvider({
|
|
|
77
84
|
validation: userValidationConfig,
|
|
78
85
|
cors: userCorsConfig,
|
|
79
86
|
network: userNetworkConfig,
|
|
87
|
+
centrifugo: userCentrifugoConfig,
|
|
80
88
|
onError,
|
|
81
89
|
}: ErrorTrackingProviderProps) {
|
|
82
90
|
const [errors, setErrors] = useState<StoredError[]>([]);
|
|
@@ -97,6 +105,11 @@ export function ErrorTrackingProvider({
|
|
|
97
105
|
...userNetworkConfig,
|
|
98
106
|
};
|
|
99
107
|
|
|
108
|
+
const centrifugoConfig: Required<CentrifugoErrorConfig> = {
|
|
109
|
+
...DEFAULT_CENTRIFUGO_CONFIG,
|
|
110
|
+
...userCentrifugoConfig,
|
|
111
|
+
};
|
|
112
|
+
|
|
100
113
|
/**
|
|
101
114
|
* Clear all errors
|
|
102
115
|
*/
|
|
@@ -122,7 +135,7 @@ export function ErrorTrackingProvider({
|
|
|
122
135
|
* Handle any error event
|
|
123
136
|
*/
|
|
124
137
|
const handleError = useCallback(
|
|
125
|
-
(detail: ErrorDetail, config: Required<ValidationErrorConfig | CORSErrorConfig | NetworkErrorConfig>) => {
|
|
138
|
+
(detail: ErrorDetail, config: Required<ValidationErrorConfig | CORSErrorConfig | NetworkErrorConfig | CentrifugoErrorConfig>) => {
|
|
126
139
|
// Create stored error with ID
|
|
127
140
|
const storedError: StoredError = {
|
|
128
141
|
...detail,
|
|
@@ -201,24 +214,40 @@ export function ErrorTrackingProvider({
|
|
|
201
214
|
handlers.push({ event: ERROR_EVENTS.NETWORK, handler });
|
|
202
215
|
}
|
|
203
216
|
|
|
217
|
+
// Centrifugo errors
|
|
218
|
+
if (centrifugoConfig.enabled) {
|
|
219
|
+
const handler = (event: Event) => {
|
|
220
|
+
if (!(event instanceof CustomEvent)) return;
|
|
221
|
+
const detail: CentrifugoErrorDetail = {
|
|
222
|
+
...event.detail,
|
|
223
|
+
type: 'centrifugo' as const,
|
|
224
|
+
};
|
|
225
|
+
handleError(detail, centrifugoConfig);
|
|
226
|
+
};
|
|
227
|
+
window.addEventListener(ERROR_EVENTS.CENTRIFUGO, handler);
|
|
228
|
+
handlers.push({ event: ERROR_EVENTS.CENTRIFUGO, handler });
|
|
229
|
+
}
|
|
230
|
+
|
|
204
231
|
// Cleanup
|
|
205
232
|
return () => {
|
|
206
233
|
handlers.forEach(({ event, handler }) => {
|
|
207
234
|
window.removeEventListener(event, handler);
|
|
208
235
|
});
|
|
209
236
|
};
|
|
210
|
-
}, [handleError, validationConfig, corsConfig, networkConfig]);
|
|
237
|
+
}, [handleError, validationConfig, corsConfig, networkConfig, centrifugoConfig]);
|
|
211
238
|
|
|
212
239
|
// Filter errors by type
|
|
213
240
|
const validationErrors = errors.filter((e) => e.type === 'validation') as StoredError<ValidationErrorDetail>[];
|
|
214
241
|
const corsErrors = errors.filter((e) => e.type === 'cors') as StoredError<CORSErrorDetail>[];
|
|
215
242
|
const networkErrors = errors.filter((e) => e.type === 'network') as StoredError<NetworkErrorDetail>[];
|
|
243
|
+
const centrifugoErrors = errors.filter((e) => e.type === 'centrifugo') as StoredError<CentrifugoErrorDetail>[];
|
|
216
244
|
|
|
217
245
|
const value: ErrorTrackingContextValue = {
|
|
218
246
|
errors,
|
|
219
247
|
validationErrors,
|
|
220
248
|
corsErrors,
|
|
221
249
|
networkErrors,
|
|
250
|
+
centrifugoErrors,
|
|
222
251
|
clearErrors,
|
|
223
252
|
clearErrorsByType,
|
|
224
253
|
clearError,
|
|
@@ -228,6 +257,7 @@ export function ErrorTrackingProvider({
|
|
|
228
257
|
validation: validationConfig,
|
|
229
258
|
cors: corsConfig,
|
|
230
259
|
network: networkConfig,
|
|
260
|
+
centrifugo: centrifugoConfig,
|
|
231
261
|
},
|
|
232
262
|
};
|
|
233
263
|
|
|
@@ -59,10 +59,25 @@ export interface NetworkErrorDetail extends BaseErrorDetail {
|
|
|
59
59
|
statusCode?: number;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Centrifugo error detail (from centrifugo-error event)
|
|
64
|
+
*/
|
|
65
|
+
export interface CentrifugoErrorDetail extends BaseErrorDetail {
|
|
66
|
+
type: 'centrifugo';
|
|
67
|
+
/** RPC method that failed */
|
|
68
|
+
method: string;
|
|
69
|
+
/** Error message */
|
|
70
|
+
error: string;
|
|
71
|
+
/** Error code from Centrifugo */
|
|
72
|
+
code?: number;
|
|
73
|
+
/** Additional data sent with the request */
|
|
74
|
+
data?: any;
|
|
75
|
+
}
|
|
76
|
+
|
|
62
77
|
/**
|
|
63
78
|
* Union type of all error details
|
|
64
79
|
*/
|
|
65
|
-
export type ErrorDetail = ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail;
|
|
80
|
+
export type ErrorDetail = ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail | CentrifugoErrorDetail;
|
|
66
81
|
|
|
67
82
|
/**
|
|
68
83
|
* Stored error with unique ID
|
|
@@ -170,6 +185,23 @@ export interface NetworkErrorConfig extends ErrorTypeConfig {
|
|
|
170
185
|
showStatusCode?: boolean;
|
|
171
186
|
}
|
|
172
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Centrifugo error specific config
|
|
190
|
+
*/
|
|
191
|
+
export interface CentrifugoErrorConfig extends ErrorTypeConfig {
|
|
192
|
+
/**
|
|
193
|
+
* Show RPC method in toast
|
|
194
|
+
* @default true
|
|
195
|
+
*/
|
|
196
|
+
showMethod?: boolean;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Show error code in toast
|
|
200
|
+
* @default true
|
|
201
|
+
*/
|
|
202
|
+
showCode?: boolean;
|
|
203
|
+
}
|
|
204
|
+
|
|
173
205
|
/**
|
|
174
206
|
* Complete error tracking configuration
|
|
175
207
|
*/
|
|
@@ -189,6 +221,11 @@ export interface ErrorTrackingConfig {
|
|
|
189
221
|
*/
|
|
190
222
|
network?: NetworkErrorConfig;
|
|
191
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Centrifugo error tracking configuration
|
|
226
|
+
*/
|
|
227
|
+
centrifugo?: CentrifugoErrorConfig;
|
|
228
|
+
|
|
192
229
|
/**
|
|
193
230
|
* Custom error handler (called before toast for all errors)
|
|
194
231
|
* Return false to prevent default toast notification
|
|
@@ -212,11 +249,14 @@ export interface ErrorTrackingContextValue {
|
|
|
212
249
|
/** Network errors only */
|
|
213
250
|
networkErrors: StoredError<NetworkErrorDetail>[];
|
|
214
251
|
|
|
252
|
+
/** Centrifugo errors only */
|
|
253
|
+
centrifugoErrors: StoredError<CentrifugoErrorDetail>[];
|
|
254
|
+
|
|
215
255
|
/** Clear all errors */
|
|
216
256
|
clearErrors: () => void;
|
|
217
257
|
|
|
218
258
|
/** Clear errors by type */
|
|
219
|
-
clearErrorsByType: (type: 'validation' | 'cors' | 'network') => void;
|
|
259
|
+
clearErrorsByType: (type: 'validation' | 'cors' | 'network' | 'centrifugo') => void;
|
|
220
260
|
|
|
221
261
|
/** Clear specific error by ID */
|
|
222
262
|
clearError: (id: string) => void;
|
|
@@ -232,6 +272,7 @@ export interface ErrorTrackingContextValue {
|
|
|
232
272
|
validation: Required<ValidationErrorConfig>;
|
|
233
273
|
cors: Required<CORSErrorConfig>;
|
|
234
274
|
network: Required<NetworkErrorConfig>;
|
|
275
|
+
centrifugo: Required<CentrifugoErrorConfig>;
|
|
235
276
|
};
|
|
236
277
|
}
|
|
237
278
|
|
|
@@ -242,6 +283,7 @@ export const ERROR_EVENTS = {
|
|
|
242
283
|
VALIDATION: 'zod-validation-error',
|
|
243
284
|
CORS: 'cors-error',
|
|
244
285
|
NETWORK: 'network-error',
|
|
286
|
+
CENTRIFUGO: 'centrifugo-error',
|
|
245
287
|
} as const;
|
|
246
288
|
|
|
247
289
|
/**
|
|
@@ -276,3 +318,10 @@ export const DEFAULT_NETWORK_CONFIG: Required<NetworkErrorConfig> = {
|
|
|
276
318
|
showMethod: true,
|
|
277
319
|
showStatusCode: true,
|
|
278
320
|
};
|
|
321
|
+
|
|
322
|
+
export const DEFAULT_CENTRIFUGO_CONFIG: Required<CentrifugoErrorConfig> = {
|
|
323
|
+
...DEFAULT_ERROR_CONFIG,
|
|
324
|
+
duration: 0, // Don't auto-dismiss centrifugo errors
|
|
325
|
+
showMethod: true,
|
|
326
|
+
showCode: true,
|
|
327
|
+
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ZodError } from 'zod';
|
|
8
|
-
import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail } from '../types';
|
|
8
|
+
import type { ValidationErrorDetail, CORSErrorDetail, NetworkErrorDetail, CentrifugoErrorDetail } from '../types';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Format Zod error issues for display
|
|
@@ -83,6 +83,22 @@ export function formatNetworkErrorForClipboard(detail: NetworkErrorDetail): stri
|
|
|
83
83
|
return JSON.stringify(errorData, null, 2);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Format centrifugo error for clipboard
|
|
88
|
+
*/
|
|
89
|
+
export function formatCentrifugoErrorForClipboard(detail: CentrifugoErrorDetail): string {
|
|
90
|
+
const errorData = {
|
|
91
|
+
type: 'centrifugo',
|
|
92
|
+
timestamp: detail.timestamp.toISOString(),
|
|
93
|
+
method: detail.method,
|
|
94
|
+
error: detail.error,
|
|
95
|
+
...(detail.code !== undefined && { code: detail.code }),
|
|
96
|
+
...(detail.data && { data: detail.data }),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return JSON.stringify(errorData, null, 2);
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
/**
|
|
87
103
|
* Extract domain from URL
|
|
88
104
|
*/
|
|
@@ -98,7 +114,7 @@ export function extractDomain(url: string): string {
|
|
|
98
114
|
/**
|
|
99
115
|
* Format error title based on type
|
|
100
116
|
*/
|
|
101
|
-
export function formatErrorTitle(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail): string {
|
|
117
|
+
export function formatErrorTitle(detail: ValidationErrorDetail | CORSErrorDetail | NetworkErrorDetail | CentrifugoErrorDetail): string {
|
|
102
118
|
switch (detail.type) {
|
|
103
119
|
case 'validation':
|
|
104
120
|
return `❌ Validation Error in ${detail.operation}`;
|
|
@@ -108,6 +124,10 @@ export function formatErrorTitle(detail: ValidationErrorDetail | CORSErrorDetail
|
|
|
108
124
|
return detail.statusCode
|
|
109
125
|
? `⚠️ Network Error (${detail.statusCode})`
|
|
110
126
|
: '⚠️ Network Error';
|
|
127
|
+
case 'centrifugo':
|
|
128
|
+
return detail.code !== undefined
|
|
129
|
+
? `🔌 Centrifugo Error (${detail.code})`
|
|
130
|
+
: '🔌 Centrifugo Error';
|
|
111
131
|
default:
|
|
112
132
|
return '❌ Error';
|
|
113
133
|
}
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
// Layout components
|
|
29
29
|
export * from './layouts';
|
|
30
30
|
|
|
31
|
+
// Re-export useRouter from nextjs-toploader for progress bar support
|
|
32
|
+
// Use this instead of 'next/navigation' useRouter for router.push() to trigger progress
|
|
33
|
+
export { useRouter } from 'nextjs-toploader/app';
|
|
34
|
+
|
|
31
35
|
// Snippets - Reusable UI components (includes Analytics)
|
|
32
36
|
export * from './snippets';
|
|
33
37
|
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
'use client';
|
|
46
46
|
|
|
47
47
|
import dynamic from 'next/dynamic';
|
|
48
|
+
import NextTopLoader from 'nextjs-toploader';
|
|
48
49
|
import { ReactNode } from 'react';
|
|
49
50
|
import { SWRConfig } from 'swr';
|
|
50
51
|
|
|
@@ -52,8 +53,6 @@ import { getCentrifugoAuthTokenRetrieve } from '@djangocfg/api';
|
|
|
52
53
|
import { AuthProvider } from '@djangocfg/api/auth';
|
|
53
54
|
import { CentrifugoProvider } from '@djangocfg/centrifugo';
|
|
54
55
|
import { SonnerToaster, ThemeProvider, TooltipProvider } from '@djangocfg/ui-nextjs';
|
|
55
|
-
|
|
56
|
-
import { PageProgress } from '../../components/core/PageProgress';
|
|
57
56
|
import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
|
|
58
57
|
import { ErrorTrackingProvider } from '../../components/errors/ErrorsTracker';
|
|
59
58
|
import { AnalyticsProvider } from '../../snippets/Analytics';
|
|
@@ -143,7 +142,12 @@ export function BaseApp({
|
|
|
143
142
|
onError={errorTracking?.onError}
|
|
144
143
|
>
|
|
145
144
|
{children}
|
|
146
|
-
<
|
|
145
|
+
<NextTopLoader
|
|
146
|
+
color="hsl(var(--primary))"
|
|
147
|
+
height={3}
|
|
148
|
+
showSpinner={false}
|
|
149
|
+
shadow="0 0 10px hsl(var(--primary)), 0 0 5px hsl(var(--primary))"
|
|
150
|
+
/>
|
|
147
151
|
<SonnerToaster />
|
|
148
152
|
|
|
149
153
|
{/* PWA Install Hint */}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { Bell, X } from 'lucide-react';
|
|
11
11
|
import React, { useEffect, useState } from 'react';
|
|
12
12
|
|
|
13
|
+
import { useAuth } from '@djangocfg/api/auth';
|
|
13
14
|
import { Button } from '@djangocfg/ui-nextjs';
|
|
14
15
|
|
|
15
16
|
import { usePushNotifications } from '../hooks/usePushNotifications';
|
|
@@ -60,6 +61,7 @@ export function PushPrompt({
|
|
|
60
61
|
onEnabled,
|
|
61
62
|
onDismissed,
|
|
62
63
|
}: PushPromptProps) {
|
|
64
|
+
const { isAuthenticated, isLoading: isAuthLoading } = useAuth();
|
|
63
65
|
const { isSupported, permission, isSubscribed, subscribe } = usePushNotifications({
|
|
64
66
|
vapidPublicKey,
|
|
65
67
|
subscribeEndpoint,
|
|
@@ -70,6 +72,11 @@ export function PushPrompt({
|
|
|
70
72
|
|
|
71
73
|
// Check if should show
|
|
72
74
|
useEffect(() => {
|
|
75
|
+
// Wait for auth to complete, don't show for unauthenticated users
|
|
76
|
+
if (isAuthLoading || !isAuthenticated) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
if (!isSupported || isSubscribed || permission === 'denied') {
|
|
74
81
|
return;
|
|
75
82
|
}
|
|
@@ -89,7 +96,7 @@ export function PushPrompt({
|
|
|
89
96
|
// Show after delay
|
|
90
97
|
const timer = setTimeout(() => setShow(true), delayMs);
|
|
91
98
|
return () => clearTimeout(timer);
|
|
92
|
-
}, [isSupported, isSubscribed, permission, requirePWA, resetAfterDays, delayMs]);
|
|
99
|
+
}, [isAuthLoading, isAuthenticated, isSupported, isSubscribed, permission, requirePWA, resetAfterDays, delayMs]);
|
|
93
100
|
|
|
94
101
|
const handleEnable = async () => {
|
|
95
102
|
setEnabling(true);
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PageProgress - Loading progress bar
|
|
3
|
-
*
|
|
4
|
-
* Shows a progress bar at the top of the page during route transitions
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
'use client';
|
|
8
|
-
|
|
9
|
-
import { usePathname } from 'next/navigation';
|
|
10
|
-
import { useEffect, useRef, useState } from 'react';
|
|
11
|
-
|
|
12
|
-
export function PageProgress() {
|
|
13
|
-
const pathname = usePathname();
|
|
14
|
-
const [loading, setLoading] = useState(false);
|
|
15
|
-
const [progress, setProgress] = useState(0);
|
|
16
|
-
const [mounted, setMounted] = useState(false);
|
|
17
|
-
const progressTimer = useRef<NodeJS.Timeout | null>(null);
|
|
18
|
-
const prevPathname = useRef<string | null>(null);
|
|
19
|
-
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
setMounted(true);
|
|
22
|
-
}, []);
|
|
23
|
-
|
|
24
|
-
// Simulate realistic progress
|
|
25
|
-
const startFakeProgress = () => {
|
|
26
|
-
// Clear any existing timer
|
|
27
|
-
if (progressTimer.current) {
|
|
28
|
-
clearInterval(progressTimer.current);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
setProgress(0);
|
|
32
|
-
|
|
33
|
-
// Quickly go to 20% to show immediate feedback
|
|
34
|
-
setTimeout(() => setProgress(20), 50);
|
|
35
|
-
|
|
36
|
-
// Then slowly increase to 90% (never reach 100% until actually complete)
|
|
37
|
-
progressTimer.current = setInterval(() => {
|
|
38
|
-
setProgress((prevProgress) => {
|
|
39
|
-
if (prevProgress >= 90) {
|
|
40
|
-
if (progressTimer.current) {
|
|
41
|
-
clearInterval(progressTimer.current);
|
|
42
|
-
}
|
|
43
|
-
return 90;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Slow down as we get closer to 90%
|
|
47
|
-
const increment = 90 - prevProgress;
|
|
48
|
-
return prevProgress + (increment / 10);
|
|
49
|
-
});
|
|
50
|
-
}, 300);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const completeProgress = () => {
|
|
54
|
-
// Clear any existing timer
|
|
55
|
-
if (progressTimer.current) {
|
|
56
|
-
clearInterval(progressTimer.current);
|
|
57
|
-
progressTimer.current = null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Jump to 100% and then hide after showing completion
|
|
61
|
-
setProgress(100);
|
|
62
|
-
setTimeout(() => {
|
|
63
|
-
setLoading(false);
|
|
64
|
-
setTimeout(() => {
|
|
65
|
-
setProgress(0);
|
|
66
|
-
}, 300); // Wait for fade out animation
|
|
67
|
-
}, 500); // Show 100% for half a second
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Track pathname changes (App Router equivalent of routeChangeStart/Complete)
|
|
71
|
-
useEffect(() => {
|
|
72
|
-
// Skip on initial mount
|
|
73
|
-
if (prevPathname.current === null) {
|
|
74
|
-
prevPathname.current = pathname;
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Only trigger if pathname actually changed
|
|
79
|
-
if (prevPathname.current !== pathname) {
|
|
80
|
-
setLoading(true);
|
|
81
|
-
startFakeProgress();
|
|
82
|
-
|
|
83
|
-
// Complete progress after a short delay (simulating route change)
|
|
84
|
-
const timeout = setTimeout(() => {
|
|
85
|
-
completeProgress();
|
|
86
|
-
prevPathname.current = pathname;
|
|
87
|
-
}, 100);
|
|
88
|
-
|
|
89
|
-
return () => {
|
|
90
|
-
clearTimeout(timeout);
|
|
91
|
-
if (progressTimer.current) {
|
|
92
|
-
clearInterval(progressTimer.current);
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
}, [pathname]);
|
|
97
|
-
|
|
98
|
-
if (!mounted) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<div
|
|
104
|
-
data-page-progress="root"
|
|
105
|
-
data-loading={loading}
|
|
106
|
-
data-progress={progress}
|
|
107
|
-
className={`fixed top-0 left-0 w-full transition-opacity duration-300 ${
|
|
108
|
-
loading ? 'opacity-100' : 'opacity-0'
|
|
109
|
-
}`}
|
|
110
|
-
style={{
|
|
111
|
-
zIndex: 99999,
|
|
112
|
-
height: '3px',
|
|
113
|
-
}}
|
|
114
|
-
>
|
|
115
|
-
<div
|
|
116
|
-
className="h-full transition-all duration-200 ease-linear"
|
|
117
|
-
style={{
|
|
118
|
-
width: `${progress}%`,
|
|
119
|
-
background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 50%, #3b82f6 100%)',
|
|
120
|
-
boxShadow: '0 0 10px rgba(59, 130, 246, 0.6), 0 0 20px rgba(59, 130, 246, 0.4), 0 0 30px rgba(59, 130, 246, 0.2)',
|
|
121
|
-
filter: 'drop-shadow(0 0 8px rgba(59, 130, 246, 0.8))',
|
|
122
|
-
}}
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|