@djangocfg/layouts 2.0.9 → 2.0.11
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 +5 -5
- package/src/components/core/ClientOnly.tsx +73 -0
- package/src/components/core/index.ts +2 -0
- package/src/components/errors/ErrorLayout.tsx +6 -7
- package/src/components/index.ts +1 -4
- package/src/layouts/AppLayout/AppLayout.tsx +25 -30
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +9 -21
- package/src/components/UpdateNotifier/UpdateNotifier.tsx +0 -178
- package/src/components/UpdateNotifier/index.ts +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -92,9 +92,9 @@
|
|
|
92
92
|
"check": "tsc --noEmit"
|
|
93
93
|
},
|
|
94
94
|
"peerDependencies": {
|
|
95
|
-
"@djangocfg/api": "^1.4.
|
|
96
|
-
"@djangocfg/centrifugo": "^1.4.
|
|
97
|
-
"@djangocfg/ui": "^1.4.
|
|
95
|
+
"@djangocfg/api": "^1.4.41",
|
|
96
|
+
"@djangocfg/centrifugo": "^1.4.41",
|
|
97
|
+
"@djangocfg/ui": "^1.4.41",
|
|
98
98
|
"@hookform/resolvers": "^5.2.0",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
100
|
"lucide-react": "^0.545.0",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"uuid": "^11.1.0"
|
|
115
115
|
},
|
|
116
116
|
"devDependencies": {
|
|
117
|
-
"@djangocfg/typescript-config": "^1.4.
|
|
117
|
+
"@djangocfg/typescript-config": "^1.4.41",
|
|
118
118
|
"@types/node": "^24.7.2",
|
|
119
119
|
"@types/react": "19.2.2",
|
|
120
120
|
"@types/react-dom": "19.2.1",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClientOnly Component
|
|
3
|
+
*
|
|
4
|
+
* Renders children only on the client side to prevent SSR hydration mismatch.
|
|
5
|
+
* Shows a fallback (loading state) during SSR and initial client mount.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { ClientOnly } from '@djangocfg/layouts/components';
|
|
10
|
+
*
|
|
11
|
+
* // With default loading fallback
|
|
12
|
+
* <ClientOnly>
|
|
13
|
+
* <ComponentThatUsesClientOnlyAPIs />
|
|
14
|
+
* </ClientOnly>
|
|
15
|
+
*
|
|
16
|
+
* // With custom fallback
|
|
17
|
+
* <ClientOnly fallback={<MyCustomLoader />}>
|
|
18
|
+
* <ComponentThatUsesClientOnlyAPIs />
|
|
19
|
+
* </ClientOnly>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use client';
|
|
24
|
+
|
|
25
|
+
import { useState, useEffect, ReactNode } from 'react';
|
|
26
|
+
import { Preloader } from '@djangocfg/ui/components';
|
|
27
|
+
|
|
28
|
+
export interface ClientOnlyProps {
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* Fallback to show during SSR and initial mount
|
|
32
|
+
* @default Preloader with "Loading..." text
|
|
33
|
+
*/
|
|
34
|
+
fallback?: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Default fallback - fullscreen preloader
|
|
39
|
+
*/
|
|
40
|
+
const defaultFallback = (
|
|
41
|
+
<Preloader
|
|
42
|
+
variant="fullscreen"
|
|
43
|
+
text="Loading..."
|
|
44
|
+
size="lg"
|
|
45
|
+
backdrop={true}
|
|
46
|
+
backdropOpacity={80}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* ClientOnly - Prevents SSR hydration mismatch
|
|
52
|
+
*
|
|
53
|
+
* Use this to wrap components that:
|
|
54
|
+
* - Access browser-only APIs (window, localStorage, etc.)
|
|
55
|
+
* - Have different initial state on server vs client
|
|
56
|
+
* - Use authentication state that differs between SSR and CSR
|
|
57
|
+
*/
|
|
58
|
+
export function ClientOnly({
|
|
59
|
+
children,
|
|
60
|
+
fallback = defaultFallback,
|
|
61
|
+
}: ClientOnlyProps) {
|
|
62
|
+
const [mounted, setMounted] = useState(false);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
setMounted(true);
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
if (!mounted) {
|
|
69
|
+
return <>{fallback}</>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return <>{children}</>;
|
|
73
|
+
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Core components exports
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
export { ClientOnly } from './ClientOnly';
|
|
6
|
+
export type { ClientOnlyProps } from './ClientOnly';
|
|
5
7
|
export { JsonLd } from './JsonLd';
|
|
6
8
|
export { LucideIcon } from './LucideIcon';
|
|
7
9
|
export type { LucideIconProps } from './LucideIcon';
|
|
@@ -73,13 +73,12 @@ function getErrorIcon(code?: string | number): React.ReactNode {
|
|
|
73
73
|
viewBox="0 0 24 24"
|
|
74
74
|
aria-hidden="true"
|
|
75
75
|
>
|
|
76
|
-
{/* Server Error Icon */}
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
/>
|
|
76
|
+
{/* Server Error Icon - Server with X */}
|
|
77
|
+
<rect x="2" y="3" width="20" height="7" rx="1" strokeWidth={1.5} />
|
|
78
|
+
<rect x="2" y="14" width="20" height="7" rx="1" strokeWidth={1.5} />
|
|
79
|
+
<circle cx="6" cy="6.5" r="1" fill="currentColor" />
|
|
80
|
+
<circle cx="6" cy="17.5" r="1" fill="currentColor" />
|
|
81
|
+
<path strokeLinecap="round" strokeWidth={1.5} d="M22 2L2 22" />
|
|
83
82
|
</svg>
|
|
84
83
|
);
|
|
85
84
|
case '403':
|
package/src/components/index.ts
CHANGED
|
@@ -46,8 +46,7 @@ import { AuthProvider, type AuthConfig } from '../../auth/context';
|
|
|
46
46
|
import { ErrorTrackingProvider, type ValidationErrorConfig, type CORSErrorConfig, type NetworkErrorConfig } from '../../components/errors/ErrorsTracker';
|
|
47
47
|
import { AnalyticsProvider } from '../../snippets/Analytics';
|
|
48
48
|
import { PageProgress } from '../../components/core/PageProgress';
|
|
49
|
-
import {
|
|
50
|
-
import { Suspense } from '../../components/core';
|
|
49
|
+
import { Suspense, ClientOnly } from '../../components/core';
|
|
51
50
|
|
|
52
51
|
export type LayoutMode = 'public' | 'private' | 'admin';
|
|
53
52
|
|
|
@@ -139,31 +138,25 @@ export interface AppLayoutProps {
|
|
|
139
138
|
supportEmail?: string;
|
|
140
139
|
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
141
140
|
};
|
|
142
|
-
|
|
143
|
-
/** Update notifier configuration */
|
|
144
|
-
updateNotifier?: {
|
|
145
|
-
enabled?: boolean;
|
|
146
|
-
};
|
|
147
141
|
}
|
|
148
142
|
|
|
149
143
|
/**
|
|
150
144
|
* AppLayout Content - Renders layout with all providers
|
|
145
|
+
*
|
|
146
|
+
* SSR is only enabled for publicLayout.
|
|
147
|
+
* Private and admin layouts are wrapped in ClientOnly to avoid hydration mismatch.
|
|
151
148
|
*/
|
|
152
149
|
function AppLayoutContent({
|
|
153
150
|
children,
|
|
154
151
|
publicLayout,
|
|
155
152
|
privateLayout,
|
|
156
153
|
adminLayout,
|
|
157
|
-
theme,
|
|
158
|
-
auth,
|
|
159
154
|
analytics,
|
|
160
155
|
centrifugo,
|
|
161
|
-
errorTracking,
|
|
162
156
|
errorBoundary,
|
|
163
|
-
updateNotifier,
|
|
164
157
|
}: AppLayoutProps) {
|
|
165
158
|
const pathname = usePathname();
|
|
166
|
-
|
|
159
|
+
|
|
167
160
|
const layoutMode = useMemo(
|
|
168
161
|
() => determineLayoutMode(
|
|
169
162
|
pathname,
|
|
@@ -173,46 +166,51 @@ function AppLayoutContent({
|
|
|
173
166
|
),
|
|
174
167
|
[pathname, adminLayout, privateLayout, publicLayout]
|
|
175
168
|
);
|
|
176
|
-
|
|
169
|
+
|
|
177
170
|
const enableErrorBoundary = errorBoundary?.enabled !== false;
|
|
178
|
-
|
|
171
|
+
|
|
179
172
|
// Render appropriate layout based on mode
|
|
180
173
|
const renderLayout = () => {
|
|
181
174
|
switch (layoutMode) {
|
|
182
175
|
case 'admin':
|
|
183
176
|
if (!adminLayout && privateLayout) {
|
|
184
|
-
// Fallback to private layout if no admin layout provided
|
|
185
177
|
return (
|
|
186
|
-
<
|
|
187
|
-
<
|
|
188
|
-
|
|
178
|
+
<ClientOnly>
|
|
179
|
+
<Suspense>
|
|
180
|
+
<privateLayout.component>{children}</privateLayout.component>
|
|
181
|
+
</Suspense>
|
|
182
|
+
</ClientOnly>
|
|
189
183
|
);
|
|
190
184
|
}
|
|
191
185
|
if (!adminLayout) {
|
|
192
186
|
return children;
|
|
193
187
|
}
|
|
194
188
|
return (
|
|
195
|
-
<
|
|
196
|
-
<
|
|
197
|
-
|
|
189
|
+
<ClientOnly>
|
|
190
|
+
<Suspense>
|
|
191
|
+
<adminLayout.component>{children}</adminLayout.component>
|
|
192
|
+
</Suspense>
|
|
193
|
+
</ClientOnly>
|
|
198
194
|
);
|
|
199
|
-
|
|
195
|
+
|
|
200
196
|
case 'private':
|
|
201
197
|
if (!privateLayout) {
|
|
202
|
-
// Fallback to public if no private layout provided
|
|
203
198
|
if (publicLayout) {
|
|
204
199
|
return <publicLayout.component>{children}</publicLayout.component>;
|
|
205
200
|
}
|
|
206
201
|
return children;
|
|
207
202
|
}
|
|
208
203
|
return (
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
|
|
204
|
+
<ClientOnly>
|
|
205
|
+
<Suspense>
|
|
206
|
+
<privateLayout.component>{children}</privateLayout.component>
|
|
207
|
+
</Suspense>
|
|
208
|
+
</ClientOnly>
|
|
212
209
|
);
|
|
213
|
-
|
|
210
|
+
|
|
214
211
|
case 'public':
|
|
215
212
|
default:
|
|
213
|
+
// Public layout renders with SSR (no ClientOnly wrapper)
|
|
216
214
|
if (!publicLayout) {
|
|
217
215
|
return children;
|
|
218
216
|
}
|
|
@@ -252,9 +250,6 @@ function AppLayoutContent({
|
|
|
252
250
|
<>
|
|
253
251
|
{content}
|
|
254
252
|
<PageProgress />
|
|
255
|
-
<UpdateNotifier
|
|
256
|
-
enabled={updateNotifier?.enabled !== false}
|
|
257
|
-
/>
|
|
258
253
|
<Toaster />
|
|
259
254
|
</>
|
|
260
255
|
);
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Private Layout
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Layout for authenticated user pages (dashboard, profile, etc.)
|
|
5
5
|
* Import and use directly with props - no complex configs needed!
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Features:
|
|
8
8
|
* - Responsive sidebar with mobile burger menu
|
|
9
9
|
* - Keyboard shortcut (Ctrl/Cmd + B) to toggle sidebar
|
|
10
10
|
* - Header with sidebar trigger and user menu
|
|
11
11
|
* - Configurable content padding
|
|
12
|
-
*
|
|
12
|
+
* - NO SSR - renders only on client to avoid hydration mismatch
|
|
13
|
+
*
|
|
13
14
|
* @example
|
|
14
15
|
* ```tsx
|
|
15
16
|
* import { PrivateLayout } from '@djangocfg/layouts';
|
|
16
|
-
*
|
|
17
|
+
*
|
|
17
18
|
* <PrivateLayout
|
|
18
19
|
* sidebar={{
|
|
19
20
|
* items: [
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
* >
|
|
29
30
|
* {children}
|
|
30
31
|
* </PrivateLayout>
|
|
31
|
-
*
|
|
32
|
+
*
|
|
32
33
|
* Note: User data (name, email, avatar) is automatically loaded from useAuth() context
|
|
33
34
|
* Keyboard shortcut: Ctrl/Cmd + B to toggle sidebar
|
|
34
35
|
* ```
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
'use client';
|
|
38
39
|
|
|
39
40
|
import React, { ReactNode } from 'react';
|
|
40
|
-
import { SidebarProvider, SidebarInset, Preloader,
|
|
41
|
+
import { SidebarProvider, SidebarInset, Preloader, ButtonLink } from '@djangocfg/ui/components';
|
|
41
42
|
import { useAuth } from '../../auth';
|
|
42
43
|
import { PrivateSidebar, PrivateHeader, PrivateContent } from './components';
|
|
43
44
|
import type { LucideIcon as LucideIconType } from 'lucide-react';
|
|
@@ -76,20 +77,10 @@ export function PrivateLayout({
|
|
|
76
77
|
header,
|
|
77
78
|
contentPadding = 'default',
|
|
78
79
|
}: PrivateLayoutProps) {
|
|
79
|
-
const { isAuthenticated, isLoading
|
|
80
|
-
|
|
81
|
-
// Debug logging in development
|
|
82
|
-
if (process.env.NODE_ENV === 'development') {
|
|
83
|
-
console.log('[PrivateLayout] Render state:', {
|
|
84
|
-
isLoading,
|
|
85
|
-
isAuthenticated,
|
|
86
|
-
hasUser: !!user,
|
|
87
|
-
hasSidebar: !!sidebar,
|
|
88
|
-
hasHeader: !!header,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
80
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
91
81
|
|
|
92
82
|
// Show loading state while auth is being checked
|
|
83
|
+
// Note: SSR hydration is handled by ClientOnly wrapper in AppLayout
|
|
93
84
|
if (isLoading) {
|
|
94
85
|
return (
|
|
95
86
|
<Preloader
|
|
@@ -104,9 +95,6 @@ export function PrivateLayout({
|
|
|
104
95
|
|
|
105
96
|
// Don't render if user is not authenticated
|
|
106
97
|
if (!isAuthenticated) {
|
|
107
|
-
if (process.env.NODE_ENV === 'development') {
|
|
108
|
-
console.warn('[PrivateLayout] User not authenticated, returning null');
|
|
109
|
-
}
|
|
110
98
|
return (
|
|
111
99
|
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
|
|
112
100
|
<h3 className="text-2xl font-bold">
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Update Notifier Component
|
|
3
|
-
*
|
|
4
|
-
* Checks npm registry for @djangocfg package updates
|
|
5
|
-
* Shows toast notification when new version is available
|
|
6
|
-
* Uses localStorage to cache check and avoid spam
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
'use client';
|
|
10
|
-
|
|
11
|
-
import React, { useEffect, useState, useRef } from 'react';
|
|
12
|
-
import consola from 'consola';
|
|
13
|
-
import { toast } from '@djangocfg/ui/hooks';
|
|
14
|
-
import { useLocalStorage } from '@djangocfg/ui/hooks';
|
|
15
|
-
|
|
16
|
-
const PACKAGE_NAME = '@djangocfg/layouts';
|
|
17
|
-
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
18
|
-
const CACHE_KEY = 'djangocfg_update_check';
|
|
19
|
-
|
|
20
|
-
interface UpdateCheckCache {
|
|
21
|
-
lastCheck: number;
|
|
22
|
-
latestVersion: string;
|
|
23
|
-
dismissed: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const defaultCache: UpdateCheckCache = {
|
|
27
|
-
lastCheck: 0,
|
|
28
|
-
latestVersion: '',
|
|
29
|
-
dismissed: false,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export interface UpdateNotifierProps {
|
|
33
|
-
/**
|
|
34
|
-
* Enable update notifications
|
|
35
|
-
* @default false
|
|
36
|
-
*/
|
|
37
|
-
enabled?: boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Get current package version from package.json
|
|
42
|
-
* Uses require in runtime to avoid TypeScript rootDir issues
|
|
43
|
-
*/
|
|
44
|
-
function getCurrentVersion(): string | null {
|
|
45
|
-
try {
|
|
46
|
-
// Use require in runtime (works in both dev and build)
|
|
47
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
|
-
const packageJson = require('../../../package.json');
|
|
49
|
-
return packageJson.version || null;
|
|
50
|
-
} catch (error) {
|
|
51
|
-
consola.warn('[UpdateNotifier] Failed to load package.json:', error);
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Compare semver versions
|
|
58
|
-
* Returns true if newVersion > currentVersion
|
|
59
|
-
*/
|
|
60
|
-
function isNewerVersion(current: string, latest: string): boolean {
|
|
61
|
-
const parseCurrent = current.split('.').map(Number);
|
|
62
|
-
const parseLatest = latest.split('.').map(Number);
|
|
63
|
-
|
|
64
|
-
for (let i = 0; i < 3; i++) {
|
|
65
|
-
if (parseLatest[i] > parseCurrent[i]) return true;
|
|
66
|
-
if (parseLatest[i] < parseCurrent[i]) return false;
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Fetch latest version from npm registry
|
|
73
|
-
*/
|
|
74
|
-
async function fetchLatestVersion(): Promise<string | null> {
|
|
75
|
-
try {
|
|
76
|
-
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
77
|
-
method: 'GET',
|
|
78
|
-
headers: { 'Accept': 'application/json' },
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
if (!response.ok) return null;
|
|
82
|
-
|
|
83
|
-
const data = await response.json();
|
|
84
|
-
return data.version || null;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
consola.warn('[UpdateNotifier] Failed to check for updates:', error);
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function UpdateNotifier({ enabled = false }: UpdateNotifierProps) {
|
|
92
|
-
const [checked, setChecked] = useState(false);
|
|
93
|
-
const [cache, setCache] = useLocalStorage<UpdateCheckCache | null>(CACHE_KEY, null);
|
|
94
|
-
const cacheRef = useRef(cache);
|
|
95
|
-
|
|
96
|
-
// Keep cacheRef in sync with cache
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
cacheRef.current = cache;
|
|
99
|
-
}, [cache]);
|
|
100
|
-
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (!enabled || checked || typeof window === 'undefined') return;
|
|
103
|
-
|
|
104
|
-
const checkForUpdates = async () => {
|
|
105
|
-
// Get current version from package.json
|
|
106
|
-
const currentVersion = getCurrentVersion();
|
|
107
|
-
if (!currentVersion) {
|
|
108
|
-
setChecked(true);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Check cache first (use ref to get latest value)
|
|
113
|
-
const now = Date.now();
|
|
114
|
-
const cachedData = cacheRef.current || defaultCache;
|
|
115
|
-
|
|
116
|
-
// If we checked recently, skip
|
|
117
|
-
if (cachedData && cachedData.lastCheck > 0 && (now - cachedData.lastCheck) < CHECK_INTERVAL_MS) {
|
|
118
|
-
// Show notification if there's an update and it wasn't dismissed
|
|
119
|
-
if (cachedData.latestVersion && !cachedData.dismissed && isNewerVersion(currentVersion, cachedData.latestVersion)) {
|
|
120
|
-
showUpdateNotification(currentVersion, cachedData.latestVersion, setCache);
|
|
121
|
-
}
|
|
122
|
-
setChecked(true);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Fetch latest version from npm
|
|
127
|
-
const latestVersion = await fetchLatestVersion();
|
|
128
|
-
|
|
129
|
-
if (!latestVersion) {
|
|
130
|
-
setChecked(true);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Update cache
|
|
135
|
-
setCache({
|
|
136
|
-
lastCheck: now,
|
|
137
|
-
latestVersion,
|
|
138
|
-
dismissed: false,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Show notification if newer version available
|
|
142
|
-
if (isNewerVersion(currentVersion, latestVersion)) {
|
|
143
|
-
showUpdateNotification(currentVersion, latestVersion, setCache);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
setChecked(true);
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
// Check after a short delay to not block initial render
|
|
150
|
-
const timer = setTimeout(checkForUpdates, 2000);
|
|
151
|
-
|
|
152
|
-
return () => clearTimeout(timer);
|
|
153
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
154
|
-
}, [enabled, checked]);
|
|
155
|
-
|
|
156
|
-
return null; // This component doesn't render anything
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Show update notification toast
|
|
161
|
-
*/
|
|
162
|
-
function showUpdateNotification(
|
|
163
|
-
currentVersion: string,
|
|
164
|
-
latestVersion: string,
|
|
165
|
-
setCache: (value: UpdateCheckCache | null | ((val: UpdateCheckCache | null) => UpdateCheckCache | null)) => void
|
|
166
|
-
) {
|
|
167
|
-
toast({
|
|
168
|
-
title: `📦 Update Available`,
|
|
169
|
-
description: `New version ${latestVersion} of @djangocfg packages is available. You're using ${currentVersion}. Run: pnpm update @djangocfg/layouts@latest`,
|
|
170
|
-
duration: 10000,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Mark as dismissed in cache after showing
|
|
174
|
-
setCache((prev) => {
|
|
175
|
-
if (!prev) return null;
|
|
176
|
-
return { ...prev, dismissed: true };
|
|
177
|
-
});
|
|
178
|
-
}
|