@djangocfg/layouts 1.2.24 → 1.2.25
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 +15 -5
- package/src/auth/context/AccountsContext.tsx +209 -0
- package/src/auth/context/AuthContext.tsx +18 -5
- package/src/auth/context/index.ts +3 -1
- package/src/auth/hooks/index.ts +9 -1
- package/src/auth/hooks/useProfileCache.ts +141 -0
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
- package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +29 -7
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +1 -1
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +1 -1
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +1 -1
- package/src/layouts/UILayout/core/UIGuideApp.client.tsx +18 -0
- package/src/layouts/UILayout/core/UIGuideApp.tsx +23 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.25",
|
|
4
4
|
"description": "Layout system and components for Unrealon applications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DjangoCFG",
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
"import": "./src/auth/index.ts",
|
|
21
21
|
"default": "./src/auth/index.ts"
|
|
22
22
|
},
|
|
23
|
+
"./auth/context": {
|
|
24
|
+
"types": "./src/auth/context/index.ts",
|
|
25
|
+
"import": "./src/auth/context/index.ts",
|
|
26
|
+
"default": "./src/auth/context/index.ts"
|
|
27
|
+
},
|
|
28
|
+
"./auth/hooks": {
|
|
29
|
+
"types": "./src/auth/hooks/index.ts",
|
|
30
|
+
"import": "./src/auth/hooks/index.ts",
|
|
31
|
+
"default": "./src/auth/hooks/index.ts"
|
|
32
|
+
},
|
|
23
33
|
"./layouts": {
|
|
24
34
|
"types": "./src/layouts/index.ts",
|
|
25
35
|
"import": "./src/layouts/index.ts",
|
|
@@ -53,9 +63,9 @@
|
|
|
53
63
|
"check": "tsc --noEmit"
|
|
54
64
|
},
|
|
55
65
|
"peerDependencies": {
|
|
56
|
-
"@djangocfg/api": "^1.2.
|
|
57
|
-
"@djangocfg/og-image": "^1.2.
|
|
58
|
-
"@djangocfg/ui": "^1.2.
|
|
66
|
+
"@djangocfg/api": "^1.2.25",
|
|
67
|
+
"@djangocfg/og-image": "^1.2.25",
|
|
68
|
+
"@djangocfg/ui": "^1.2.25",
|
|
59
69
|
"@hookform/resolvers": "^5.2.0",
|
|
60
70
|
"consola": "^3.4.2",
|
|
61
71
|
"lucide-react": "^0.468.0",
|
|
@@ -76,7 +86,7 @@
|
|
|
76
86
|
"vidstack": "0.6.15"
|
|
77
87
|
},
|
|
78
88
|
"devDependencies": {
|
|
79
|
-
"@djangocfg/typescript-config": "^1.2.
|
|
89
|
+
"@djangocfg/typescript-config": "^1.2.25",
|
|
80
90
|
"@types/node": "^24.7.2",
|
|
81
91
|
"@types/react": "19.2.2",
|
|
82
92
|
"@types/react-dom": "19.2.1",
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accounts Context
|
|
3
|
+
*
|
|
4
|
+
* Manages user authentication and profile operations using generated SWR hooks
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - OTP-based authentication
|
|
8
|
+
* - User profile management
|
|
9
|
+
* - Avatar upload
|
|
10
|
+
* - Profile updates
|
|
11
|
+
* - localStorage cache with TTL (1 hour)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
"use client";
|
|
15
|
+
|
|
16
|
+
import { createContext, useContext, ReactNode, useState, useCallback } from 'react';
|
|
17
|
+
import { getCachedProfile, setCachedProfile, clearProfileCache } from '../hooks/useProfileCache';
|
|
18
|
+
import {
|
|
19
|
+
api,
|
|
20
|
+
usePartialUpdateAccountsProfilePartialUpdate,
|
|
21
|
+
useUpdateAccountsProfileUpdateUpdate,
|
|
22
|
+
useCreateAccountsProfileAvatarCreate,
|
|
23
|
+
useCreateAccountsOtpRequestCreate,
|
|
24
|
+
useCreateAccountsOtpVerifyCreate,
|
|
25
|
+
useCreateAccountsTokenRefreshCreate,
|
|
26
|
+
getAccountsProfileRetrieve,
|
|
27
|
+
PatchedUserProfileUpdateRequestSchema,
|
|
28
|
+
type User,
|
|
29
|
+
type UserProfileUpdateRequest,
|
|
30
|
+
type PatchedUserProfileUpdateRequest,
|
|
31
|
+
type OTPRequestRequest,
|
|
32
|
+
type OTPVerifyRequest,
|
|
33
|
+
type OTPRequestResponse,
|
|
34
|
+
type OTPVerifyResponse,
|
|
35
|
+
type TokenRefresh,
|
|
36
|
+
type API,
|
|
37
|
+
} from '@djangocfg/api';
|
|
38
|
+
|
|
39
|
+
// Re-export schemas for external use
|
|
40
|
+
export { PatchedUserProfileUpdateRequestSchema };
|
|
41
|
+
export type { PatchedUserProfileUpdateRequest };
|
|
42
|
+
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// Context Type
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export interface AccountsContextValue {
|
|
48
|
+
// Current user profile
|
|
49
|
+
profile?: User;
|
|
50
|
+
isLoadingProfile: boolean;
|
|
51
|
+
profileError: Error | null;
|
|
52
|
+
|
|
53
|
+
// Profile operations
|
|
54
|
+
updateProfile: (data: UserProfileUpdateRequest) => Promise<User>;
|
|
55
|
+
partialUpdateProfile: (data: PatchedUserProfileUpdateRequest) => Promise<User>;
|
|
56
|
+
uploadAvatar: (formData: FormData) => Promise<User>;
|
|
57
|
+
refreshProfile: () => Promise<User | undefined>;
|
|
58
|
+
|
|
59
|
+
// Authentication
|
|
60
|
+
requestOTP: (data: OTPRequestRequest) => Promise<OTPRequestResponse>;
|
|
61
|
+
verifyOTP: (data: OTPVerifyRequest) => Promise<OTPVerifyResponse>;
|
|
62
|
+
refreshToken: (refresh: string) => Promise<TokenRefresh>;
|
|
63
|
+
logout: () => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
67
|
+
// Context
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
const AccountsContext = createContext<AccountsContextValue | undefined>(undefined);
|
|
71
|
+
|
|
72
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
73
|
+
// Provider Component
|
|
74
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
interface AccountsProviderProps {
|
|
77
|
+
children: ReactNode;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
81
|
+
// State management with localStorage cache
|
|
82
|
+
const [profile, setProfile] = useState<User | undefined>(() => {
|
|
83
|
+
// Initialize from cache on mount
|
|
84
|
+
const cached = getCachedProfile();
|
|
85
|
+
return cached || undefined;
|
|
86
|
+
});
|
|
87
|
+
const [isLoadingProfile, setIsLoadingProfile] = useState(false);
|
|
88
|
+
const [profileError, setProfileError] = useState<Error | null>(null);
|
|
89
|
+
|
|
90
|
+
// Mutation hooks
|
|
91
|
+
const updateMutation = useUpdateAccountsProfileUpdateUpdate();
|
|
92
|
+
const partialUpdateMutation = usePartialUpdateAccountsProfilePartialUpdate();
|
|
93
|
+
const avatarMutation = useCreateAccountsProfileAvatarCreate();
|
|
94
|
+
const otpRequestMutation = useCreateAccountsOtpRequestCreate();
|
|
95
|
+
const otpVerifyMutation = useCreateAccountsOtpVerifyCreate();
|
|
96
|
+
const tokenRefreshMutation = useCreateAccountsTokenRefreshCreate();
|
|
97
|
+
|
|
98
|
+
// Refresh profile - fetch and cache
|
|
99
|
+
const refreshProfile = useCallback(async (): Promise<User | undefined> => {
|
|
100
|
+
setIsLoadingProfile(true);
|
|
101
|
+
setProfileError(null);
|
|
102
|
+
try {
|
|
103
|
+
const result = await getAccountsProfileRetrieve(api as unknown as API);
|
|
104
|
+
setProfile(result);
|
|
105
|
+
// Save to cache with 1 hour TTL
|
|
106
|
+
setCachedProfile(result);
|
|
107
|
+
return result;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const err = error instanceof Error ? error : new Error('Failed to fetch profile');
|
|
110
|
+
setProfileError(err);
|
|
111
|
+
throw err;
|
|
112
|
+
} finally {
|
|
113
|
+
setIsLoadingProfile(false);
|
|
114
|
+
}
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
// Update profile (full)
|
|
118
|
+
const updateProfile = async (data: UserProfileUpdateRequest): Promise<User> => {
|
|
119
|
+
const result = await updateMutation(data, api as unknown as API);
|
|
120
|
+
await refreshProfile();
|
|
121
|
+
return result as User;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Partial update profile
|
|
125
|
+
const partialUpdateProfile = async (data: PatchedUserProfileUpdateRequest): Promise<User> => {
|
|
126
|
+
const result = await partialUpdateMutation(data, api as unknown as API);
|
|
127
|
+
await refreshProfile();
|
|
128
|
+
return result as User;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Upload avatar
|
|
132
|
+
const uploadAvatar = async (formData: FormData): Promise<User> => {
|
|
133
|
+
const result = await avatarMutation(formData, api as unknown as API);
|
|
134
|
+
await refreshProfile();
|
|
135
|
+
return result as User;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Request OTP
|
|
139
|
+
const requestOTP = async (data: OTPRequestRequest): Promise<OTPRequestResponse> => {
|
|
140
|
+
const result = await otpRequestMutation(data, api as unknown as API);
|
|
141
|
+
return result as OTPRequestResponse;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Verify OTP
|
|
145
|
+
const verifyOTP = async (data: OTPVerifyRequest): Promise<OTPVerifyResponse> => {
|
|
146
|
+
const result = await otpVerifyMutation(data, api as unknown as API);
|
|
147
|
+
|
|
148
|
+
// Automatically save tokens after successful verification
|
|
149
|
+
if (result.access && result.refresh) {
|
|
150
|
+
api.setToken(result.access, result.refresh);
|
|
151
|
+
// Refresh profile to load user data with new token
|
|
152
|
+
await refreshProfile();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result as OTPVerifyResponse;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Refresh token
|
|
159
|
+
const refreshToken = async (refresh: string): Promise<TokenRefresh> => {
|
|
160
|
+
const result = await tokenRefreshMutation({ refresh }, api as unknown as API);
|
|
161
|
+
|
|
162
|
+
// Automatically save new access token
|
|
163
|
+
if (result.access) {
|
|
164
|
+
api.setToken(result.access, refresh);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result as TokenRefresh;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Logout - clear tokens, profile state, and cache
|
|
171
|
+
const logout = useCallback(() => {
|
|
172
|
+
api.clearTokens();
|
|
173
|
+
setProfile(undefined);
|
|
174
|
+
setProfileError(null);
|
|
175
|
+
clearProfileCache();
|
|
176
|
+
}, []);
|
|
177
|
+
|
|
178
|
+
const value: AccountsContextValue = {
|
|
179
|
+
profile,
|
|
180
|
+
isLoadingProfile,
|
|
181
|
+
profileError,
|
|
182
|
+
updateProfile,
|
|
183
|
+
partialUpdateProfile,
|
|
184
|
+
uploadAvatar,
|
|
185
|
+
refreshProfile,
|
|
186
|
+
requestOTP,
|
|
187
|
+
verifyOTP,
|
|
188
|
+
refreshToken,
|
|
189
|
+
logout,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<AccountsContext.Provider value={value}>
|
|
194
|
+
{children}
|
|
195
|
+
</AccountsContext.Provider>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
200
|
+
// Hook
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
export function useAccountsContext(): AccountsContextValue {
|
|
204
|
+
const context = useContext(AccountsContext);
|
|
205
|
+
if (!context) {
|
|
206
|
+
throw new Error('useAccountsContext must be used within AccountsProvider');
|
|
207
|
+
}
|
|
208
|
+
return context;
|
|
209
|
+
}
|
|
@@ -4,8 +4,9 @@ import React, {
|
|
|
4
4
|
} from 'react';
|
|
5
5
|
|
|
6
6
|
import { api, Enums } from '@djangocfg/api';
|
|
7
|
-
import { useAccountsContext, AccountsProvider } from '
|
|
7
|
+
import { useAccountsContext, AccountsProvider } from './AccountsContext';
|
|
8
8
|
import { useLocalStorage } from '@djangocfg/ui/hooks';
|
|
9
|
+
import { getCachedProfile, clearProfileCache } from '../hooks/useProfileCache';
|
|
9
10
|
|
|
10
11
|
import { authLogger } from '../../utils/logger';
|
|
11
12
|
import type { AuthConfig, AuthContextType, AuthProviderProps, UserProfile } from './types';
|
|
@@ -74,6 +75,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
74
75
|
const clearAuthState = useCallback((caller: string) => {
|
|
75
76
|
authLogger.info('clearAuthState >> caller', caller);
|
|
76
77
|
api.clearTokens();
|
|
78
|
+
clearProfileCache(); // Clear profile cache from localStorage
|
|
77
79
|
// Note: user is now managed by AccountsContext, will auto-update
|
|
78
80
|
setInitialized(true);
|
|
79
81
|
setIsLoading(false);
|
|
@@ -148,9 +150,18 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
148
150
|
const hasTokens = hasValidTokens();
|
|
149
151
|
authLogger.info('Has tokens:', hasTokens);
|
|
150
152
|
|
|
151
|
-
//
|
|
152
|
-
if (
|
|
153
|
-
authLogger.info('Profile already loaded from cache, skipping
|
|
153
|
+
// Check if profile is already loaded from cache (AccountsContext initialization)
|
|
154
|
+
if (userRef.current) {
|
|
155
|
+
authLogger.info('Profile already loaded from AccountsContext cache, skipping API request');
|
|
156
|
+
setInitialized(true);
|
|
157
|
+
setIsLoading(false);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check if cache exists in localStorage (before userRef updates)
|
|
162
|
+
const cachedProfile = getCachedProfile();
|
|
163
|
+
if (cachedProfile) {
|
|
164
|
+
authLogger.info('Profile found in localStorage cache, skipping API request');
|
|
154
165
|
setInitialized(true);
|
|
155
166
|
setIsLoading(false);
|
|
156
167
|
return;
|
|
@@ -159,6 +170,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
159
170
|
if (hasTokens) {
|
|
160
171
|
setIsLoading(true);
|
|
161
172
|
try {
|
|
173
|
+
authLogger.info('No cached profile found, loading from API...');
|
|
162
174
|
await loadCurrentProfile();
|
|
163
175
|
} catch (error) {
|
|
164
176
|
authLogger.error('Failed to load profile during initialization:', error);
|
|
@@ -173,7 +185,8 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
173
185
|
};
|
|
174
186
|
|
|
175
187
|
initializeAuth();
|
|
176
|
-
|
|
188
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
189
|
+
}, [initialized]);
|
|
177
190
|
|
|
178
191
|
// Redirect logic - only for unauthenticated users on protected pages
|
|
179
192
|
useEffect(() => {
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { AuthProvider, useAuth } from './AuthContext';
|
|
2
|
-
export type { AuthConfig, AuthContextType, AuthProviderProps, UserProfile } from './types';
|
|
2
|
+
export type { AuthConfig, AuthContextType, AuthProviderProps, UserProfile } from './types';
|
|
3
|
+
export { AccountsProvider, useAccountsContext, PatchedUserProfileUpdateRequestSchema } from './AccountsContext';
|
|
4
|
+
export type { AccountsContextValue, PatchedUserProfileUpdateRequest } from './AccountsContext';
|
package/src/auth/hooks/index.ts
CHANGED
|
@@ -3,4 +3,12 @@ export { useAuthGuard } from './useAuthGuard';
|
|
|
3
3
|
export { useSessionStorage } from './useSessionStorage';
|
|
4
4
|
export { useLocalStorage } from './useLocalStorage';
|
|
5
5
|
export { useAuthForm } from './useAuthForm';
|
|
6
|
-
export { useAutoAuth } from './useAutoAuth';
|
|
6
|
+
export { useAutoAuth } from './useAutoAuth';
|
|
7
|
+
export {
|
|
8
|
+
getCachedProfile,
|
|
9
|
+
setCachedProfile,
|
|
10
|
+
clearProfileCache,
|
|
11
|
+
hasValidCache,
|
|
12
|
+
getCacheMetadata,
|
|
13
|
+
type ProfileCacheOptions
|
|
14
|
+
} from './useProfileCache';
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile Cache Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides localStorage-based caching for user profile with:
|
|
5
|
+
* - TTL (Time To Live) - default 1 hour
|
|
6
|
+
* - Version control for migrations
|
|
7
|
+
* - Automatic invalidation on expiry
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { User } from '@djangocfg/api';
|
|
11
|
+
import { profileLogger } from '../../utils/logger';
|
|
12
|
+
|
|
13
|
+
// Cache configuration
|
|
14
|
+
const CACHE_KEY = 'user_profile_cache';
|
|
15
|
+
const CACHE_VERSION = 1;
|
|
16
|
+
const DEFAULT_TTL = 3600000; // 1 hour in milliseconds
|
|
17
|
+
|
|
18
|
+
export interface ProfileCacheOptions {
|
|
19
|
+
/** Time to live in milliseconds (default: 1 hour) */
|
|
20
|
+
ttl?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CachedProfile {
|
|
24
|
+
version: number;
|
|
25
|
+
data: User;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
ttl: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get cached profile from localStorage
|
|
32
|
+
* @returns User profile if valid cache exists, null otherwise
|
|
33
|
+
*/
|
|
34
|
+
export function getCachedProfile(): User | null {
|
|
35
|
+
try {
|
|
36
|
+
if (typeof window === 'undefined') return null;
|
|
37
|
+
|
|
38
|
+
const cached = localStorage.getItem(CACHE_KEY);
|
|
39
|
+
if (!cached) return null;
|
|
40
|
+
|
|
41
|
+
const cachedData: CachedProfile = JSON.parse(cached);
|
|
42
|
+
|
|
43
|
+
// Version check
|
|
44
|
+
if (cachedData.version !== CACHE_VERSION) {
|
|
45
|
+
profileLogger.warn('Cache version mismatch, clearing cache');
|
|
46
|
+
clearProfileCache();
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// TTL check
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const age = now - cachedData.timestamp;
|
|
53
|
+
if (age > cachedData.ttl) {
|
|
54
|
+
profileLogger.info('Cache expired, clearing');
|
|
55
|
+
clearProfileCache();
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
profileLogger.debug('Cache hit, age:', Math.round(age / 1000), 'seconds');
|
|
60
|
+
return cachedData.data;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
profileLogger.error('Error reading cache:', error);
|
|
63
|
+
clearProfileCache();
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Save profile to localStorage cache
|
|
70
|
+
* @param profile - User profile to cache
|
|
71
|
+
* @param options - Cache options (TTL)
|
|
72
|
+
*/
|
|
73
|
+
export function setCachedProfile(profile: User, options?: ProfileCacheOptions): void {
|
|
74
|
+
try {
|
|
75
|
+
if (typeof window === 'undefined') return;
|
|
76
|
+
|
|
77
|
+
const cachedData: CachedProfile = {
|
|
78
|
+
version: CACHE_VERSION,
|
|
79
|
+
data: profile,
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
ttl: options?.ttl || DEFAULT_TTL,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
localStorage.setItem(CACHE_KEY, JSON.stringify(cachedData));
|
|
85
|
+
profileLogger.debug('Profile cached, TTL:', cachedData.ttl / 1000, 'seconds');
|
|
86
|
+
} catch (error) {
|
|
87
|
+
profileLogger.error('Error writing cache:', error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clear profile cache from localStorage
|
|
93
|
+
*/
|
|
94
|
+
export function clearProfileCache(): void {
|
|
95
|
+
try {
|
|
96
|
+
if (typeof window === 'undefined') return;
|
|
97
|
+
localStorage.removeItem(CACHE_KEY);
|
|
98
|
+
profileLogger.debug('Cache cleared');
|
|
99
|
+
} catch (error) {
|
|
100
|
+
profileLogger.error('Error clearing cache:', error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if cached profile is valid (exists and not expired)
|
|
106
|
+
*/
|
|
107
|
+
export function hasValidCache(): boolean {
|
|
108
|
+
return getCachedProfile() !== null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get cache metadata (age, TTL, etc.)
|
|
113
|
+
*/
|
|
114
|
+
export function getCacheMetadata(): {
|
|
115
|
+
exists: boolean;
|
|
116
|
+
age?: number;
|
|
117
|
+
ttl?: number;
|
|
118
|
+
expiresIn?: number;
|
|
119
|
+
} | null {
|
|
120
|
+
try {
|
|
121
|
+
if (typeof window === 'undefined') return null;
|
|
122
|
+
|
|
123
|
+
const cached = localStorage.getItem(CACHE_KEY);
|
|
124
|
+
if (!cached) return { exists: false };
|
|
125
|
+
|
|
126
|
+
const cachedData: CachedProfile = JSON.parse(cached);
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
const age = now - cachedData.timestamp;
|
|
129
|
+
const expiresIn = cachedData.ttl - age;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
exists: true,
|
|
133
|
+
age,
|
|
134
|
+
ttl: cachedData.ttl,
|
|
135
|
+
expiresIn: Math.max(0, expiresIn),
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
profileLogger.error('Error reading metadata:', error);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -16,36 +16,36 @@ export interface PackageInfo {
|
|
|
16
16
|
/**
|
|
17
17
|
* Package versions registry
|
|
18
18
|
* Auto-synced from package.json files
|
|
19
|
-
* Last updated: 2025-11-
|
|
19
|
+
* Last updated: 2025-11-05T19:45:52.509Z
|
|
20
20
|
*/
|
|
21
21
|
const PACKAGE_VERSIONS: PackageInfo[] = [
|
|
22
22
|
{
|
|
23
23
|
"name": "@djangocfg/ui",
|
|
24
|
-
"version": "1.2.
|
|
24
|
+
"version": "1.2.25"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"name": "@djangocfg/api",
|
|
28
|
-
"version": "1.2.
|
|
28
|
+
"version": "1.2.25"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"name": "@djangocfg/layouts",
|
|
32
|
-
"version": "1.2.
|
|
32
|
+
"version": "1.2.25"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "@djangocfg/markdown",
|
|
36
|
-
"version": "1.2.
|
|
36
|
+
"version": "1.2.25"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "@djangocfg/og-image",
|
|
40
|
-
"version": "1.2.
|
|
40
|
+
"version": "1.2.25"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"name": "@djangocfg/eslint-config",
|
|
44
|
-
"version": "1.2.
|
|
44
|
+
"version": "1.2.25"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"name": "@djangocfg/typescript-config",
|
|
48
|
-
"version": "1.2.
|
|
48
|
+
"version": "1.2.25"
|
|
49
49
|
}
|
|
50
50
|
];
|
|
51
51
|
|
|
@@ -82,15 +82,42 @@ export function AdminLayout({
|
|
|
82
82
|
config,
|
|
83
83
|
enableParentSync = true
|
|
84
84
|
}: AdminLayoutProps) {
|
|
85
|
+
// Only run on client side
|
|
85
86
|
const [isMounted, setIsMounted] = React.useState(false);
|
|
86
|
-
const { user, isLoading, isAuthenticated, loadCurrentProfile } = useAuth();
|
|
87
|
-
// console.log('[AdminLayout] Rendering with user:', user, 'isLoading:', isLoading, 'isAuthenticated:', isAuthenticated);
|
|
88
87
|
|
|
89
88
|
// Track mount state to prevent hydration mismatch
|
|
90
89
|
React.useEffect(() => {
|
|
91
90
|
setIsMounted(true);
|
|
92
91
|
}, []);
|
|
93
92
|
|
|
93
|
+
// Minimalist loading component
|
|
94
|
+
const LoadingState = () => (
|
|
95
|
+
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
96
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
97
|
+
<div className="w-2 h-2 bg-current rounded-full animate-pulse" style={{ animationDelay: '0ms' }} />
|
|
98
|
+
<div className="w-2 h-2 bg-current rounded-full animate-pulse" style={{ animationDelay: '150ms' }} />
|
|
99
|
+
<div className="w-2 h-2 bg-current rounded-full animate-pulse" style={{ animationDelay: '300ms' }} />
|
|
100
|
+
<span className="ml-2">Loading...</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// During SSR and initial render, show loading to prevent hydration mismatch
|
|
106
|
+
if (!isMounted) {
|
|
107
|
+
return <LoadingState />;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return <AdminLayoutClient config={config} enableParentSync={enableParentSync}>{children}</AdminLayoutClient>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function AdminLayoutClient({
|
|
114
|
+
children,
|
|
115
|
+
config,
|
|
116
|
+
enableParentSync = true
|
|
117
|
+
}: AdminLayoutProps) {
|
|
118
|
+
const { user, isLoading, isAuthenticated, loadCurrentProfile } = useAuth();
|
|
119
|
+
// console.log('[AdminLayout] Rendering with user:', user, 'isLoading:', isLoading, 'isAuthenticated:', isAuthenticated);
|
|
120
|
+
|
|
94
121
|
// useCfgApp hook is called here to initialize iframe communication
|
|
95
122
|
// Automatically sets tokens in API client when received from parent
|
|
96
123
|
const { isEmbedded } = useCfgApp({
|
|
@@ -129,11 +156,6 @@ export function AdminLayout({
|
|
|
129
156
|
</div>
|
|
130
157
|
);
|
|
131
158
|
|
|
132
|
-
// During SSR and initial render, show loading to prevent hydration mismatch
|
|
133
|
-
if (!isMounted) {
|
|
134
|
-
return <LoadingState />;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
159
|
// Show loading while auth is initializing (waiting for tokens from parent or profile loading)
|
|
138
160
|
// OR if user object is not loaded yet (null/undefined)
|
|
139
161
|
// OR if authentication status is not yet determined
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
CardHeader,
|
|
10
10
|
CardTitle,
|
|
11
11
|
} from '@djangocfg/ui/components';
|
|
12
|
-
import { AccountsProvider } from '@djangocfg/
|
|
12
|
+
import { AccountsProvider } from '@djangocfg/layouts/auth/context';
|
|
13
13
|
import { useAuth } from '../../auth';
|
|
14
14
|
|
|
15
15
|
import { AvatarSection, ProfileForm } from './components';
|
|
@@ -5,7 +5,7 @@ import React, { useState } from 'react';
|
|
|
5
5
|
import { toast } from 'sonner';
|
|
6
6
|
|
|
7
7
|
import { Avatar, AvatarFallback, Button } from '@djangocfg/ui/components';
|
|
8
|
-
import { useAccountsContext } from '@djangocfg/
|
|
8
|
+
import { useAccountsContext } from '@djangocfg/layouts/auth/context';
|
|
9
9
|
import { useAuth } from '../../../auth';
|
|
10
10
|
|
|
11
11
|
export const AvatarSection = () => {
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
useAccountsContext,
|
|
23
23
|
PatchedUserProfileUpdateRequestSchema,
|
|
24
24
|
type PatchedUserProfileUpdateRequest
|
|
25
|
-
} from '@djangocfg/
|
|
25
|
+
} from '@djangocfg/layouts/auth/context';
|
|
26
26
|
import { useAuth } from '../../../auth';
|
|
27
27
|
|
|
28
28
|
export const ProfileForm = () => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI Guide App
|
|
3
|
+
*
|
|
4
|
+
* Complete UI Guide application with UILayout
|
|
5
|
+
* Uses config-driven approach with context for navigation
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import UIGuideView from './UIGuideView';
|
|
12
|
+
|
|
13
|
+
export function UIGuideApp() {
|
|
14
|
+
// UIGuideView now includes UIGuideLanding as 'overview' category
|
|
15
|
+
// and uses ShowcaseProvider context for navigation
|
|
16
|
+
// All component data comes from centralized config
|
|
17
|
+
return <UIGuideView />;
|
|
18
|
+
}
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* UI Guide App
|
|
2
|
+
* UI Guide App - Dynamic Import Wrapper
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Lazy loads the heavy UILayout library (~1.5MB with Mermaid, PrettyCode, etc.)
|
|
5
|
+
* Only loads when UI documentation page is accessed
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
9
9
|
|
|
10
|
+
import dynamic from 'next/dynamic';
|
|
10
11
|
import React from 'react';
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
// Dynamic import with loading state
|
|
14
|
+
const UIGuideAppClient = dynamic(() => import('./UIGuideApp.client').then(mod => ({ default: mod.UIGuideApp })), {
|
|
15
|
+
ssr: false,
|
|
16
|
+
loading: () => (
|
|
17
|
+
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
18
|
+
<div className="text-center space-y-4">
|
|
19
|
+
<div className="flex justify-center">
|
|
20
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="space-y-2">
|
|
23
|
+
<h2 className="text-xl font-semibold text-foreground">Loading UI Guide</h2>
|
|
24
|
+
<p className="text-sm text-muted-foreground">Preparing component showcase...</p>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
),
|
|
29
|
+
});
|
|
12
30
|
|
|
13
31
|
export function UIGuideApp() {
|
|
14
|
-
|
|
15
|
-
// and uses ShowcaseProvider context for navigation
|
|
16
|
-
// All component data comes from centralized config
|
|
17
|
-
return <UIGuideView />;
|
|
32
|
+
return <UIGuideAppClient />;
|
|
18
33
|
}
|