@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.48",
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.48",
96
- "@djangocfg/centrifugo": "^2.1.48",
97
- "@djangocfg/ui-nextjs": "^2.1.48",
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.48",
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
- formatCORSErrorForClipboard, formatNetworkErrorForClipboard, formatValidationErrorForClipboard
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 network errors
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
- DEFAULT_CORS_CONFIG, DEFAULT_NETWORK_CONFIG, DEFAULT_VALIDATION_CONFIG, ERROR_EVENTS
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
- <PageProgress />
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
-