@djangocfg/layouts 1.4.30 → 2.0.2
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/README.md +277 -18
- package/package.json +15 -24
- package/src/auth/context/AuthContext.tsx +5 -5
- package/src/auth/hooks/useAuthGuard.ts +1 -1
- package/src/auth/hooks/useAutoAuth.ts +8 -7
- package/src/components/ErrorBoundary.tsx +78 -0
- package/src/components/JsonLd.tsx +31 -0
- package/src/components/LucideIcon.tsx +91 -0
- package/src/components/PageProgress.tsx +127 -0
- package/src/components/Suspense.tsx +29 -0
- package/src/{layouts/AppLayout/components → components}/UpdateNotifier/UpdateNotifier.tsx +56 -49
- package/src/components/index.ts +10 -0
- package/src/index.ts +25 -7
- package/src/layouts/AdminLayout/AdminLayout.tsx +46 -0
- package/src/layouts/AdminLayout/index.ts +7 -0
- package/src/layouts/AppLayout/AppLayout.tsx +278 -326
- package/src/layouts/AppLayout/index.ts +2 -39
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/AuthContext.tsx +3 -2
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/AuthHelp.tsx +1 -0
- package/src/layouts/AuthLayout/AuthLayout.tsx +61 -0
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/IdentifierForm.tsx +47 -34
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/OTPForm.tsx +2 -3
- package/src/layouts/AuthLayout/index.ts +24 -0
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/types.ts +1 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +144 -0
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +32 -0
- package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +57 -0
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +141 -0
- package/src/layouts/PrivateLayout/components/index.ts +8 -0
- package/src/layouts/PrivateLayout/index.ts +7 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +15 -7
- package/src/layouts/PublicLayout/PublicLayout.tsx +121 -0
- package/src/layouts/PublicLayout/components/PublicFooter.tsx +190 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +117 -0
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +101 -0
- package/src/layouts/PublicLayout/components/index.ts +8 -0
- package/src/layouts/PublicLayout/index.ts +7 -0
- package/src/layouts/_components/UserMenu.tsx +160 -0
- package/src/layouts/_components/index.ts +7 -0
- package/src/layouts/index.ts +15 -8
- package/src/snippets/Analytics/AnalyticsProvider.tsx +8 -4
- package/src/snippets/Analytics/useAnalytics.ts +11 -21
- package/src/snippets/Chat/ChatWidget.tsx +4 -4
- package/src/snippets/ContactForm/ContactFormProvider.tsx +32 -19
- package/src/snippets/ContactForm/ContactPage.tsx +2 -4
- package/src/snippets/ContactForm/types.ts +3 -2
- package/src/snippets/index.ts +0 -1
- package/src/layouts/AppLayout/README.md +0 -204
- package/src/layouts/AppLayout/SUMMARY.md +0 -240
- package/src/layouts/AppLayout/USAGE.md +0 -312
- package/src/layouts/AppLayout/components/ErrorBoundary.tsx +0 -112
- package/src/layouts/AppLayout/components/PageProgress.tsx +0 -123
- package/src/layouts/AppLayout/components/Seo.tsx +0 -171
- package/src/layouts/AppLayout/components/UserMenu.tsx +0 -385
- package/src/layouts/AppLayout/components/index.ts +0 -11
- package/src/layouts/AppLayout/context/AppContext.tsx +0 -151
- package/src/layouts/AppLayout/context/index.ts +0 -5
- package/src/layouts/AppLayout/hooks/index.ts +0 -8
- package/src/layouts/AppLayout/hooks/useLayoutMode.ts +0 -26
- package/src/layouts/AppLayout/hooks/useNavigation.ts +0 -51
- package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +0 -224
- package/src/layouts/AppLayout/layouts/AdminLayout/README.md +0 -409
- package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.example.tsx +0 -98
- package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.tsx +0 -149
- package/src/layouts/AppLayout/layouts/AdminLayout/components/ParentSync.tsx +0 -146
- package/src/layouts/AppLayout/layouts/AdminLayout/components/index.ts +0 -3
- package/src/layouts/AppLayout/layouts/AdminLayout/context/CfgAppContext.tsx +0 -48
- package/src/layouts/AppLayout/layouts/AdminLayout/context/index.ts +0 -2
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/index.ts +0 -6
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +0 -279
- package/src/layouts/AppLayout/layouts/AdminLayout/index.ts +0 -24
- package/src/layouts/AppLayout/layouts/AdminLayout/lottie/energizing.json +0 -1
- package/src/layouts/AppLayout/layouts/AdminLayout/types/index.ts +0 -45
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +0 -41
- package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +0 -15
- package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +0 -82
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +0 -62
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +0 -89
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +0 -181
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +0 -9
- package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +0 -5
- package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +0 -44
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +0 -242
- package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileDrawer.tsx +0 -150
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +0 -169
- package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +0 -5
- package/src/layouts/AppLayout/layouts/index.ts +0 -7
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +0 -80
- package/src/layouts/AppLayout/providers/index.ts +0 -5
- package/src/layouts/AppLayout/types/config.ts +0 -79
- package/src/layouts/AppLayout/types/index.ts +0 -11
- package/src/layouts/AppLayout/types/layout.ts +0 -54
- package/src/layouts/AppLayout/types/navigation.ts +0 -43
- package/src/layouts/AppLayout/types/page.ts +0 -80
- package/src/layouts/AppLayout/types/routes.ts +0 -43
- package/src/layouts/AppLayout/utils/index.ts +0 -5
- package/src/layouts/AppLayout/utils/routeDetection.ts +0 -31
- package/src/layouts/ErrorLayout/ErrorLayout.tsx +0 -173
- package/src/layouts/ErrorLayout/errorConfig.tsx +0 -152
- package/src/layouts/ErrorLayout/index.ts +0 -8
- package/src/layouts/SimpleLayout/SimpleLayout.tsx +0 -72
- package/src/layouts/SimpleLayout/index.ts +0 -3
- package/src/snippets/VideoPlayer/README.md +0 -238
- package/src/snippets/VideoPlayer/VideoControls.tsx +0 -137
- package/src/snippets/VideoPlayer/VideoPlayer.tsx +0 -248
- package/src/snippets/VideoPlayer/index.ts +0 -8
- package/src/snippets/VideoPlayer/types.ts +0 -61
- package/src/types/index.ts +0 -2
- package/src/types/pageConfig.ts +0 -100
- /package/src/{validation → components/ErrorsTracker}/README.md +0 -0
- /package/src/{validation → components/ErrorsTracker}/components/ErrorButtons.tsx +0 -0
- /package/src/{validation → components/ErrorsTracker}/components/ErrorToast.tsx +0 -0
- /package/src/{validation → components/ErrorsTracker}/hooks.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/index.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/providers/ErrorTrackingProvider.tsx +0 -0
- /package/src/{validation → components/ErrorsTracker}/types.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/utils/curl-generator.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/utils/formatters.ts +0 -0
- /package/src/{layouts/AppLayout/components → components}/UpdateNotifier/index.ts +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { createContext, useContext } from 'react';
|
|
4
4
|
|
|
5
|
-
import { useAuthForm } from '
|
|
5
|
+
import { useAuthForm } from '../../auth/hooks';
|
|
6
6
|
|
|
7
7
|
import type { AuthContextType, AuthProps } from './types';
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ export const AuthProvider: React.FC<AuthProps> = ({
|
|
|
14
14
|
supportUrl,
|
|
15
15
|
termsUrl,
|
|
16
16
|
privacyUrl,
|
|
17
|
-
enablePhoneAuth = false, // Default to
|
|
17
|
+
enablePhoneAuth = false, // Default to false for backward compatibility
|
|
18
18
|
onIdentifierSuccess,
|
|
19
19
|
onOTPSuccess,
|
|
20
20
|
onError,
|
|
@@ -51,3 +51,4 @@ export const useAuthContext = () => {
|
|
|
51
51
|
}
|
|
52
52
|
return context;
|
|
53
53
|
};
|
|
54
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Layout
|
|
3
|
+
*
|
|
4
|
+
* Layout for authentication pages with OTP authentication (email/phone)
|
|
5
|
+
* Supports two-step authentication flow: identifier input → OTP verification
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { AuthLayout } from '@djangocfg/layouts';
|
|
10
|
+
*
|
|
11
|
+
* <AuthLayout
|
|
12
|
+
* sourceUrl="https://example.com"
|
|
13
|
+
* supportUrl="https://example.com/support"
|
|
14
|
+
* termsUrl="https://example.com/terms"
|
|
15
|
+
* privacyUrl="https://example.com/privacy"
|
|
16
|
+
* enablePhoneAuth={true}
|
|
17
|
+
* >
|
|
18
|
+
* {/* Optional custom content above forms *\/}
|
|
19
|
+
* </AuthLayout>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use client';
|
|
24
|
+
|
|
25
|
+
import React from 'react';
|
|
26
|
+
|
|
27
|
+
import { AuthProvider, useAuthContext } from './AuthContext';
|
|
28
|
+
import { IdentifierForm } from './IdentifierForm';
|
|
29
|
+
import { OTPForm } from './OTPForm';
|
|
30
|
+
import { Suspense } from '../../components';
|
|
31
|
+
|
|
32
|
+
import type { AuthProps } from './types';
|
|
33
|
+
|
|
34
|
+
export type AuthLayoutProps = AuthProps;
|
|
35
|
+
|
|
36
|
+
export const AuthLayout: React.FC<AuthProps> = (props) => {
|
|
37
|
+
return (
|
|
38
|
+
<Suspense>
|
|
39
|
+
<AuthProvider {...props}>
|
|
40
|
+
<div
|
|
41
|
+
className={`min-h-screen flex flex-col items-center justify-center bg-background py-6 px-4 sm:py-12 sm:px-6 lg:px-8 ${props.className || ''}`}
|
|
42
|
+
>
|
|
43
|
+
<div className="w-full sm:max-w-md space-y-8">
|
|
44
|
+
{props.children}
|
|
45
|
+
|
|
46
|
+
<AuthContent />
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</AuthProvider>
|
|
50
|
+
</Suspense>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Separate component to use the context
|
|
55
|
+
const AuthContent: React.FC = () => {
|
|
56
|
+
const { step } = useAuthContext();
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div>{step === 'identifier' ? <IdentifierForm /> : <OTPForm />}</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -184,23 +184,29 @@ export const IdentifierForm: React.FC = () => {
|
|
|
184
184
|
<div className="text-sm text-muted-foreground leading-5">
|
|
185
185
|
<Label htmlFor="terms" className="cursor-pointer">
|
|
186
186
|
I agree to the{' '}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
187
|
+
{termsUrl && (
|
|
188
|
+
<>
|
|
189
|
+
<a
|
|
190
|
+
href={termsUrl}
|
|
191
|
+
target="_blank"
|
|
192
|
+
rel="noopener noreferrer"
|
|
193
|
+
className="text-primary hover:underline font-medium"
|
|
194
|
+
>
|
|
195
|
+
Terms of Service
|
|
196
|
+
</a>{' '}
|
|
197
|
+
and{' '}
|
|
198
|
+
</>
|
|
199
|
+
)}
|
|
200
|
+
{privacyUrl && (
|
|
201
|
+
<a
|
|
202
|
+
href={privacyUrl}
|
|
203
|
+
target="_blank"
|
|
204
|
+
rel="noopener noreferrer"
|
|
205
|
+
className="text-primary hover:underline font-medium"
|
|
206
|
+
>
|
|
207
|
+
Privacy Policy
|
|
208
|
+
</a>
|
|
209
|
+
)}
|
|
204
210
|
</Label>
|
|
205
211
|
</div>
|
|
206
212
|
</div>
|
|
@@ -267,23 +273,29 @@ export const IdentifierForm: React.FC = () => {
|
|
|
267
273
|
<div className="text-sm text-muted-foreground leading-5">
|
|
268
274
|
<Label htmlFor="terms-email" className="cursor-pointer">
|
|
269
275
|
I agree to the{' '}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
276
|
+
{termsUrl && (
|
|
277
|
+
<>
|
|
278
|
+
<a
|
|
279
|
+
href={termsUrl}
|
|
280
|
+
target="_blank"
|
|
281
|
+
rel="noopener noreferrer"
|
|
282
|
+
className="text-primary hover:underline font-medium"
|
|
283
|
+
>
|
|
284
|
+
Terms of Service
|
|
285
|
+
</a>{' '}
|
|
286
|
+
and{' '}
|
|
287
|
+
</>
|
|
288
|
+
)}
|
|
289
|
+
{privacyUrl && (
|
|
290
|
+
<a
|
|
291
|
+
href={privacyUrl}
|
|
292
|
+
target="_blank"
|
|
293
|
+
rel="noopener noreferrer"
|
|
294
|
+
className="text-primary hover:underline font-medium"
|
|
295
|
+
>
|
|
296
|
+
Privacy Policy
|
|
297
|
+
</a>
|
|
298
|
+
)}
|
|
287
299
|
</Label>
|
|
288
300
|
</div>
|
|
289
301
|
</div>
|
|
@@ -322,3 +334,4 @@ export const IdentifierForm: React.FC = () => {
|
|
|
322
334
|
</Card>
|
|
323
335
|
);
|
|
324
336
|
};
|
|
337
|
+
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
|
|
3
|
-
// @ts-nocheck
|
|
4
3
|
import React from 'react';
|
|
5
4
|
import { Mail, MessageCircle, ArrowLeft, RotateCw, ShieldCheck } from 'lucide-react';
|
|
6
5
|
|
|
@@ -73,7 +72,6 @@ export const OTPForm: React.FC = () => {
|
|
|
73
72
|
Enter verification code
|
|
74
73
|
</label>
|
|
75
74
|
<div className="flex justify-center">
|
|
76
|
-
{/* @ts-expect-error - TypeScript doesn't recognize children in JSX props for discriminated union */}
|
|
77
75
|
<InputOTP
|
|
78
76
|
value={otp}
|
|
79
77
|
onChange={setOtp}
|
|
@@ -176,3 +174,4 @@ export const OTPForm: React.FC = () => {
|
|
|
176
174
|
</Card>
|
|
177
175
|
);
|
|
178
176
|
};
|
|
177
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Layout exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { AuthLayout } from './AuthLayout';
|
|
6
|
+
export type { AuthLayoutProps } from './AuthLayout';
|
|
7
|
+
|
|
8
|
+
// Context and hooks (renamed to avoid conflict with main AuthProvider)
|
|
9
|
+
export { AuthProvider as AuthLayoutProvider, useAuthContext } from './AuthContext';
|
|
10
|
+
|
|
11
|
+
// Forms
|
|
12
|
+
export { IdentifierForm } from './IdentifierForm';
|
|
13
|
+
export { OTPForm } from './OTPForm';
|
|
14
|
+
|
|
15
|
+
// Help component
|
|
16
|
+
export { AuthHelp } from './AuthHelp';
|
|
17
|
+
|
|
18
|
+
// Types (renamed to avoid conflict with main AuthContextType)
|
|
19
|
+
export type {
|
|
20
|
+
AuthContextType as AuthLayoutContextType,
|
|
21
|
+
AuthProps as AuthLayoutFormProps,
|
|
22
|
+
AuthHelpProps
|
|
23
|
+
} from './types';
|
|
24
|
+
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private Layout
|
|
3
|
+
*
|
|
4
|
+
* Layout for authenticated user pages (dashboard, profile, etc.)
|
|
5
|
+
* Import and use directly with props - no complex configs needed!
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Responsive sidebar with mobile burger menu
|
|
9
|
+
* - Keyboard shortcut (Ctrl/Cmd + B) to toggle sidebar
|
|
10
|
+
* - Header with sidebar trigger and user menu
|
|
11
|
+
* - Configurable content padding
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* import { PrivateLayout } from '@djangocfg/layouts';
|
|
16
|
+
*
|
|
17
|
+
* <PrivateLayout
|
|
18
|
+
* sidebar={{
|
|
19
|
+
* items: [
|
|
20
|
+
* { label: 'Dashboard', href: '/dashboard', icon: 'LayoutDashboard' },
|
|
21
|
+
* { label: 'Profile', href: '/profile', icon: 'User' }
|
|
22
|
+
* ]
|
|
23
|
+
* }}
|
|
24
|
+
* header={{
|
|
25
|
+
* title: 'Dashboard',
|
|
26
|
+
* profilePath: '/profile' // Optional, defaults to '/profile'
|
|
27
|
+
* }}
|
|
28
|
+
* >
|
|
29
|
+
* {children}
|
|
30
|
+
* </PrivateLayout>
|
|
31
|
+
*
|
|
32
|
+
* Note: User data (name, email, avatar) is automatically loaded from useAuth() context
|
|
33
|
+
* Keyboard shortcut: Ctrl/Cmd + B to toggle sidebar
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
'use client';
|
|
38
|
+
|
|
39
|
+
import React, { ReactNode } from 'react';
|
|
40
|
+
import { SidebarProvider, SidebarInset, Preloader, Button, ButtonLink } from '@djangocfg/ui/components';
|
|
41
|
+
import { useAuth } from '../../auth';
|
|
42
|
+
import { PrivateSidebar, PrivateHeader, PrivateContent } from './components';
|
|
43
|
+
import type { LucideIcon as LucideIconType } from 'lucide-react';
|
|
44
|
+
|
|
45
|
+
export interface SidebarItem {
|
|
46
|
+
label: string;
|
|
47
|
+
href: string;
|
|
48
|
+
icon?: string | LucideIconType;
|
|
49
|
+
badge?: string | number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface SidebarConfig {
|
|
53
|
+
items: SidebarItem[];
|
|
54
|
+
homeHref?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface HeaderConfig {
|
|
58
|
+
title?: string;
|
|
59
|
+
/** Profile path (optional, defaults to '/profile') */
|
|
60
|
+
profilePath?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PrivateLayoutProps {
|
|
64
|
+
children: ReactNode;
|
|
65
|
+
/** Sidebar configuration */
|
|
66
|
+
sidebar?: SidebarConfig;
|
|
67
|
+
/** Header configuration */
|
|
68
|
+
header?: HeaderConfig;
|
|
69
|
+
/** Content padding */
|
|
70
|
+
contentPadding?: 'none' | 'default';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function PrivateLayout({
|
|
74
|
+
children,
|
|
75
|
+
sidebar,
|
|
76
|
+
header,
|
|
77
|
+
contentPadding = 'default',
|
|
78
|
+
}: PrivateLayoutProps) {
|
|
79
|
+
const { isAuthenticated, isLoading, user } = useAuth();
|
|
80
|
+
|
|
81
|
+
// Debug logging in development
|
|
82
|
+
if (process.env.NODE_ENV === 'development') {
|
|
83
|
+
console.log('[PrivateLayout] Render state:', {
|
|
84
|
+
isLoading,
|
|
85
|
+
isAuthenticated,
|
|
86
|
+
hasUser: !!user,
|
|
87
|
+
hasSidebar: !!sidebar,
|
|
88
|
+
hasHeader: !!header,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Show loading state while auth is being checked
|
|
93
|
+
if (isLoading) {
|
|
94
|
+
return (
|
|
95
|
+
<Preloader
|
|
96
|
+
variant="fullscreen"
|
|
97
|
+
text="Authenticating..."
|
|
98
|
+
size="lg"
|
|
99
|
+
backdrop={true}
|
|
100
|
+
backdropOpacity={80}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Don't render if user is not authenticated
|
|
106
|
+
if (!isAuthenticated) {
|
|
107
|
+
if (process.env.NODE_ENV === 'development') {
|
|
108
|
+
console.warn('[PrivateLayout] User not authenticated, returning null');
|
|
109
|
+
}
|
|
110
|
+
return (
|
|
111
|
+
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
|
|
112
|
+
<h3 className="text-2xl font-bold">
|
|
113
|
+
Not Authenticated
|
|
114
|
+
</h3>
|
|
115
|
+
<p className="text-muted-foreground">
|
|
116
|
+
Please log in to continue.
|
|
117
|
+
</p>
|
|
118
|
+
<ButtonLink
|
|
119
|
+
variant="outline"
|
|
120
|
+
href="/auth"
|
|
121
|
+
>
|
|
122
|
+
Login
|
|
123
|
+
</ButtonLink>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<SidebarProvider defaultOpen={true}>
|
|
130
|
+
{/* Sidebar */}
|
|
131
|
+
{sidebar && <PrivateSidebar sidebar={sidebar} />}
|
|
132
|
+
|
|
133
|
+
{/* Main content area */}
|
|
134
|
+
<SidebarInset className="flex flex-col">
|
|
135
|
+
{/* Header with sidebar trigger */}
|
|
136
|
+
{(header || isAuthenticated) && <PrivateHeader header={header} />}
|
|
137
|
+
|
|
138
|
+
{/* Page content */}
|
|
139
|
+
<PrivateContent padding={contentPadding}>{children}</PrivateContent>
|
|
140
|
+
</SidebarInset>
|
|
141
|
+
</SidebarProvider>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private Layout Content
|
|
3
|
+
*
|
|
4
|
+
* Main content wrapper for PrivateLayout
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React, { ReactNode } from 'react';
|
|
10
|
+
import { cn } from '@djangocfg/ui/lib';
|
|
11
|
+
|
|
12
|
+
interface PrivateContentProps {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
padding?: 'none' | 'default';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function PrivateContent({
|
|
18
|
+
children,
|
|
19
|
+
padding = 'default',
|
|
20
|
+
}: PrivateContentProps) {
|
|
21
|
+
return (
|
|
22
|
+
<main
|
|
23
|
+
className={cn(
|
|
24
|
+
'flex-1 overflow-y-auto',
|
|
25
|
+
padding === 'default' && 'p-4 sm:p-6 lg:p-8'
|
|
26
|
+
)}
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</main>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private Layout Header
|
|
3
|
+
*
|
|
4
|
+
* Header component for PrivateLayout with sidebar trigger
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import Link from 'next/link';
|
|
11
|
+
import {
|
|
12
|
+
Button,
|
|
13
|
+
Separator,
|
|
14
|
+
SidebarTrigger,
|
|
15
|
+
} from '@djangocfg/ui/components';
|
|
16
|
+
import { ThemeToggle } from '@djangocfg/ui/theme';
|
|
17
|
+
import { useAuth } from '../../../auth';
|
|
18
|
+
import { UserMenu } from '../../_components/UserMenu';
|
|
19
|
+
import type { HeaderConfig } from '../PrivateLayout';
|
|
20
|
+
|
|
21
|
+
interface PrivateHeaderProps {
|
|
22
|
+
header?: HeaderConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function PrivateHeader({ header }: PrivateHeaderProps) {
|
|
26
|
+
const { user, logout } = useAuth();
|
|
27
|
+
const profilePath = header?.profilePath || '/profile';
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<header className="sticky top-0 z-10 h-16 flex items-center justify-between px-4 shrink-0 bg-background border-b border-border">
|
|
31
|
+
{/* Left side */}
|
|
32
|
+
<div className="flex items-center gap-4">
|
|
33
|
+
<SidebarTrigger className="-ml-1" />
|
|
34
|
+
<Separator orientation="vertical" className="mr-2 h-4" />
|
|
35
|
+
|
|
36
|
+
{header?.title && (
|
|
37
|
+
<h1 className="text-lg font-semibold text-foreground">
|
|
38
|
+
{header.title}
|
|
39
|
+
</h1>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Right side */}
|
|
44
|
+
<div className="flex items-center gap-3">
|
|
45
|
+
{/* Theme Toggle */}
|
|
46
|
+
<ThemeToggle />
|
|
47
|
+
|
|
48
|
+
{/* User Menu */}
|
|
49
|
+
<UserMenu
|
|
50
|
+
variant="desktop"
|
|
51
|
+
profilePath={profilePath}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</header>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private Layout Sidebar
|
|
3
|
+
*
|
|
4
|
+
* Sidebar navigation component for PrivateLayout
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React from 'react';
|
|
10
|
+
import Link from 'next/link';
|
|
11
|
+
import { usePathname } from 'next/navigation';
|
|
12
|
+
import {
|
|
13
|
+
Sidebar,
|
|
14
|
+
SidebarContent,
|
|
15
|
+
SidebarGroup,
|
|
16
|
+
SidebarGroupContent,
|
|
17
|
+
SidebarGroupLabel,
|
|
18
|
+
SidebarHeader,
|
|
19
|
+
SidebarMenu,
|
|
20
|
+
SidebarMenuBadge,
|
|
21
|
+
SidebarMenuButton,
|
|
22
|
+
SidebarMenuItem,
|
|
23
|
+
useSidebar,
|
|
24
|
+
} from '@djangocfg/ui/components';
|
|
25
|
+
import { cn } from '@djangocfg/ui/lib';
|
|
26
|
+
import { LucideIcon } from '../../../components';
|
|
27
|
+
import type { SidebarItem, SidebarConfig } from '../PrivateLayout';
|
|
28
|
+
|
|
29
|
+
interface PrivateSidebarProps {
|
|
30
|
+
sidebar: SidebarConfig;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function PrivateSidebar({ sidebar }: PrivateSidebarProps) {
|
|
34
|
+
const pathname = usePathname();
|
|
35
|
+
const { state, isMobile } = useSidebar();
|
|
36
|
+
const homeHref = sidebar.homeHref || '/';
|
|
37
|
+
|
|
38
|
+
const isActive = (href: string) => {
|
|
39
|
+
const matches = pathname === href || pathname.startsWith(href + '/');
|
|
40
|
+
if (!matches) return false;
|
|
41
|
+
|
|
42
|
+
// Check if there's a more specific (longer) path that also matches
|
|
43
|
+
return !sidebar.items.some(otherItem =>
|
|
44
|
+
otherItem.href !== href &&
|
|
45
|
+
otherItem.href.startsWith(href + '/') &&
|
|
46
|
+
(pathname === otherItem.href || pathname.startsWith(otherItem.href + '/'))
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Sidebar collapsible="icon">
|
|
52
|
+
<SidebarHeader>
|
|
53
|
+
<div
|
|
54
|
+
className="flex items-center gap-3"
|
|
55
|
+
style={
|
|
56
|
+
state === 'collapsed'
|
|
57
|
+
? {
|
|
58
|
+
paddingLeft: '7px',
|
|
59
|
+
paddingTop: '0.5rem',
|
|
60
|
+
paddingBottom: '0.5rem',
|
|
61
|
+
transition: 'padding 200ms ease-in-out',
|
|
62
|
+
}
|
|
63
|
+
: {
|
|
64
|
+
padding: '0.5rem',
|
|
65
|
+
transition: 'padding 200ms ease-in-out',
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
>
|
|
69
|
+
<Link href={homeHref}>
|
|
70
|
+
<div className="flex items-center gap-3">
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
'bg-primary rounded-sm flex items-center justify-center flex-shrink-0',
|
|
74
|
+
isMobile ? 'h-10 w-10' : 'h-8 w-8'
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<span className="text-primary-foreground font-bold text-sm">
|
|
78
|
+
D
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
{state !== 'collapsed' && (
|
|
82
|
+
<span
|
|
83
|
+
className={cn(
|
|
84
|
+
'font-semibold text-foreground truncate',
|
|
85
|
+
isMobile && 'text-base'
|
|
86
|
+
)}
|
|
87
|
+
style={{ whiteSpace: 'nowrap' }}
|
|
88
|
+
>
|
|
89
|
+
Dashboard
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</Link>
|
|
94
|
+
</div>
|
|
95
|
+
</SidebarHeader>
|
|
96
|
+
|
|
97
|
+
<SidebarContent>
|
|
98
|
+
<SidebarGroup>
|
|
99
|
+
<SidebarGroupContent>
|
|
100
|
+
<SidebarMenu>
|
|
101
|
+
{sidebar.items.map((item) => {
|
|
102
|
+
const active = isActive(item.href);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<SidebarMenuItem key={item.href}>
|
|
106
|
+
<SidebarMenuButton
|
|
107
|
+
asChild
|
|
108
|
+
isActive={active}
|
|
109
|
+
tooltip={item.label}
|
|
110
|
+
size={isMobile ? 'lg' : 'default'}
|
|
111
|
+
>
|
|
112
|
+
<Link href={item.href}>
|
|
113
|
+
{item.icon && (
|
|
114
|
+
<LucideIcon
|
|
115
|
+
icon={
|
|
116
|
+
typeof item.icon === 'string'
|
|
117
|
+
? item.icon
|
|
118
|
+
: item.icon
|
|
119
|
+
}
|
|
120
|
+
className={isMobile ? 'h-5 w-5' : 'h-4 w-4'}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
<span className={isMobile ? 'text-base' : ''}>
|
|
124
|
+
{item.label}
|
|
125
|
+
</span>
|
|
126
|
+
{item.badge && (
|
|
127
|
+
<SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
|
|
128
|
+
)}
|
|
129
|
+
</Link>
|
|
130
|
+
</SidebarMenuButton>
|
|
131
|
+
</SidebarMenuItem>
|
|
132
|
+
);
|
|
133
|
+
})}
|
|
134
|
+
</SidebarMenu>
|
|
135
|
+
</SidebarGroupContent>
|
|
136
|
+
</SidebarGroup>
|
|
137
|
+
</SidebarContent>
|
|
138
|
+
</Sidebar>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
@@ -43,13 +43,15 @@ const ProfileContent = ({
|
|
|
43
43
|
});
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (onUnauthenticated) {
|
|
48
|
+
onUnauthenticated();
|
|
49
|
+
}
|
|
50
|
+
}, [onUnauthenticated]);
|
|
51
|
+
|
|
52
|
+
|
|
46
53
|
// Show auth check if no user
|
|
47
54
|
if (!user && !isLoading) {
|
|
48
|
-
React.useEffect(() => {
|
|
49
|
-
if (onUnauthenticated) {
|
|
50
|
-
onUnauthenticated();
|
|
51
|
-
}
|
|
52
|
-
}, [onUnauthenticated]);
|
|
53
55
|
|
|
54
56
|
return (
|
|
55
57
|
<div className="flex items-center justify-center min-h-screen">
|
|
@@ -63,8 +65,14 @@ const ProfileContent = ({
|
|
|
63
65
|
|
|
64
66
|
if (isLoading) {
|
|
65
67
|
return (
|
|
66
|
-
<div
|
|
67
|
-
<
|
|
68
|
+
<div>
|
|
69
|
+
<Preloader
|
|
70
|
+
variant="fullscreen"
|
|
71
|
+
text="Authenticating..."
|
|
72
|
+
size="lg"
|
|
73
|
+
backdrop={true}
|
|
74
|
+
backdropOpacity={80}
|
|
75
|
+
/>
|
|
68
76
|
</div>
|
|
69
77
|
);
|
|
70
78
|
}
|