@djangocfg/layouts 1.0.1
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/LICENSE +21 -0
- package/README.md +77 -0
- package/package.json +86 -0
- package/src/auth/README.md +962 -0
- package/src/auth/context/AuthContext.tsx +458 -0
- package/src/auth/context/index.ts +2 -0
- package/src/auth/context/types.ts +63 -0
- package/src/auth/hooks/index.ts +6 -0
- package/src/auth/hooks/useAuthForm.ts +329 -0
- package/src/auth/hooks/useAuthGuard.ts +23 -0
- package/src/auth/hooks/useAuthRedirect.ts +51 -0
- package/src/auth/hooks/useAutoAuth.ts +42 -0
- package/src/auth/hooks/useLocalStorage.ts +211 -0
- package/src/auth/hooks/useSessionStorage.ts +186 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/middlewares/index.ts +1 -0
- package/src/auth/middlewares/proxy.ts +24 -0
- package/src/auth/server.ts +6 -0
- package/src/auth/utils/errors.ts +34 -0
- package/src/auth/utils/index.ts +2 -0
- package/src/auth/utils/validation.ts +14 -0
- package/src/index.ts +15 -0
- package/src/layouts/AppLayout/AppLayout.tsx +123 -0
- package/src/layouts/AppLayout/README.md +204 -0
- package/src/layouts/AppLayout/SUMMARY.md +240 -0
- package/src/layouts/AppLayout/USAGE.md +312 -0
- package/src/layouts/AppLayout/components/PageProgress.tsx +104 -0
- package/src/layouts/AppLayout/components/Seo.tsx +87 -0
- package/src/layouts/AppLayout/components/index.ts +6 -0
- package/src/layouts/AppLayout/context/AppContext.tsx +146 -0
- package/src/layouts/AppLayout/context/index.ts +5 -0
- package/src/layouts/AppLayout/hooks/index.ts +6 -0
- package/src/layouts/AppLayout/hooks/useLayoutMode.ts +26 -0
- package/src/layouts/AppLayout/hooks/useNavigation.ts +49 -0
- package/src/layouts/AppLayout/index.ts +31 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthContext.tsx +51 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +111 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +40 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +330 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +158 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +13 -0
- package/src/layouts/AppLayout/layouts/AuthLayout/types.ts +61 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +92 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +60 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +170 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +164 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +7 -0
- package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +5 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +44 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +136 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +262 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +289 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +159 -0
- package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +5 -0
- package/src/layouts/AppLayout/layouts/index.ts +7 -0
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +47 -0
- package/src/layouts/AppLayout/providers/index.ts +5 -0
- package/src/layouts/AppLayout/types/config.ts +40 -0
- package/src/layouts/AppLayout/types/index.ts +10 -0
- package/src/layouts/AppLayout/types/layout.ts +47 -0
- package/src/layouts/AppLayout/types/navigation.ts +41 -0
- package/src/layouts/AppLayout/types/routes.ts +45 -0
- package/src/layouts/AppLayout/utils/index.ts +5 -0
- package/src/layouts/AppLayout/utils/routeDetection.ts +31 -0
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +125 -0
- package/src/layouts/PaymentsLayout/README.md +133 -0
- package/src/layouts/PaymentsLayout/components/CreateApiKeyDialog.tsx +172 -0
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +203 -0
- package/src/layouts/PaymentsLayout/components/DeleteApiKeyDialog.tsx +100 -0
- package/src/layouts/PaymentsLayout/components/index.ts +4 -0
- package/src/layouts/PaymentsLayout/events.ts +106 -0
- package/src/layouts/PaymentsLayout/index.ts +20 -0
- package/src/layouts/PaymentsLayout/types.ts +19 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeyMetrics.tsx +109 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +194 -0
- package/src/layouts/PaymentsLayout/views/apikeys/components/index.ts +3 -0
- package/src/layouts/PaymentsLayout/views/apikeys/index.tsx +19 -0
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +99 -0
- package/src/layouts/PaymentsLayout/views/overview/components/MetricsCards.tsx +103 -0
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +138 -0
- package/src/layouts/PaymentsLayout/views/overview/components/index.ts +4 -0
- package/src/layouts/PaymentsLayout/views/overview/index.tsx +23 -0
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +282 -0
- package/src/layouts/PaymentsLayout/views/payments/components/index.ts +2 -0
- package/src/layouts/PaymentsLayout/views/payments/index.tsx +18 -0
- package/src/layouts/PaymentsLayout/views/tariffs/index.tsx +29 -0
- package/src/layouts/PaymentsLayout/views/transactions/index.tsx +29 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +110 -0
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +146 -0
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +208 -0
- package/src/layouts/ProfileLayout/components/index.ts +3 -0
- package/src/layouts/ProfileLayout/index.ts +3 -0
- package/src/layouts/SupportLayout/README.md +91 -0
- package/src/layouts/SupportLayout/SupportLayout.tsx +178 -0
- package/src/layouts/SupportLayout/components/CreateTicketDialog.tsx +154 -0
- package/src/layouts/SupportLayout/components/MessageInput.tsx +92 -0
- package/src/layouts/SupportLayout/components/MessageList.tsx +312 -0
- package/src/layouts/SupportLayout/components/TicketCard.tsx +96 -0
- package/src/layouts/SupportLayout/components/TicketList.tsx +152 -0
- package/src/layouts/SupportLayout/components/index.ts +6 -0
- package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +260 -0
- package/src/layouts/SupportLayout/context/index.ts +2 -0
- package/src/layouts/SupportLayout/events.ts +31 -0
- package/src/layouts/SupportLayout/hooks/index.ts +2 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +118 -0
- package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +91 -0
- package/src/layouts/SupportLayout/index.ts +6 -0
- package/src/layouts/SupportLayout/types.ts +23 -0
- package/src/layouts/index.ts +9 -0
- package/src/snippets/AuthDialog/AuthDialog.tsx +88 -0
- package/src/snippets/AuthDialog/events.ts +21 -0
- package/src/snippets/AuthDialog/index.ts +3 -0
- package/src/snippets/AuthDialog/useAuthDialog.ts +27 -0
- package/src/snippets/Breadcrumbs.tsx +80 -0
- package/src/snippets/Chat/ChatUIContext.tsx +110 -0
- package/src/snippets/Chat/ChatWidget.tsx +476 -0
- package/src/snippets/Chat/README.md +122 -0
- package/src/snippets/Chat/components/MessageInput.tsx +124 -0
- package/src/snippets/Chat/components/MessageList.tsx +168 -0
- package/src/snippets/Chat/components/SessionList.tsx +192 -0
- package/src/snippets/Chat/components/index.ts +9 -0
- package/src/snippets/Chat/hooks/index.ts +6 -0
- package/src/snippets/Chat/hooks/useInfiniteSessions.ts +83 -0
- package/src/snippets/Chat/index.tsx +44 -0
- package/src/snippets/Chat/types.ts +79 -0
- package/src/snippets/VideoPlayer/README.md +203 -0
- package/src/snippets/VideoPlayer/VideoControls.tsx +133 -0
- package/src/snippets/VideoPlayer/VideoPlayer.tsx +114 -0
- package/src/snippets/VideoPlayer/index.ts +8 -0
- package/src/snippets/VideoPlayer/types.ts +61 -0
- package/src/snippets/index.ts +10 -0
- package/src/styles/dashboard.css +41 -0
- package/src/styles/index.css +20 -0
- package/src/styles/sources.css +6 -0
- package/src/types/index.ts +1 -0
- package/src/types/pageConfig.ts +103 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/logger.ts +57 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple sessionStorage hook with better error handling
|
|
5
|
+
* @param key - Storage key
|
|
6
|
+
* @param initialValue - Default value if key doesn't exist
|
|
7
|
+
* @returns [value, setValue, removeValue] - Current value, setter function, and remove function
|
|
8
|
+
*/
|
|
9
|
+
export function useSessionStorage<T>(key: string, initialValue: T) {
|
|
10
|
+
// Get initial value from sessionStorage or use provided initialValue
|
|
11
|
+
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
12
|
+
if (typeof window === 'undefined') {
|
|
13
|
+
return initialValue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const item = window.sessionStorage.getItem(key);
|
|
18
|
+
return item ? JSON.parse(item) : initialValue;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(`Error reading sessionStorage key "${key}":`, error);
|
|
21
|
+
return initialValue;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Check data size and limit
|
|
26
|
+
const checkDataSize = (data: any): boolean => {
|
|
27
|
+
try {
|
|
28
|
+
const jsonString = JSON.stringify(data);
|
|
29
|
+
const sizeInBytes = new Blob([jsonString]).size;
|
|
30
|
+
const sizeInKB = sizeInBytes / 1024;
|
|
31
|
+
|
|
32
|
+
// Limit to 1MB per item
|
|
33
|
+
if (sizeInKB > 1024) {
|
|
34
|
+
console.warn(`Data size (${sizeInKB.toFixed(2)}KB) exceeds 1MB limit for key "${key}"`);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`Error checking data size for key "${key}":`, error);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Clear old data when sessionStorage is full
|
|
46
|
+
const clearOldData = () => {
|
|
47
|
+
try {
|
|
48
|
+
const keys = Object.keys(sessionStorage).filter(key => key && typeof key === 'string');
|
|
49
|
+
// Remove oldest items if we have more than 50 items
|
|
50
|
+
if (keys.length > 50) {
|
|
51
|
+
const itemsToRemove = Math.ceil(keys.length * 0.2);
|
|
52
|
+
for (let i = 0; i < itemsToRemove; i++) {
|
|
53
|
+
try {
|
|
54
|
+
const key = keys[i];
|
|
55
|
+
if (key) {
|
|
56
|
+
sessionStorage.removeItem(key);
|
|
57
|
+
sessionStorage.removeItem(`${key}_timestamp`);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Ignore errors when removing items
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error clearing old sessionStorage data:', error);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Force clear all data if quota is exceeded
|
|
70
|
+
const forceClearAll = () => {
|
|
71
|
+
try {
|
|
72
|
+
const keys = Object.keys(sessionStorage);
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
try {
|
|
75
|
+
sessionStorage.removeItem(key);
|
|
76
|
+
} catch {
|
|
77
|
+
// Ignore errors when removing items
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Error force clearing sessionStorage:', error);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Update sessionStorage when value changes
|
|
86
|
+
const setValue = (value: T | ((val: T) => T)) => {
|
|
87
|
+
try {
|
|
88
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
89
|
+
|
|
90
|
+
// Check data size before attempting to save
|
|
91
|
+
if (!checkDataSize(valueToStore)) {
|
|
92
|
+
console.warn(`Data size too large for key "${key}", removing key`);
|
|
93
|
+
// Remove the key if data is too large
|
|
94
|
+
try {
|
|
95
|
+
window.sessionStorage.removeItem(key);
|
|
96
|
+
window.sessionStorage.removeItem(`${key}_timestamp`);
|
|
97
|
+
} catch {
|
|
98
|
+
// Ignore errors when removing
|
|
99
|
+
}
|
|
100
|
+
// Still update the state
|
|
101
|
+
setStoredValue(valueToStore);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
setStoredValue(valueToStore);
|
|
106
|
+
|
|
107
|
+
if (typeof window !== 'undefined') {
|
|
108
|
+
// Try to set the value
|
|
109
|
+
try {
|
|
110
|
+
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
111
|
+
// Add timestamp for cleanup
|
|
112
|
+
window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
113
|
+
} catch (storageError: any) {
|
|
114
|
+
// If quota exceeded, clear old data and try again
|
|
115
|
+
if (storageError.name === 'QuotaExceededError' ||
|
|
116
|
+
storageError.code === 22 ||
|
|
117
|
+
storageError.message?.includes('quota')) {
|
|
118
|
+
console.warn('sessionStorage quota exceeded, clearing old data...');
|
|
119
|
+
clearOldData();
|
|
120
|
+
|
|
121
|
+
// Try again after clearing
|
|
122
|
+
try {
|
|
123
|
+
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
124
|
+
window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
125
|
+
} catch (retryError) {
|
|
126
|
+
console.error(`Failed to set sessionStorage key "${key}" after clearing old data:`, retryError);
|
|
127
|
+
// If still fails, force clear all and try one more time
|
|
128
|
+
try {
|
|
129
|
+
forceClearAll();
|
|
130
|
+
window.sessionStorage.setItem(key, JSON.stringify(valueToStore));
|
|
131
|
+
window.sessionStorage.setItem(`${key}_timestamp`, Date.now().toString());
|
|
132
|
+
} catch (finalError) {
|
|
133
|
+
console.error(`Failed to set sessionStorage key "${key}" after force clearing:`, finalError);
|
|
134
|
+
// If still fails, just update the state without sessionStorage
|
|
135
|
+
setStoredValue(valueToStore);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
throw storageError;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(`Error setting sessionStorage key "${key}":`, error);
|
|
145
|
+
// Still update the state even if sessionStorage fails
|
|
146
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
147
|
+
setStoredValue(valueToStore);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Remove value from sessionStorage
|
|
152
|
+
const removeValue = () => {
|
|
153
|
+
try {
|
|
154
|
+
setStoredValue(initialValue);
|
|
155
|
+
if (typeof window !== 'undefined') {
|
|
156
|
+
try {
|
|
157
|
+
window.sessionStorage.removeItem(key);
|
|
158
|
+
window.sessionStorage.removeItem(`${key}_timestamp`);
|
|
159
|
+
} catch (removeError: any) {
|
|
160
|
+
// If removal fails due to quota, try to clear some data first
|
|
161
|
+
if (removeError.name === 'QuotaExceededError' ||
|
|
162
|
+
removeError.code === 22 ||
|
|
163
|
+
removeError.message?.includes('quota')) {
|
|
164
|
+
console.warn('sessionStorage quota exceeded during removal, clearing old data...');
|
|
165
|
+
clearOldData();
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
window.sessionStorage.removeItem(key);
|
|
169
|
+
window.sessionStorage.removeItem(`${key}_timestamp`);
|
|
170
|
+
} catch (retryError) {
|
|
171
|
+
console.error(`Failed to remove sessionStorage key "${key}" after clearing:`, retryError);
|
|
172
|
+
// If still fails, force clear all
|
|
173
|
+
forceClearAll();
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
throw removeError;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(`Error removing sessionStorage key "${key}":`, error);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return [storedValue, setValue, removeValue] as const;
|
|
186
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { middleware, config } from './proxy';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
export function middleware(request: NextRequest) {
|
|
4
|
+
const { pathname, search } = request.nextUrl;
|
|
5
|
+
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
|
6
|
+
|
|
7
|
+
// Proxy /media/* - Images and static files
|
|
8
|
+
if (pathname.startsWith('/media/')) {
|
|
9
|
+
const targetUrl = `${apiUrl}${pathname}${search}`;
|
|
10
|
+
return NextResponse.rewrite(targetUrl, { request: { headers: request.headers } });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Proxy /api/* - API endpoints
|
|
14
|
+
if (pathname.startsWith('/api/')) {
|
|
15
|
+
const targetUrl = `${apiUrl}${pathname}${search}`;
|
|
16
|
+
return NextResponse.rewrite(targetUrl, { request: { headers: request.headers } });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return NextResponse.next();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const config = {
|
|
23
|
+
matcher: ['/media/:path*', '/api/:path*'],
|
|
24
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format authentication error messages
|
|
3
|
+
*/
|
|
4
|
+
export const formatAuthError = (error: any): string => {
|
|
5
|
+
if (typeof error === 'string') {
|
|
6
|
+
return error;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (error?.message) {
|
|
10
|
+
return error.message;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (error?.response?.data?.message) {
|
|
14
|
+
return error.response.data.message;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (error?.response?.data?.detail) {
|
|
18
|
+
return error.response.data.detail;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return 'An unexpected error occurred';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Common error messages
|
|
26
|
+
*/
|
|
27
|
+
export const AUTH_ERRORS = {
|
|
28
|
+
INVALID_EMAIL: 'Please enter a valid email address',
|
|
29
|
+
INVALID_OTP: 'Please enter a valid 6-digit verification code',
|
|
30
|
+
NETWORK_ERROR: 'Network error. Please check your connection',
|
|
31
|
+
SERVER_ERROR: 'Server error. Please try again later',
|
|
32
|
+
UNAUTHORIZED: 'Unauthorized. Please sign in again',
|
|
33
|
+
TOKEN_EXPIRED: 'Session expired. Please sign in again',
|
|
34
|
+
} as const;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email validation utility
|
|
3
|
+
*/
|
|
4
|
+
export const validateEmail = (email: string): boolean => {
|
|
5
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6
|
+
return emailRegex.test(email);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* OTP validation utility
|
|
11
|
+
*/
|
|
12
|
+
export const validateOTP = (otp: string): boolean => {
|
|
13
|
+
return /^\d{6}$/.test(otp);
|
|
14
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @djangocfg/layouts
|
|
3
|
+
*
|
|
4
|
+
* Reusable layout components and authentication system
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Auth system
|
|
8
|
+
export * from './auth';
|
|
9
|
+
|
|
10
|
+
// Layout components
|
|
11
|
+
export * from './layouts';
|
|
12
|
+
|
|
13
|
+
// Snippets - Reusable UI components
|
|
14
|
+
export * from './snippets';
|
|
15
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppLayout - Unified Application Layout System
|
|
3
|
+
*
|
|
4
|
+
* Single component that handles all layout logic:
|
|
5
|
+
* - Auto-detects route type (public/private/auth)
|
|
6
|
+
* - Applies correct layout automatically
|
|
7
|
+
* - Manages all state through context
|
|
8
|
+
* - Zero prop drilling
|
|
9
|
+
*
|
|
10
|
+
* Usage in _app.tsx:
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <AppLayout config={appLayoutConfig}>
|
|
13
|
+
* <Component {...pageProps} />
|
|
14
|
+
* </AppLayout>
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use client';
|
|
19
|
+
|
|
20
|
+
import React, { ReactNode, useState, useEffect } from 'react';
|
|
21
|
+
import { useRouter } from 'next/router';
|
|
22
|
+
import { AppContextProvider } from './context';
|
|
23
|
+
import { CoreProviders } from './providers';
|
|
24
|
+
import { Seo, PageProgress } from './components';
|
|
25
|
+
import { PublicLayout } from './layouts/PublicLayout';
|
|
26
|
+
import { PrivateLayout } from './layouts/PrivateLayout';
|
|
27
|
+
import { AuthLayout } from './layouts/AuthLayout';
|
|
28
|
+
import { determineLayoutMode, getRedirectUrl } from './utils';
|
|
29
|
+
import { useAuth } from '../../auth';
|
|
30
|
+
import type { AppLayoutConfig } from './types';
|
|
31
|
+
|
|
32
|
+
export interface AppLayoutProps {
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
config: AppLayoutConfig;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Layout Router Component
|
|
39
|
+
*
|
|
40
|
+
* Determines which layout to use based on route
|
|
41
|
+
* Uses AppContext - no props passed down!
|
|
42
|
+
*/
|
|
43
|
+
function LayoutRouter({ children }: { children: ReactNode }) {
|
|
44
|
+
const router = useRouter();
|
|
45
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
46
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
47
|
+
|
|
48
|
+
// SSR/Hydration protection
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setIsMounted(true);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
// Get layout mode from context
|
|
54
|
+
const [layoutMode, setLayoutMode] = useState<'public' | 'private' | 'auth'>('public');
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
// This will be properly determined by AppContext
|
|
58
|
+
// For now, simple detection
|
|
59
|
+
if (router.pathname.startsWith('/auth')) {
|
|
60
|
+
setLayoutMode('auth');
|
|
61
|
+
} else if (router.pathname.startsWith('/private')) {
|
|
62
|
+
setLayoutMode('private');
|
|
63
|
+
} else {
|
|
64
|
+
setLayoutMode('public');
|
|
65
|
+
}
|
|
66
|
+
}, [router.pathname]);
|
|
67
|
+
|
|
68
|
+
// Show loading during SSR or auth check
|
|
69
|
+
if (!isMounted || isLoading) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
72
|
+
<div className="text-muted-foreground">Loading...</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Render appropriate layout
|
|
78
|
+
switch (layoutMode) {
|
|
79
|
+
case 'auth':
|
|
80
|
+
return <AuthLayout>{children}</AuthLayout>;
|
|
81
|
+
|
|
82
|
+
case 'private':
|
|
83
|
+
return <PrivateLayout>{children}</PrivateLayout>;
|
|
84
|
+
|
|
85
|
+
case 'public':
|
|
86
|
+
default:
|
|
87
|
+
return <PublicLayout>{children}</PublicLayout>;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* AppLayout - Main Component
|
|
93
|
+
*
|
|
94
|
+
* Single entry point for all layout logic
|
|
95
|
+
* Wrap your app once in _app.tsx
|
|
96
|
+
*/
|
|
97
|
+
export function AppLayout({ children, config }: AppLayoutProps) {
|
|
98
|
+
return (
|
|
99
|
+
<CoreProviders config={config}>
|
|
100
|
+
<AppContextProvider config={config}>
|
|
101
|
+
{/* SEO Meta Tags */}
|
|
102
|
+
<Seo
|
|
103
|
+
pageConfig={{
|
|
104
|
+
title: config.app.name,
|
|
105
|
+
description: config.app.description,
|
|
106
|
+
ogImage: {
|
|
107
|
+
title: config.app.name,
|
|
108
|
+
subtitle: config.app.description,
|
|
109
|
+
},
|
|
110
|
+
}}
|
|
111
|
+
icons={config.app.icons}
|
|
112
|
+
siteUrl={config.app.siteUrl}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
{/* Loading Progress Bar */}
|
|
116
|
+
<PageProgress />
|
|
117
|
+
|
|
118
|
+
{/* Smart Layout Router */}
|
|
119
|
+
<LayoutRouter>{children}</LayoutRouter>
|
|
120
|
+
</AppContextProvider>
|
|
121
|
+
</CoreProviders>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# AppLayout - Unified Application Layout System
|
|
2
|
+
|
|
3
|
+
Умный самодостаточный компонент для управления всеми layout'ами приложения.
|
|
4
|
+
|
|
5
|
+
## 📁 Структура
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
AppLayout/
|
|
9
|
+
├── types/ # Все типы
|
|
10
|
+
│ ├── config.ts # AppLayoutConfig
|
|
11
|
+
│ ├── layout.ts # PublicLayoutConfig, PrivateLayoutConfig
|
|
12
|
+
│ ├── navigation.ts # NavigationItem, DashboardMenuItem
|
|
13
|
+
│ ├── routes.ts # RouteConfig, RouteDetectors
|
|
14
|
+
│ └── index.ts
|
|
15
|
+
│
|
|
16
|
+
├── context/ # Unified App Context
|
|
17
|
+
│ ├── AppContext.tsx # Главный контекст приложения
|
|
18
|
+
│ └── index.ts
|
|
19
|
+
│
|
|
20
|
+
├── hooks/ # Custom hooks
|
|
21
|
+
│ ├── useLayoutMode.ts # Определение текущего режима
|
|
22
|
+
│ ├── useNavigation.ts # Навигационные хуки
|
|
23
|
+
│ └── index.ts
|
|
24
|
+
│
|
|
25
|
+
├── providers/ # Provider components
|
|
26
|
+
│ ├── ThemeProvider.tsx # Тема
|
|
27
|
+
│ ├── AuthProvider.tsx # Аутентификация
|
|
28
|
+
│ └── index.ts
|
|
29
|
+
│
|
|
30
|
+
├── layouts/ # Layout renderers
|
|
31
|
+
│ ├── PublicLayout/ # Публичный layout
|
|
32
|
+
│ ├── PrivateLayout/ # Приватный layout (Dashboard)
|
|
33
|
+
│ ├── AuthLayout/ # Auth layout (минимальный)
|
|
34
|
+
│ └── index.ts
|
|
35
|
+
│
|
|
36
|
+
├── components/ # UI components
|
|
37
|
+
│ ├── Seo.tsx # SEO meta tags
|
|
38
|
+
│ ├── PageProgress.tsx # Loading bar
|
|
39
|
+
│ └── index.ts
|
|
40
|
+
│
|
|
41
|
+
├── utils/ # Utilities
|
|
42
|
+
│ ├── routeDetection.ts # Определение типа маршрута
|
|
43
|
+
│ └── index.ts
|
|
44
|
+
│
|
|
45
|
+
├── AppLayout.tsx # Главный компонент
|
|
46
|
+
├── index.ts # Public exports
|
|
47
|
+
└── README.md # Документация
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 🎯 Основная идея
|
|
51
|
+
|
|
52
|
+
**Единая точка входа** - один компонент `<AppLayout>` управляет всем:
|
|
53
|
+
- Автоматически определяет тип страницы (public/private/auth)
|
|
54
|
+
- Применяет нужный layout
|
|
55
|
+
- Управляет состоянием через единый контекст
|
|
56
|
+
- Предоставляет хуки для доступа к функционалу
|
|
57
|
+
|
|
58
|
+
## 🚀 Использование
|
|
59
|
+
|
|
60
|
+
### В _app.tsx (единственное место подключения)
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { AppLayout } from '@djangocfg/layouts';
|
|
64
|
+
import { appLayoutConfig } from '@/core/appLayoutConfig';
|
|
65
|
+
|
|
66
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
67
|
+
return (
|
|
68
|
+
<AppLayout config={appLayoutConfig}>
|
|
69
|
+
<Component {...pageProps} />
|
|
70
|
+
</AppLayout>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Конфигурация
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
// core/appLayoutConfig.ts
|
|
79
|
+
import type { AppLayoutConfig } from '@djangocfg/layouts';
|
|
80
|
+
|
|
81
|
+
export const appLayoutConfig: AppLayoutConfig = {
|
|
82
|
+
app: {
|
|
83
|
+
name: 'My App',
|
|
84
|
+
logoPath: '/logo.svg',
|
|
85
|
+
},
|
|
86
|
+
api: {
|
|
87
|
+
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
|
88
|
+
},
|
|
89
|
+
routes: {
|
|
90
|
+
auth: '/auth',
|
|
91
|
+
defaultCallback: '/dashboard',
|
|
92
|
+
detectors: {
|
|
93
|
+
isPublicRoute: (path) => !path.startsWith('/private'),
|
|
94
|
+
isPrivateRoute: (path) => path.startsWith('/private'),
|
|
95
|
+
isAuthRoute: (path) => path.startsWith('/auth'),
|
|
96
|
+
getUnauthenticatedRedirect: (path) =>
|
|
97
|
+
path.startsWith('/private') ? '/auth' : null,
|
|
98
|
+
getPageTitle: (path) => 'My App',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
publicLayout: {
|
|
102
|
+
navigation: { /* ... */ },
|
|
103
|
+
userMenu: { /* ... */ },
|
|
104
|
+
footer: { /* ... */ },
|
|
105
|
+
},
|
|
106
|
+
privateLayout: {
|
|
107
|
+
menuGroups: [ /* ... */ ],
|
|
108
|
+
showChat: true,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 🎨 Использование в компонентах
|
|
114
|
+
|
|
115
|
+
### Доступ к контексту
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { useAppContext } from '@djangocfg/layouts';
|
|
119
|
+
|
|
120
|
+
function MyComponent() {
|
|
121
|
+
const {
|
|
122
|
+
config,
|
|
123
|
+
layoutMode,
|
|
124
|
+
mobileMenuOpen,
|
|
125
|
+
toggleMobileMenu
|
|
126
|
+
} = useAppContext();
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<button onClick={toggleMobileMenu}>
|
|
130
|
+
{layoutMode === 'private' ? 'Dashboard' : 'Home'}
|
|
131
|
+
</button>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Использование хуков
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import { useLayoutMode, useNavigation } from '@djangocfg/layouts';
|
|
140
|
+
|
|
141
|
+
function MyComponent() {
|
|
142
|
+
const mode = useLayoutMode(); // 'public' | 'private' | 'auth'
|
|
143
|
+
const { isActive } = useNavigation();
|
|
144
|
+
|
|
145
|
+
if (mode === 'private') {
|
|
146
|
+
return <DashboardView />;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return <PublicView />;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 🏗️ Архитектурные принципы
|
|
154
|
+
|
|
155
|
+
### 1. Single Source of Truth
|
|
156
|
+
Вся конфигурация в одном месте - `AppLayoutConfig`
|
|
157
|
+
|
|
158
|
+
### 2. Context над Props
|
|
159
|
+
Никакого prop drilling - всё через `useAppContext()`
|
|
160
|
+
|
|
161
|
+
### 3. Декомпозиция
|
|
162
|
+
Каждая папка отвечает за свою область:
|
|
163
|
+
- `types/` - только типы
|
|
164
|
+
- `context/` - состояние и контекст
|
|
165
|
+
- `hooks/` - переиспользуемая логика
|
|
166
|
+
- `layouts/` - рендеринг layout'ов
|
|
167
|
+
- `components/` - UI компоненты
|
|
168
|
+
|
|
169
|
+
### 4. Автоматизация
|
|
170
|
+
Layout определяется автоматически на основе маршрута
|
|
171
|
+
|
|
172
|
+
### 5. Расширяемость
|
|
173
|
+
Легко добавить новый layout или функционал
|
|
174
|
+
|
|
175
|
+
## 📦 Экспорты
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
// Главный компонент
|
|
179
|
+
export { AppLayout } from './AppLayout';
|
|
180
|
+
|
|
181
|
+
// Контекст и хуки
|
|
182
|
+
export { useAppContext } from './context';
|
|
183
|
+
export { useLayoutMode, useNavigation } from './hooks';
|
|
184
|
+
|
|
185
|
+
// Типы
|
|
186
|
+
export type {
|
|
187
|
+
AppLayoutConfig,
|
|
188
|
+
PublicLayoutConfig,
|
|
189
|
+
PrivateLayoutConfig,
|
|
190
|
+
RouteConfig,
|
|
191
|
+
NavigationItem,
|
|
192
|
+
DashboardMenuItem,
|
|
193
|
+
} from './types';
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## 🎯 Преимущества
|
|
197
|
+
|
|
198
|
+
✅ **Одно место подключения** - только в `_app.tsx`
|
|
199
|
+
✅ **Нет prop drilling** - всё через контекст
|
|
200
|
+
✅ **Автоматический роутинг** - layout определяется сам
|
|
201
|
+
✅ **Типобезопасность** - TypeScript везде
|
|
202
|
+
✅ **Легкая настройка** - один конфиг объект
|
|
203
|
+
✅ **Декомпозиция** - чистая структура папок
|
|
204
|
+
✅ **Переиспользование** - хуки для всего
|