@djangocfg/layouts 1.2.30 → 1.2.32
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/auth/context/AccountsContext.tsx +40 -9
- package/src/auth/context/AuthContext.tsx +28 -12
- package/src/auth/context/types.ts +1 -1
- package/src/auth/hooks/useAuthForm.ts +2 -1
- package/src/auth/hooks/useAutoAuth.ts +2 -1
- package/src/auth/hooks/useLocalStorage.ts +19 -18
- package/src/auth/hooks/useProfileCache.ts +4 -1
- package/src/auth/hooks/useSessionStorage.ts +19 -18
- package/src/layouts/AppLayout/components/ErrorBoundary.tsx +3 -2
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
- package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +30 -19
- package/src/layouts/AppLayout/layouts/AdminLayout/components/ParentSync.tsx +29 -18
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +34 -17
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +2 -1
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -1
- package/src/utils/logger.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.32",
|
|
4
4
|
"description": "Layout system and components for Unrealon applications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DjangoCFG",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"check": "tsc --noEmit"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@djangocfg/api": "^1.2.
|
|
67
|
-
"@djangocfg/og-image": "^1.2.
|
|
68
|
-
"@djangocfg/ui": "^1.2.
|
|
66
|
+
"@djangocfg/api": "^1.2.32",
|
|
67
|
+
"@djangocfg/og-image": "^1.2.32",
|
|
68
|
+
"@djangocfg/ui": "^1.2.32",
|
|
69
69
|
"@hookform/resolvers": "^5.2.0",
|
|
70
70
|
"consola": "^3.4.2",
|
|
71
71
|
"lucide-react": "^0.468.0",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"vidstack": "0.6.15"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@djangocfg/typescript-config": "^1.2.
|
|
89
|
+
"@djangocfg/typescript-config": "^1.2.32",
|
|
90
90
|
"@types/node": "^24.7.2",
|
|
91
91
|
"@types/react": "19.2.2",
|
|
92
92
|
"@types/react-dom": "19.2.1",
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
"use client";
|
|
15
15
|
|
|
16
|
-
import { createContext, useContext, ReactNode, useState, useCallback } from 'react';
|
|
16
|
+
import { createContext, useContext, ReactNode, useState, useCallback, useRef, useEffect } from 'react';
|
|
17
17
|
import { getCachedProfile, setCachedProfile, clearProfileCache } from '../hooks/useProfileCache';
|
|
18
|
+
import { authLogger } from '../../utils/logger';
|
|
18
19
|
import {
|
|
19
20
|
api,
|
|
20
21
|
usePartialUpdateAccountsProfilePartialUpdate,
|
|
@@ -54,7 +55,7 @@ export interface AccountsContextValue {
|
|
|
54
55
|
updateProfile: (data: UserProfileUpdateRequest) => Promise<User>;
|
|
55
56
|
partialUpdateProfile: (data: PatchedUserProfileUpdateRequest) => Promise<User>;
|
|
56
57
|
uploadAvatar: (formData: FormData) => Promise<User>;
|
|
57
|
-
refreshProfile: () => Promise<User | undefined>;
|
|
58
|
+
refreshProfile: (callerId?: string) => Promise<User | undefined>;
|
|
58
59
|
|
|
59
60
|
// Authentication
|
|
60
61
|
requestOTP: (data: OTPRequestRequest) => Promise<OTPRequestResponse>;
|
|
@@ -87,6 +88,19 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
87
88
|
const [isLoadingProfile, setIsLoadingProfile] = useState(false);
|
|
88
89
|
const [profileError, setProfileError] = useState<Error | null>(null);
|
|
89
90
|
|
|
91
|
+
// Use refs to access current state without adding to dependencies
|
|
92
|
+
const profileRef = useRef<User | undefined>(profile);
|
|
93
|
+
const isLoadingRef = useRef(false);
|
|
94
|
+
|
|
95
|
+
// Keep refs in sync with state
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
profileRef.current = profile;
|
|
98
|
+
}, [profile]);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
isLoadingRef.current = isLoadingProfile;
|
|
102
|
+
}, [isLoadingProfile]);
|
|
103
|
+
|
|
90
104
|
// Mutation hooks
|
|
91
105
|
const updateMutation = useUpdateAccountsProfileUpdateUpdate();
|
|
92
106
|
const partialUpdateMutation = usePartialUpdateAccountsProfilePartialUpdate();
|
|
@@ -95,13 +109,29 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
95
109
|
const otpVerifyMutation = useCreateAccountsOtpVerifyCreate();
|
|
96
110
|
const tokenRefreshMutation = useCreateAccountsTokenRefreshCreate();
|
|
97
111
|
|
|
98
|
-
// Refresh profile - fetch and cache
|
|
99
|
-
const refreshProfile = useCallback(async (): Promise<User | undefined> => {
|
|
112
|
+
// Refresh profile - fetch and cache with stable callback
|
|
113
|
+
const refreshProfile = useCallback(async (callerId?: string): Promise<User | undefined> => {
|
|
114
|
+
// Use refs to check current state without creating dependency
|
|
115
|
+
const currentProfile = profileRef.current;
|
|
116
|
+
const currentLoading = isLoadingRef.current;
|
|
117
|
+
|
|
118
|
+
// Prevent duplicate calls if profile is already loaded and not loading
|
|
119
|
+
if (currentProfile && !currentLoading) {
|
|
120
|
+
authLogger.debug(`Profile already loaded, returning cached (caller: ${callerId})`);
|
|
121
|
+
return currentProfile;
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
setIsLoadingProfile(true);
|
|
125
|
+
isLoadingRef.current = true;
|
|
101
126
|
setProfileError(null);
|
|
102
127
|
try {
|
|
128
|
+
// Log caller for debugging excessive API calls using consola logger
|
|
129
|
+
if (callerId) {
|
|
130
|
+
authLogger.debug(`Profile refresh called by: ${callerId}`);
|
|
131
|
+
}
|
|
103
132
|
const result = await getAccountsProfileRetrieve(api as unknown as API);
|
|
104
133
|
setProfile(result);
|
|
134
|
+
profileRef.current = result;
|
|
105
135
|
// Save to cache with 1 hour TTL
|
|
106
136
|
setCachedProfile(result);
|
|
107
137
|
return result;
|
|
@@ -111,27 +141,28 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
111
141
|
throw err;
|
|
112
142
|
} finally {
|
|
113
143
|
setIsLoadingProfile(false);
|
|
144
|
+
isLoadingRef.current = false;
|
|
114
145
|
}
|
|
115
|
-
}, []);
|
|
146
|
+
}, []); // Empty dependencies - callback is stable
|
|
116
147
|
|
|
117
148
|
// Update profile (full)
|
|
118
149
|
const updateProfile = async (data: UserProfileUpdateRequest): Promise<User> => {
|
|
119
150
|
const result = await updateMutation(data, api as unknown as API);
|
|
120
|
-
await refreshProfile();
|
|
151
|
+
await refreshProfile('AccountsContext.updateProfile');
|
|
121
152
|
return result as User;
|
|
122
153
|
};
|
|
123
154
|
|
|
124
155
|
// Partial update profile
|
|
125
156
|
const partialUpdateProfile = async (data: PatchedUserProfileUpdateRequest): Promise<User> => {
|
|
126
157
|
const result = await partialUpdateMutation(data, api as unknown as API);
|
|
127
|
-
await refreshProfile();
|
|
158
|
+
await refreshProfile('AccountsContext.partialUpdateProfile');
|
|
128
159
|
return result as User;
|
|
129
160
|
};
|
|
130
161
|
|
|
131
162
|
// Upload avatar
|
|
132
163
|
const uploadAvatar = async (formData: FormData): Promise<User> => {
|
|
133
164
|
const result = await avatarMutation(formData, api as unknown as API);
|
|
134
|
-
await refreshProfile();
|
|
165
|
+
await refreshProfile('AccountsContext.uploadAvatar');
|
|
135
166
|
return result as User;
|
|
136
167
|
};
|
|
137
168
|
|
|
@@ -149,7 +180,7 @@ export function AccountsProvider({ children }: AccountsProviderProps) {
|
|
|
149
180
|
if (result.access && result.refresh) {
|
|
150
181
|
api.setToken(result.access, result.refresh);
|
|
151
182
|
// Refresh profile to load user data with new token
|
|
152
|
-
await refreshProfile();
|
|
183
|
+
await refreshProfile('AccountsContext.verifyOTP');
|
|
153
184
|
}
|
|
154
185
|
|
|
155
186
|
return result as OTPVerifyResponse;
|
|
@@ -59,6 +59,7 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
59
59
|
// Use refs to avoid dependency issues
|
|
60
60
|
const userRef = useRef(user);
|
|
61
61
|
const configRef = useRef(config);
|
|
62
|
+
const isLoadingProfileRef = useRef(false);
|
|
62
63
|
|
|
63
64
|
// Update refs when values change
|
|
64
65
|
useEffect(() => {
|
|
@@ -93,43 +94,58 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
|
|
|
93
94
|
return false;
|
|
94
95
|
}, [clearAuthState]);
|
|
95
96
|
|
|
96
|
-
// Simple profile loading without retry - now uses AccountsContext
|
|
97
|
-
const loadCurrentProfile = useCallback(async (): Promise<void> => {
|
|
98
|
-
|
|
97
|
+
// Simple profile loading without retry - now uses AccountsContext with memoization
|
|
98
|
+
const loadCurrentProfile = useCallback(async (callerId?: string): Promise<void> => {
|
|
99
|
+
const finalCallerId = callerId || 'AuthContext.loadCurrentProfile';
|
|
100
|
+
|
|
101
|
+
// Check if profile loading is already in progress
|
|
102
|
+
if (isLoadingProfileRef.current) {
|
|
103
|
+
authLogger.debug(`Profile loading already in progress, skipping duplicate call from: ${finalCallerId}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
authLogger.debug(`loadCurrentProfile called by: ${finalCallerId}`);
|
|
108
|
+
|
|
99
109
|
try {
|
|
110
|
+
isLoadingProfileRef.current = true;
|
|
111
|
+
|
|
100
112
|
// Ensure API clients are properly initialized with current token
|
|
101
113
|
const isAuth = api.isAuthenticated();
|
|
102
114
|
const token = api.getToken();
|
|
103
|
-
//
|
|
115
|
+
// authLogger.debug('isAuthenticated:', isAuth, 'token:', token ? token.substring(0, 20) + '...' : 'null');
|
|
104
116
|
|
|
105
117
|
if (!isAuth) {
|
|
106
|
-
|
|
118
|
+
authLogger.warn('No valid authentication token, throwing error');
|
|
107
119
|
throw new Error('No valid authentication token');
|
|
108
120
|
}
|
|
109
121
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
// Check if profile is already loaded in AccountsContext to prevent duplicate API calls
|
|
123
|
+
if (accounts.profile && !accounts.isLoadingProfile) {
|
|
124
|
+
authLogger.debug('Profile already loaded in AccountsContext, skipping API call');
|
|
125
|
+
setInitialized(true);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Refresh profile from AccountsContext (now with memoization)
|
|
130
|
+
const refreshedProfile = await accounts.refreshProfile(finalCallerId);
|
|
113
131
|
|
|
114
132
|
if (refreshedProfile) {
|
|
115
|
-
// console.log('[AuthContext] Profile loaded successfully:', refreshedProfile.id, 'is_staff:', refreshedProfile.is_staff, 'is_superuser:', refreshedProfile.is_superuser);
|
|
116
133
|
authLogger.info('Profile loaded successfully:', refreshedProfile.id);
|
|
117
134
|
} else {
|
|
118
|
-
console.warn('[AuthContext] Profile refresh returned undefined - but keeping tokens');
|
|
119
135
|
authLogger.warn('Profile refresh returned undefined - but keeping tokens');
|
|
120
136
|
}
|
|
121
137
|
|
|
122
138
|
// Always mark as initialized if we have valid tokens
|
|
123
139
|
// Don't clear tokens just because profile fetch failed
|
|
124
140
|
setInitialized(true);
|
|
125
|
-
// console.log('[AuthContext] loadCurrentProfile completed, initialized=true');
|
|
126
141
|
} catch (error) {
|
|
127
|
-
console.error('[AuthContext] Failed to load profile:', error);
|
|
128
142
|
authLogger.error('Failed to load profile:', error);
|
|
129
143
|
// Use global error handler first, fallback to clearing state
|
|
130
144
|
if (!handleGlobalAuthError(error, 'loadCurrentProfile')) {
|
|
131
145
|
clearAuthState('loadCurrentProfile:error');
|
|
132
146
|
}
|
|
147
|
+
} finally {
|
|
148
|
+
isLoadingProfileRef.current = false;
|
|
133
149
|
}
|
|
134
150
|
}, [clearAuthState, handleGlobalAuthError, accounts]);
|
|
135
151
|
|
|
@@ -29,7 +29,7 @@ export interface AuthContextType {
|
|
|
29
29
|
isLoading: boolean;
|
|
30
30
|
isAuthenticated: boolean;
|
|
31
31
|
isAdminUser: boolean;
|
|
32
|
-
loadCurrentProfile: () => Promise<void>;
|
|
32
|
+
loadCurrentProfile: (callerId?: string) => Promise<void>;
|
|
33
33
|
checkAuthAndRedirect: () => Promise<void>;
|
|
34
34
|
|
|
35
35
|
// Token Methods
|
|
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
3
3
|
import { useAuth } from '../context';
|
|
4
4
|
import { useAutoAuth } from './useAutoAuth';
|
|
5
5
|
import { useLocalStorage } from './useLocalStorage';
|
|
6
|
+
import { authLogger } from '../../utils/logger';
|
|
6
7
|
|
|
7
8
|
export interface AuthFormState {
|
|
8
9
|
identifier: string; // Email or phone number
|
|
@@ -269,7 +270,7 @@ export const useAuthForm = (options: UseAuthFormOptions): AuthFormState & AuthFo
|
|
|
269
270
|
// Auto-detect OTP from URL query parameters
|
|
270
271
|
useAutoAuth({
|
|
271
272
|
onOTPDetected: (otp: string) => {
|
|
272
|
-
|
|
273
|
+
authLogger.info('OTP detected, auto-submitting');
|
|
273
274
|
|
|
274
275
|
// Get saved identifier from auth context
|
|
275
276
|
const savedEmail = getSavedEmail();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRouter } from 'next/router';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
|
+
import { authLogger } from '../../utils/logger';
|
|
3
4
|
|
|
4
5
|
export interface UseAutoAuthOptions {
|
|
5
6
|
onOTPDetected?: (otp: string) => void;
|
|
@@ -21,7 +22,7 @@ export const useAutoAuth = (options: UseAutoAuthOptions = {}) => {
|
|
|
21
22
|
|
|
22
23
|
// Handle OTP detection
|
|
23
24
|
if (queryOtp && typeof queryOtp === 'string' && queryOtp.length === 6) {
|
|
24
|
-
|
|
25
|
+
authLogger.info('OTP detected in URL:', queryOtp);
|
|
25
26
|
onOTPDetected?.(queryOtp);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
+
import { authLogger } from '../../utils/logger';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Simple localStorage hook with better error handling
|
|
@@ -27,7 +28,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
27
28
|
return item as T;
|
|
28
29
|
}
|
|
29
30
|
} catch (error) {
|
|
30
|
-
|
|
31
|
+
authLogger.error(`Error reading localStorage key "${key}":`, error);
|
|
31
32
|
return initialValue;
|
|
32
33
|
}
|
|
33
34
|
});
|
|
@@ -41,13 +42,13 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
41
42
|
|
|
42
43
|
// Limit to 1MB per item
|
|
43
44
|
if (sizeInKB > 1024) {
|
|
44
|
-
|
|
45
|
+
authLogger.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
|
|
45
46
|
return false;
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
return true;
|
|
49
50
|
} catch (error) {
|
|
50
|
-
|
|
51
|
+
authLogger.error(`Error checking data size for key "${key}":`, error);
|
|
51
52
|
return false;
|
|
52
53
|
}
|
|
53
54
|
};
|
|
@@ -72,7 +73,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
} catch (error) {
|
|
75
|
-
|
|
76
|
+
authLogger.error('Error clearing old localStorage data:', error);
|
|
76
77
|
}
|
|
77
78
|
};
|
|
78
79
|
|
|
@@ -88,7 +89,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
} catch (error) {
|
|
91
|
-
|
|
92
|
+
authLogger.error('Error force clearing localStorage:', error);
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
|
|
@@ -99,7 +100,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
99
100
|
|
|
100
101
|
// Check data size before attempting to save
|
|
101
102
|
if (!checkDataSize(valueToStore)) {
|
|
102
|
-
|
|
103
|
+
authLogger.warn(`Data size too large for key "${key}", removing key`);
|
|
103
104
|
// Remove the key if data is too large
|
|
104
105
|
try {
|
|
105
106
|
window.localStorage.removeItem(key);
|
|
@@ -127,10 +128,10 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
127
128
|
window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
128
129
|
} catch (storageError: any) {
|
|
129
130
|
// If quota exceeded, clear old data and try again
|
|
130
|
-
if (storageError.name === 'QuotaExceededError' ||
|
|
131
|
-
storageError.code === 22 ||
|
|
131
|
+
if (storageError.name === 'QuotaExceededError' ||
|
|
132
|
+
storageError.code === 22 ||
|
|
132
133
|
storageError.message?.includes('quota')) {
|
|
133
|
-
|
|
134
|
+
authLogger.warn('localStorage quota exceeded, clearing old data...');
|
|
134
135
|
clearOldData();
|
|
135
136
|
|
|
136
137
|
// Try again after clearing
|
|
@@ -143,7 +144,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
143
144
|
}
|
|
144
145
|
window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
145
146
|
} catch (retryError) {
|
|
146
|
-
|
|
147
|
+
authLogger.error(`Failed to set localStorage key "${key}" after clearing old data:`, retryError);
|
|
147
148
|
// If still fails, force clear all and try one more time
|
|
148
149
|
try {
|
|
149
150
|
forceClearAll();
|
|
@@ -155,7 +156,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
155
156
|
}
|
|
156
157
|
window.localStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
157
158
|
} catch (finalError) {
|
|
158
|
-
|
|
159
|
+
authLogger.error(`Failed to set localStorage key "${key}" after force clearing:`, finalError);
|
|
159
160
|
// If still fails, just update the state without localStorage
|
|
160
161
|
setStoredValue(valueToStore);
|
|
161
162
|
}
|
|
@@ -166,7 +167,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
} catch (error) {
|
|
169
|
-
|
|
170
|
+
authLogger.error(`Error setting localStorage key "${key}":`, error);
|
|
170
171
|
// Still update the state even if localStorage fails
|
|
171
172
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
172
173
|
setStoredValue(valueToStore);
|
|
@@ -183,17 +184,17 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
183
184
|
window.localStorage.removeItem(`${key}_timestamp`);
|
|
184
185
|
} catch (removeError: any) {
|
|
185
186
|
// If removal fails due to quota, try to clear some data first
|
|
186
|
-
if (removeError.name === 'QuotaExceededError' ||
|
|
187
|
-
removeError.code === 22 ||
|
|
187
|
+
if (removeError.name === 'QuotaExceededError' ||
|
|
188
|
+
removeError.code === 22 ||
|
|
188
189
|
removeError.message?.includes('quota')) {
|
|
189
|
-
|
|
190
|
+
authLogger.warn('localStorage quota exceeded during removal, clearing old data...');
|
|
190
191
|
clearOldData();
|
|
191
192
|
|
|
192
193
|
try {
|
|
193
194
|
window.localStorage.removeItem(key);
|
|
194
195
|
window.localStorage.removeItem(`${key}_timestamp`);
|
|
195
196
|
} catch (retryError) {
|
|
196
|
-
|
|
197
|
+
authLogger.error(`Failed to remove localStorage key "${key}" after clearing:`, retryError);
|
|
197
198
|
// If still fails, force clear all
|
|
198
199
|
forceClearAll();
|
|
199
200
|
}
|
|
@@ -203,7 +204,7 @@ export function useLocalStorage<T>(key: string, initialValue: T) {
|
|
|
203
204
|
}
|
|
204
205
|
}
|
|
205
206
|
} catch (error) {
|
|
206
|
-
|
|
207
|
+
authLogger.error(`Error removing localStorage key "${key}":`, error);
|
|
207
208
|
}
|
|
208
209
|
};
|
|
209
210
|
|
|
@@ -36,7 +36,10 @@ export function getCachedProfile(): User | null {
|
|
|
36
36
|
if (typeof window === 'undefined') return null;
|
|
37
37
|
|
|
38
38
|
const cached = localStorage.getItem(CACHE_KEY);
|
|
39
|
-
if (!cached)
|
|
39
|
+
if (!cached) {
|
|
40
|
+
profileLogger.debug('No cached profile found');
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
40
43
|
|
|
41
44
|
const cachedData: CachedProfile = JSON.parse(cached);
|
|
42
45
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
+
import { authLogger } from '../../utils/logger';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Simple sessionStorage hook with better error handling
|
|
@@ -17,7 +18,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
17
18
|
const item = window.sessionStorage.getItem(key);
|
|
18
19
|
return item ? JSON.parse(item) : initialValue;
|
|
19
20
|
} catch (error) {
|
|
20
|
-
|
|
21
|
+
authLogger.error(`Error reading sessionStorage key "${key}":`, error);
|
|
21
22
|
return initialValue;
|
|
22
23
|
}
|
|
23
24
|
});
|
|
@@ -31,13 +32,13 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
31
32
|
|
|
32
33
|
// Limit to 1MB per item
|
|
33
34
|
if (sizeInKB > 1024) {
|
|
34
|
-
|
|
35
|
+
authLogger.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
|
|
35
36
|
return false;
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
+
|
|
38
39
|
return true;
|
|
39
40
|
} catch (error) {
|
|
40
|
-
|
|
41
|
+
authLogger.error(`Error checking data size for key "${key}":`, error);
|
|
41
42
|
return false;
|
|
42
43
|
}
|
|
43
44
|
};
|
|
@@ -62,7 +63,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
} catch (error) {
|
|
65
|
-
|
|
66
|
+
authLogger.error('Error clearing old sessionStorage data:', error);
|
|
66
67
|
}
|
|
67
68
|
};
|
|
68
69
|
|
|
@@ -78,7 +79,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
} catch (error) {
|
|
81
|
-
|
|
82
|
+
authLogger.error('Error force clearing sessionStorage:', error);
|
|
82
83
|
}
|
|
83
84
|
};
|
|
84
85
|
|
|
@@ -89,7 +90,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
89
90
|
|
|
90
91
|
// Check data size before attempting to save
|
|
91
92
|
if (!checkDataSize(valueToStore)) {
|
|
92
|
-
|
|
93
|
+
authLogger.warn(`Data size too large for key "${key}", removing key`);
|
|
93
94
|
// Remove the key if data is too large
|
|
94
95
|
try {
|
|
95
96
|
window.sessionStorage.removeItem(key);
|
|
@@ -112,10 +113,10 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
112
113
|
window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
113
114
|
} catch (storageError: any) {
|
|
114
115
|
// If quota exceeded, clear old data and try again
|
|
115
|
-
if (storageError.name === 'QuotaExceededError' ||
|
|
116
|
-
storageError.code === 22 ||
|
|
116
|
+
if (storageError.name === 'QuotaExceededError' ||
|
|
117
|
+
storageError.code === 22 ||
|
|
117
118
|
storageError.message?.includes('quota')) {
|
|
118
|
-
|
|
119
|
+
authLogger.warn('sessionStorage quota exceeded, clearing old data...');
|
|
119
120
|
clearOldData();
|
|
120
121
|
|
|
121
122
|
// Try again after clearing
|
|
@@ -123,14 +124,14 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
123
124
|
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
124
125
|
window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
125
126
|
} catch (retryError) {
|
|
126
|
-
|
|
127
|
+
authLogger.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
|
|
127
128
|
// If still fails, force clear all and try one more time
|
|
128
129
|
try {
|
|
129
130
|
forceClearAll();
|
|
130
131
|
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
131
132
|
window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
132
133
|
} catch (finalError) {
|
|
133
|
-
|
|
134
|
+
authLogger.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
|
|
134
135
|
// If still fails, just update the state without sessionStorage
|
|
135
136
|
setStoredValue(valueToStore);
|
|
136
137
|
}
|
|
@@ -141,7 +142,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
} catch (error) {
|
|
144
|
-
|
|
145
|
+
authLogger.error(`Error setting sessionStorage key "${key}":`, error);
|
|
145
146
|
// Still update the state even if sessionStorage fails
|
|
146
147
|
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
147
148
|
setStoredValue(valueToStore);
|
|
@@ -158,17 +159,17 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
158
159
|
window.sessionStorage.removeItem(`${key}_timestamp`);
|
|
159
160
|
} catch (removeError: any) {
|
|
160
161
|
// If removal fails due to quota, try to clear some data first
|
|
161
|
-
if (removeError.name === 'QuotaExceededError' ||
|
|
162
|
-
removeError.code === 22 ||
|
|
162
|
+
if (removeError.name === 'QuotaExceededError' ||
|
|
163
|
+
removeError.code === 22 ||
|
|
163
164
|
removeError.message?.includes('quota')) {
|
|
164
|
-
|
|
165
|
+
authLogger.warn('sessionStorage quota exceeded during removal, clearing old data...');
|
|
165
166
|
clearOldData();
|
|
166
167
|
|
|
167
168
|
try {
|
|
168
169
|
window.sessionStorage.removeItem(key);
|
|
169
170
|
window.sessionStorage.removeItem(`${key}_timestamp`);
|
|
170
171
|
} catch (retryError) {
|
|
171
|
-
|
|
172
|
+
authLogger.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
|
|
172
173
|
// If still fails, force clear all
|
|
173
174
|
forceClearAll();
|
|
174
175
|
}
|
|
@@ -178,7 +179,7 @@ export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
|
178
179
|
}
|
|
179
180
|
}
|
|
180
181
|
} catch (error) {
|
|
181
|
-
|
|
182
|
+
authLogger.error(`Error removing sessionStorage key "${key}":`, error);
|
|
182
183
|
}
|
|
183
184
|
};
|
|
184
185
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import React, { Component, ReactNode } from 'react';
|
|
12
12
|
import { ErrorLayout } from '../../ErrorLayout';
|
|
13
13
|
import { Bug } from 'lucide-react';
|
|
14
|
+
import logger from '../../../utils/logger';
|
|
14
15
|
|
|
15
16
|
interface ErrorBoundaryProps {
|
|
16
17
|
children: ReactNode;
|
|
@@ -46,8 +47,8 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
46
47
|
|
|
47
48
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
48
49
|
// Log error
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
logger.error('ErrorBoundary caught error:', error);
|
|
51
|
+
logger.error('Error info:', errorInfo);
|
|
51
52
|
|
|
52
53
|
// Call optional callback
|
|
53
54
|
this.props.onError?.(error, errorInfo);
|
|
@@ -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-10T07:27:36.920Z
|
|
20
20
|
*/
|
|
21
21
|
const PACKAGE_VERSIONS: PackageInfo[] = [
|
|
22
22
|
{
|
|
23
23
|
"name": "@djangocfg/ui",
|
|
24
|
-
"version": "1.2.
|
|
24
|
+
"version": "1.2.32"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"name": "@djangocfg/api",
|
|
28
|
-
"version": "1.2.
|
|
28
|
+
"version": "1.2.32"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"name": "@djangocfg/layouts",
|
|
32
|
-
"version": "1.2.
|
|
32
|
+
"version": "1.2.32"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "@djangocfg/markdown",
|
|
36
|
-
"version": "1.2.
|
|
36
|
+
"version": "1.2.32"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "@djangocfg/og-image",
|
|
40
|
-
"version": "1.2.
|
|
40
|
+
"version": "1.2.32"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"name": "@djangocfg/eslint-config",
|
|
44
|
-
"version": "1.2.
|
|
44
|
+
"version": "1.2.32"
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"name": "@djangocfg/typescript-config",
|
|
48
|
-
"version": "1.2.
|
|
48
|
+
"version": "1.2.32"
|
|
49
49
|
}
|
|
50
50
|
];
|
|
51
51
|
|
|
@@ -118,28 +118,39 @@ function AdminLayoutClient({
|
|
|
118
118
|
const { isAdminUser, user, isLoading, isAuthenticated, loadCurrentProfile } = useAuth();
|
|
119
119
|
// console.log('[AdminLayout] Rendering with user:', user, 'isLoading:', isLoading, 'isAuthenticated:', isAuthenticated);
|
|
120
120
|
|
|
121
|
+
// Use ref instead of state to prevent re-renders
|
|
122
|
+
const profileLoadedRef = React.useRef(false);
|
|
123
|
+
|
|
124
|
+
// Memoize the callback to prevent useCfgApp from re-subscribing to events
|
|
125
|
+
const handleAuthToken = React.useCallback(async (authToken: string, refreshToken?: string) => {
|
|
126
|
+
// console.log('[AdminLayout] handleAuthToken called');
|
|
127
|
+
// console.log('[AdminLayout] authToken:', authToken.substring(0, 20) + '...', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
|
|
128
|
+
|
|
129
|
+
// Always set tokens in API client
|
|
130
|
+
api.setToken(authToken, refreshToken);
|
|
131
|
+
// console.log('[AdminLayout] Tokens set in API client');
|
|
132
|
+
|
|
133
|
+
// Load user profile after setting tokens - ONLY ONCE per session
|
|
134
|
+
if (!profileLoadedRef.current) {
|
|
135
|
+
// console.log('[AdminLayout] Loading user profile (first time)...');
|
|
136
|
+
await loadCurrentProfile('AdminLayout.onAuthTokenReceived');
|
|
137
|
+
profileLoadedRef.current = true;
|
|
138
|
+
// console.log('[AdminLayout] User profile loaded and marked as loaded');
|
|
139
|
+
} else {
|
|
140
|
+
// console.log('[AdminLayout] Profile already loaded, skipping duplicate call');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Call custom handler if provided
|
|
144
|
+
if (config?.onAuthTokenReceived) {
|
|
145
|
+
// console.log('[AdminLayout] Calling custom onAuthTokenReceived handler');
|
|
146
|
+
config.onAuthTokenReceived(authToken, refreshToken);
|
|
147
|
+
}
|
|
148
|
+
}, [loadCurrentProfile, config?.onAuthTokenReceived]);
|
|
149
|
+
|
|
121
150
|
// useCfgApp hook is called here to initialize iframe communication
|
|
122
151
|
// Automatically sets tokens in API client when received from parent
|
|
123
152
|
const { isEmbedded } = useCfgApp({
|
|
124
|
-
onAuthTokenReceived:
|
|
125
|
-
// console.log('[AdminLayout] onAuthTokenReceived called');
|
|
126
|
-
// console.log('[AdminLayout] authToken:', authToken.substring(0, 20) + '...', 'refreshToken:', refreshToken ? refreshToken.substring(0, 20) + '...' : 'null');
|
|
127
|
-
|
|
128
|
-
// Always set tokens in API client
|
|
129
|
-
api.setToken(authToken, refreshToken);
|
|
130
|
-
// console.log('[AdminLayout] Tokens set in API client');
|
|
131
|
-
|
|
132
|
-
// Load user profile after setting tokens
|
|
133
|
-
// console.log('[AdminLayout] Loading user profile...');
|
|
134
|
-
await loadCurrentProfile();
|
|
135
|
-
// console.log('[AdminLayout] User profile loaded');
|
|
136
|
-
|
|
137
|
-
// Call custom handler if provided
|
|
138
|
-
if (config?.onAuthTokenReceived) {
|
|
139
|
-
// console.log('[AdminLayout] Calling custom onAuthTokenReceived handler');
|
|
140
|
-
config.onAuthTokenReceived(authToken, refreshToken);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
153
|
+
onAuthTokenReceived: handleAuthToken // Stable reference
|
|
143
154
|
});
|
|
144
155
|
|
|
145
156
|
// console.log('[AdminLayout] isEmbedded:', isEmbedded);
|
|
@@ -13,6 +13,7 @@ import { useRouter } from 'next/router';
|
|
|
13
13
|
import { useAuth } from '../../../../../auth';
|
|
14
14
|
import { useThemeContext } from '@djangocfg/ui';
|
|
15
15
|
import { useCfgApp } from '../hooks/useApp';
|
|
16
|
+
import { authLogger } from '../../../../../utils/logger';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* ParentSync Component
|
|
@@ -65,7 +66,7 @@ function ParentSyncClient() {
|
|
|
65
66
|
}
|
|
66
67
|
}, [isEmbedded, parentTheme, setTheme]);
|
|
67
68
|
|
|
68
|
-
// 2. Send auth status from iframe → parent
|
|
69
|
+
// 2. Send auth status from iframe → parent (debounced to prevent spam)
|
|
69
70
|
useEffect(() => {
|
|
70
71
|
// Only send if embedded and mounted
|
|
71
72
|
if (!isEmbedded || !isMounted) {
|
|
@@ -76,24 +77,34 @@ function ParentSyncClient() {
|
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
80
|
+
// Use primitive values to avoid unnecessary re-renders from object reference changes
|
|
81
|
+
const isAuthenticated = auth.isAuthenticated;
|
|
82
|
+
const isLoading = auth.isLoading;
|
|
83
|
+
const hasUser = !!auth.user;
|
|
84
84
|
|
|
85
|
-
// console.log('[ParentSync] 📤 Sending auth status to parent:',
|
|
85
|
+
// console.log('[ParentSync] 📤 Sending auth status to parent:', { isAuthenticated, isLoading, hasUser });
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
// Debounce the postMessage to prevent excessive calls
|
|
88
|
+
const timeoutId = setTimeout(() => {
|
|
89
|
+
try {
|
|
90
|
+
window.parent.postMessage({
|
|
91
|
+
type: 'iframe-auth-status',
|
|
92
|
+
data: { isAuthenticated, isLoading, hasUser }
|
|
93
|
+
}, '*');
|
|
94
|
+
// authLogger.debug('Auth status sent successfully');
|
|
95
|
+
} catch (e) {
|
|
96
|
+
authLogger.error('Failed to send auth status:', e);
|
|
97
|
+
}
|
|
98
|
+
}, 100); // 100ms debounce
|
|
99
|
+
|
|
100
|
+
return () => clearTimeout(timeoutId);
|
|
101
|
+
}, [
|
|
102
|
+
auth.isAuthenticated,
|
|
103
|
+
auth.isLoading,
|
|
104
|
+
!!auth.user, // Convert to boolean to prevent object reference changes
|
|
105
|
+
isEmbedded,
|
|
106
|
+
isMounted
|
|
107
|
+
]);
|
|
97
108
|
|
|
98
109
|
// 3. iframe-resize removed - was causing log spam
|
|
99
110
|
|
|
@@ -111,7 +122,7 @@ function ParentSyncClient() {
|
|
|
111
122
|
data: { path: url }
|
|
112
123
|
}, '*');
|
|
113
124
|
} catch (e) {
|
|
114
|
-
|
|
125
|
+
authLogger.error('Failed to send navigation event:', e);
|
|
115
126
|
}
|
|
116
127
|
};
|
|
117
128
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { useState, useEffect } from 'react';
|
|
8
8
|
import { useRouter } from 'next/router';
|
|
9
|
+
import { authLogger } from '../../../../../utils/logger';
|
|
9
10
|
|
|
10
11
|
export interface UseCfgAppReturn {
|
|
11
12
|
/**
|
|
@@ -109,6 +110,9 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
109
110
|
setReferrer(document.referrer);
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
// Debounce timeout for parent-auth messages
|
|
114
|
+
let authTokenTimeout: NodeJS.Timeout | null = null;
|
|
115
|
+
|
|
112
116
|
// Listen for messages from parent window
|
|
113
117
|
const handleMessage = (event: MessageEvent) => {
|
|
114
118
|
// console.log('[useCfgApp] RAW message event:', {
|
|
@@ -124,19 +128,28 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
124
128
|
switch (type) {
|
|
125
129
|
case 'parent-auth':
|
|
126
130
|
// console.log('[useCfgApp] parent-auth message received');
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
options.onAuthTokenReceived(data.authToken, data.refreshToken);
|
|
133
|
-
// console.log('[useCfgApp] onAuthTokenReceived callback completed successfully');
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.error('[useCfgApp] Failed to process auth tokens:', e);
|
|
136
|
-
}
|
|
137
|
-
} else {
|
|
138
|
-
console.warn('[useCfgApp] parent-auth message received but authToken or callback missing:', { hasToken: !!data?.authToken, hasCallback: !!options?.onAuthTokenReceived });
|
|
131
|
+
|
|
132
|
+
// Cancel previous timeout to debounce rapid auth messages
|
|
133
|
+
if (authTokenTimeout) {
|
|
134
|
+
clearTimeout(authTokenTimeout);
|
|
139
135
|
}
|
|
136
|
+
|
|
137
|
+
// Debounce auth token processing to prevent rapid calls (300ms)
|
|
138
|
+
authTokenTimeout = setTimeout(() => {
|
|
139
|
+
// Receive authentication tokens from parent
|
|
140
|
+
if (data?.authToken && options?.onAuthTokenReceived) {
|
|
141
|
+
// console.log('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
|
|
142
|
+
// console.log('[useCfgApp] authToken:', data.authToken.substring(0, 20) + '...', 'refreshToken:', data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null');
|
|
143
|
+
try {
|
|
144
|
+
options.onAuthTokenReceived(data.authToken, data.refreshToken);
|
|
145
|
+
// authLogger.debug('onAuthTokenReceived callback completed successfully');
|
|
146
|
+
} catch (e) {
|
|
147
|
+
authLogger.error('Failed to process auth tokens:', e);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
authLogger.warn('parent-auth message received but authToken or callback missing:', { hasToken: !!data?.authToken, hasCallback: !!options?.onAuthTokenReceived });
|
|
151
|
+
}
|
|
152
|
+
}, 300); // 300ms debounce
|
|
140
153
|
break;
|
|
141
154
|
|
|
142
155
|
case 'parent-theme':
|
|
@@ -148,9 +161,9 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
148
161
|
if (data.themeMode) {
|
|
149
162
|
setParentThemeMode(data.themeMode);
|
|
150
163
|
}
|
|
151
|
-
//
|
|
164
|
+
// authLogger.debug('Theme set successfully:', data.theme);
|
|
152
165
|
} catch (e) {
|
|
153
|
-
|
|
166
|
+
authLogger.error('Failed to process theme:', e);
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
break;
|
|
@@ -174,9 +187,9 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
174
187
|
referrer: document.referrer
|
|
175
188
|
}
|
|
176
189
|
}, '*');
|
|
177
|
-
//
|
|
190
|
+
// authLogger.debug('iframe-ready message sent');
|
|
178
191
|
} catch (e) {
|
|
179
|
-
|
|
192
|
+
authLogger.error('Failed to notify parent about ready state:', e);
|
|
180
193
|
}
|
|
181
194
|
} else {
|
|
182
195
|
// console.log('[useCfgApp] Not in iframe, skipping iframe-ready message');
|
|
@@ -184,6 +197,10 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
184
197
|
|
|
185
198
|
return () => {
|
|
186
199
|
window.removeEventListener('message', handleMessage);
|
|
200
|
+
// Clear timeout on cleanup
|
|
201
|
+
if (authTokenTimeout) {
|
|
202
|
+
clearTimeout(authTokenTimeout);
|
|
203
|
+
}
|
|
187
204
|
};
|
|
188
205
|
}, [options]);
|
|
189
206
|
|
|
@@ -200,7 +217,7 @@ export function useCfgApp(options?: UseCfgAppOptions): UseCfgAppReturn {
|
|
|
200
217
|
}
|
|
201
218
|
}, '*');
|
|
202
219
|
} catch (e) {
|
|
203
|
-
|
|
220
|
+
authLogger.error('Failed to notify parent about navigation:', e);
|
|
204
221
|
}
|
|
205
222
|
}, [router.asPath, router.pathname, isEmbedded, isMounted]);
|
|
206
223
|
|
|
@@ -36,6 +36,7 @@ import { z } from 'zod';
|
|
|
36
36
|
import { usePaymentsContext, useRootPaymentsContext } from '@djangocfg/api/cfg/contexts';
|
|
37
37
|
import { PAYMENT_EVENTS, closePaymentsDialog } from '../events';
|
|
38
38
|
import { openPaymentDetailsDialog } from '../events';
|
|
39
|
+
import { paymentsLogger } from '../../../utils/logger';
|
|
39
40
|
|
|
40
41
|
// Payment creation schema
|
|
41
42
|
const PaymentCreateSchema = z.object({
|
|
@@ -139,7 +140,7 @@ export const CreatePaymentDialog: React.FC = () => {
|
|
|
139
140
|
openPaymentDetailsDialog(String(paymentId));
|
|
140
141
|
}
|
|
141
142
|
} catch (error) {
|
|
142
|
-
|
|
143
|
+
paymentsLogger.error('Failed to create payment:', error);
|
|
143
144
|
} finally {
|
|
144
145
|
setIsSubmitting(false);
|
|
145
146
|
}
|
|
@@ -7,6 +7,7 @@ import { toast } from 'sonner';
|
|
|
7
7
|
import { Avatar, AvatarFallback, Button } from '@djangocfg/ui/components';
|
|
8
8
|
import { useAccountsContext } from '@djangocfg/layouts/auth/context';
|
|
9
9
|
import { useAuth } from '../../../auth';
|
|
10
|
+
import { profileLogger } from '../../../utils/logger';
|
|
10
11
|
|
|
11
12
|
export const AvatarSection = () => {
|
|
12
13
|
const { user } = useAuth();
|
|
@@ -58,7 +59,7 @@ export const AvatarSection = () => {
|
|
|
58
59
|
setAvatarPreview(null);
|
|
59
60
|
} catch (error) {
|
|
60
61
|
toast.error('Failed to upload avatar');
|
|
61
|
-
|
|
62
|
+
profileLogger.error('Avatar upload error:', error);
|
|
62
63
|
} finally {
|
|
63
64
|
setIsUploading(false);
|
|
64
65
|
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -13,9 +13,11 @@ import { createConsola } from 'consola';
|
|
|
13
13
|
* - 5: trace, verbose
|
|
14
14
|
*/
|
|
15
15
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
16
|
+
const isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';
|
|
17
|
+
const showLogs = isDevelopment || isStaticBuild;
|
|
16
18
|
|
|
17
19
|
export const logger = createConsola({
|
|
18
|
-
level:
|
|
20
|
+
level: showLogs ? 4 : 1, // dev: debug, production: errors only
|
|
19
21
|
}).withTag('layouts');
|
|
20
22
|
|
|
21
23
|
// ─────────────────────────────────────────────────────────────────────────
|