@djangocfg/layouts 1.2.31 → 1.2.33

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.
Files changed (28) hide show
  1. package/package.json +5 -5
  2. package/src/auth/context/AccountsContext.tsx +40 -9
  3. package/src/auth/context/AuthContext.tsx +43 -13
  4. package/src/auth/context/types.ts +1 -1
  5. package/src/auth/hooks/useAuthForm.ts +2 -1
  6. package/src/auth/hooks/useAutoAuth.ts +2 -1
  7. package/src/auth/hooks/useLocalStorage.ts +19 -18
  8. package/src/auth/hooks/useProfileCache.ts +4 -1
  9. package/src/auth/hooks/useSessionStorage.ts +19 -18
  10. package/src/index.ts +4 -1
  11. package/src/layouts/AppLayout/AppLayout.tsx +9 -2
  12. package/src/layouts/AppLayout/components/ErrorBoundary.tsx +3 -2
  13. package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +8 -8
  14. package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +56 -19
  15. package/src/layouts/AppLayout/layouts/AdminLayout/components/ParentSync.tsx +29 -18
  16. package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +65 -19
  17. package/src/layouts/AppLayout/providers/CoreProviders.tsx +12 -2
  18. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +2 -1
  19. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -1
  20. package/src/layouts/UILayout/components/layout/Header/Header.tsx +11 -4
  21. package/src/layouts/UILayout/components/layout/Header/HeaderDesktop.tsx +14 -7
  22. package/src/layouts/UILayout/components/layout/Header/TestValidationButton.tsx +265 -0
  23. package/src/layouts/UILayout/components/layout/Header/index.ts +2 -0
  24. package/src/utils/logger.ts +3 -1
  25. package/src/validation/README.md +507 -0
  26. package/src/validation/ValidationErrorContext.tsx +333 -0
  27. package/src/validation/ValidationErrorToast.tsx +251 -0
  28. package/src/validation/index.ts +25 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.2.31",
3
+ "version": "1.2.33",
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.31",
67
- "@djangocfg/og-image": "^1.2.31",
68
- "@djangocfg/ui": "^1.2.31",
66
+ "@djangocfg/api": "^1.2.33",
67
+ "@djangocfg/og-image": "^1.2.33",
68
+ "@djangocfg/ui": "^1.2.33",
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.31",
89
+ "@djangocfg/typescript-config": "^1.2.33",
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
 
@@ -140,6 +156,10 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
140
156
  const initializeAuth = async () => {
141
157
  authLogger.info('Initializing auth...');
142
158
 
159
+ // Check if running in iframe (AdminLayout will handle auth via postMessage)
160
+ const isInIframe = typeof window !== 'undefined' && window.self !== window.top;
161
+ authLogger.info('Is in iframe:', isInIframe);
162
+
143
163
  // Debug token state
144
164
  const token = api.getToken();
145
165
  const refreshToken = api.getRefreshToken();
@@ -167,11 +187,21 @@ const AuthProviderInternal: React.FC<AuthProviderProps> = ({ children, config })
167
187
  return;
168
188
  }
169
189
 
190
+ // In iframe mode WITHOUT tokens yet - wait for AdminLayout to receive them via postMessage
191
+ // Don't initialize yet - AdminLayout.handleAuthToken will call loadCurrentProfile
192
+ if (isInIframe && !hasTokens) {
193
+ authLogger.info('Running in iframe without tokens - waiting for parent to send via postMessage');
194
+ authLogger.info('AdminLayout will handle auth initialization, skipping AuthContext init');
195
+ setInitialized(true); // Mark as initialized to prevent re-initialization
196
+ setIsLoading(false);
197
+ return;
198
+ }
199
+
170
200
  if (hasTokens) {
171
201
  setIsLoading(true);
172
202
  try {
173
203
  authLogger.info('No cached profile found, loading from API...');
174
- await loadCurrentProfile();
204
+ await loadCurrentProfile('AuthContext.initializeAuth');
175
205
  } catch (error) {
176
206
  authLogger.error('Failed to load profile during initialization:', error);
177
207
  // If profile loading fails, clear auth state
@@ -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
 
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @djangocfg/layouts
3
- *
3
+ *
4
4
  * Reusable layout components and authentication system
5
5
  */
6
6
 
@@ -13,3 +13,6 @@ export * from './layouts';
13
13
  // Snippets - Reusable UI components
14
14
  export * from './snippets';
15
15
 
16
+ // Validation error tracking
17
+ export * from './validation';
18
+
@@ -36,6 +36,7 @@ import { AdminLayout } from './layouts/AdminLayout';
36
36
  import { determineLayoutMode, getRedirectUrl } from './utils';
37
37
  import { useAuth } from '../../auth';
38
38
  import type { AppLayoutConfig } from './types';
39
+ import type { ValidationErrorConfig } from '../../validation';
39
40
 
40
41
  export interface AppLayoutProps {
41
42
  children: ReactNode;
@@ -69,6 +70,12 @@ export interface AppLayoutProps {
69
70
  * @example showPackageVersions={true}
70
71
  */
71
72
  showPackageVersions?: boolean;
73
+ /**
74
+ * Configuration for validation error tracking
75
+ * @default { enableToast: true, maxErrors: 50 }
76
+ * @example validationConfig={{ enableToast: false }}
77
+ */
78
+ validationConfig?: Partial<ValidationErrorConfig>;
72
79
  }
73
80
 
74
81
  /**
@@ -249,7 +256,7 @@ function LayoutRouter({
249
256
  * </AppLayout>
250
257
  * ```
251
258
  */
252
- export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily, showPackageVersions }: AppLayoutProps) {
259
+ export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily, showPackageVersions, validationConfig }: AppLayoutProps) {
253
260
  const router = useRouter();
254
261
 
255
262
  // Check if ErrorBoundary is enabled (default: true)
@@ -292,7 +299,7 @@ export function AppLayout({ children, config, disableLayout = false, forceLayout
292
299
  }} />
293
300
  )}
294
301
 
295
- <CoreProviders config={config}>
302
+ <CoreProviders config={config} validationConfig={validationConfig}>
296
303
  {appContent}
297
304
  </CoreProviders>
298
305
  </>
@@ -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);