@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.2.30",
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.30",
67
- "@djangocfg/og-image": "^1.2.30",
68
- "@djangocfg/ui": "^1.2.30",
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.30",
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
- // console.log('[AuthContext] loadCurrentProfile called');
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
- // console.log('[AuthContext] isAuthenticated:', isAuth, 'token:', token ? token.substring(0, 20) + '...' : 'null');
115
+ // authLogger.debug('isAuthenticated:', isAuth, 'token:', token ? token.substring(0, 20) + '...' : 'null');
104
116
 
105
117
  if (!isAuth) {
106
- console.warn('[AuthContext] No valid authentication token, throwing error');
118
+ authLogger.warn('No valid authentication token, throwing error');
107
119
  throw new Error('No valid authentication token');
108
120
  }
109
121
 
110
- // console.log('[AuthContext] Refreshing profile from AccountsContext...');
111
- // Refresh profile from AccountsContext
112
- const refreshedProfile = await accounts.refreshProfile();
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
- console.log('[useAuthForm] OTP detected, auto-submitting');
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
- console.log('[useAutoAuth] OTP detected in URL:', queryOtp);
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
- console.error(`Error reading localStorage key "${key}":`, error);
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
- console.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
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
- console.error(`Error checking data size for key "${key}":`, error);
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
- console.error('Error clearing old localStorage data:', error);
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
- console.error('Error force clearing localStorage:', error);
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
- console.warn(`Data size too large for key "${key}", removing key`);
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
- console.warn('localStorage quota exceeded, clearing old data...');
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
- console.error(`Failed to set localStorage key "${key}" after clearing old data:`, retryError);
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
- console.error(`Failed to set localStorage key "${key}" after force clearing:`, finalError);
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
- console.error(`Error setting localStorage key "${key}":`, error);
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
- console.warn('localStorage quota exceeded during removal, clearing old data...');
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
- console.error(`Failed to remove localStorage key "${key}" after clearing:`, retryError);
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
- console.error(`Error removing localStorage key "${key}":`, error);
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) return null;
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
- console.error(`Error reading sessionStorage key "${key}":`, error);
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
- console.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
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
- console.error(`Error checking data size for key "${key}":`, error);
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
- console.error('Error clearing old sessionStorage data:', error);
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
- console.error('Error force clearing sessionStorage:', error);
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
- console.warn(`Data size too large for key "${key}", removing key`);
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
- console.warn('sessionStorage quota exceeded, clearing old data...');
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
- console.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
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
- console.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
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
- console.error(`Error setting sessionStorage key "${key}":`, error);
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
- console.warn('sessionStorage quota exceeded during removal, clearing old data...');
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
- console.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
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
- console.error(`Error removing sessionStorage key "${key}":`, error);
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
- console.error('ErrorBoundary caught error:', error);
50
- console.error('Error info:', errorInfo);
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-08T09:39:52.478Z
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.30"
24
+ "version": "1.2.32"
25
25
  },
26
26
  {
27
27
  "name": "@djangocfg/api",
28
- "version": "1.2.30"
28
+ "version": "1.2.32"
29
29
  },
30
30
  {
31
31
  "name": "@djangocfg/layouts",
32
- "version": "1.2.30"
32
+ "version": "1.2.32"
33
33
  },
34
34
  {
35
35
  "name": "@djangocfg/markdown",
36
- "version": "1.2.30"
36
+ "version": "1.2.32"
37
37
  },
38
38
  {
39
39
  "name": "@djangocfg/og-image",
40
- "version": "1.2.30"
40
+ "version": "1.2.32"
41
41
  },
42
42
  {
43
43
  "name": "@djangocfg/eslint-config",
44
- "version": "1.2.30"
44
+ "version": "1.2.32"
45
45
  },
46
46
  {
47
47
  "name": "@djangocfg/typescript-config",
48
- "version": "1.2.30"
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: async (authToken, refreshToken) => {
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
- const authData = {
80
- isAuthenticated: auth.isAuthenticated,
81
- isLoading: auth.isLoading,
82
- hasUser: !!auth.user
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:', authData);
85
+ // console.log('[ParentSync] 📤 Sending auth status to parent:', { isAuthenticated, isLoading, hasUser });
86
86
 
87
- try {
88
- window.parent.postMessage({
89
- type: 'iframe-auth-status',
90
- data: authData
91
- }, '*');
92
- // console.log('[ParentSync] Auth status sent successfully');
93
- } catch (e) {
94
- console.error('[ParentSync] Failed to send auth status:', e);
95
- }
96
- }, [auth.isAuthenticated, auth.isLoading, auth.user, isEmbedded, isMounted]);
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
- console.error('[ParentSync] Failed to send navigation event:', e);
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
- // Receive authentication tokens from parent
128
- if (data?.authToken && options?.onAuthTokenReceived) {
129
- // console.log('[useCfgApp] Auth tokens found, calling onAuthTokenReceived callback');
130
- // console.log('[useCfgApp] authToken:', data.authToken.substring(0, 20) + '...', 'refreshToken:', data.refreshToken ? data.refreshToken.substring(0, 20) + '...' : 'null');
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
- // console.log('[useCfgApp] Theme set successfully:', data.theme);
164
+ // authLogger.debug('Theme set successfully:', data.theme);
152
165
  } catch (e) {
153
- console.error('[useCfgApp] Failed to process theme:', e);
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
- // console.log('[useCfgApp] iframe-ready message sent');
190
+ // authLogger.debug('iframe-ready message sent');
178
191
  } catch (e) {
179
- console.error('[useCfgApp] Failed to notify parent about ready state:', e);
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
- console.error('[iframe] Failed to notify parent about navigation:', e);
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
- console.error('Failed to create payment:', error);
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
- console.error('Avatar upload error:', error);
62
+ profileLogger.error('Avatar upload error:', error);
62
63
  } finally {
63
64
  setIsUploading(false);
64
65
  }
@@ -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: isDevelopment ? 4 : 1, // dev: debug, production: errors only
20
+ level: showLogs ? 4 : 1, // dev: debug, production: errors only
19
21
  }).withTag('layouts');
20
22
 
21
23
  // ─────────────────────────────────────────────────────────────────────────