@djangocfg/layouts 2.1.108 → 2.1.110
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 +16 -9
- package/package.json +15 -15
- package/src/layouts/AuthLayout/AuthLayout.tsx +92 -20
- package/src/layouts/AuthLayout/components/index.ts +11 -7
- package/src/layouts/AuthLayout/components/oauth/index.ts +0 -1
- package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +35 -0
- package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +56 -0
- package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +22 -0
- package/src/layouts/AuthLayout/components/shared/AuthError.tsx +26 -0
- package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +47 -0
- package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +53 -0
- package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +41 -0
- package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +42 -0
- package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +48 -0
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +57 -0
- package/src/layouts/AuthLayout/components/shared/index.ts +21 -0
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +171 -0
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +114 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +70 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +24 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +125 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +91 -0
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +92 -0
- package/src/layouts/AuthLayout/components/steps/index.ts +6 -0
- package/src/layouts/AuthLayout/constants.ts +24 -0
- package/src/layouts/AuthLayout/content.ts +78 -0
- package/src/layouts/AuthLayout/hooks/index.ts +1 -0
- package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +37 -0
- package/src/layouts/AuthLayout/index.ts +9 -5
- package/src/layouts/AuthLayout/styles/auth.css +578 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +13 -1
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +59 -46
- package/src/layouts/PrivateLayout/index.ts +1 -1
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +2 -2
- package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +2 -2
- package/src/layouts/AuthLayout/components/AuthHelp.tsx +0 -114
- package/src/layouts/AuthLayout/components/AuthSuccess.tsx +0 -101
- package/src/layouts/AuthLayout/components/IdentifierForm.tsx +0 -322
- package/src/layouts/AuthLayout/components/OTPForm.tsx +0 -174
- package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +0 -140
- package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +0 -286
- package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +0 -56
|
@@ -18,7 +18,7 @@ import { cn } from '@djangocfg/ui-core/lib';
|
|
|
18
18
|
|
|
19
19
|
import { LucideIcon } from '../../../components';
|
|
20
20
|
|
|
21
|
-
import type { SidebarItem, SidebarConfig } from '../PrivateLayout';
|
|
21
|
+
import type { SidebarItem, SidebarConfig, SidebarGroupConfig } from '../PrivateLayout';
|
|
22
22
|
|
|
23
23
|
interface PrivateSidebarProps {
|
|
24
24
|
sidebar: SidebarConfig;
|
|
@@ -29,18 +29,71 @@ export function PrivateSidebar({ sidebar }: PrivateSidebarProps) {
|
|
|
29
29
|
const { state, isMobile } = useSidebar();
|
|
30
30
|
const homeHref = sidebar.homeHref || '/';
|
|
31
31
|
|
|
32
|
+
// Get all items for active detection
|
|
33
|
+
const allItems = React.useMemo(() => {
|
|
34
|
+
return sidebar.groups.flatMap((g) => g.items);
|
|
35
|
+
}, [sidebar.groups]);
|
|
36
|
+
|
|
32
37
|
const isActive = (href: string) => {
|
|
33
38
|
const matches = pathname === href || pathname.startsWith(href + '/');
|
|
34
39
|
if (!matches) return false;
|
|
35
40
|
|
|
36
41
|
// Check if there's a more specific (longer) path that also matches
|
|
37
|
-
return !
|
|
38
|
-
otherItem
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
return !allItems.some(
|
|
43
|
+
(otherItem) =>
|
|
44
|
+
otherItem.href !== href &&
|
|
45
|
+
otherItem.href.startsWith(href + '/') &&
|
|
46
|
+
(pathname === otherItem.href ||
|
|
47
|
+
pathname.startsWith(otherItem.href + '/'))
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Render a single menu item
|
|
52
|
+
const renderMenuItem = (item: SidebarItem) => {
|
|
53
|
+
const active = isActive(item.href);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<SidebarMenuItem key={item.href}>
|
|
57
|
+
<SidebarMenuButton
|
|
58
|
+
asChild
|
|
59
|
+
isActive={active}
|
|
60
|
+
tooltip={item.label}
|
|
61
|
+
size={isMobile ? 'lg' : 'default'}
|
|
62
|
+
>
|
|
63
|
+
<Link href={item.href}>
|
|
64
|
+
{item.icon && (
|
|
65
|
+
<LucideIcon
|
|
66
|
+
icon={typeof item.icon === 'string' ? item.icon : item.icon}
|
|
67
|
+
className={isMobile ? 'h-5 w-5' : 'h-4 w-4'}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
<span className={isMobile ? 'text-base' : ''}>{item.label}</span>
|
|
71
|
+
{item.badge && <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>}
|
|
72
|
+
</Link>
|
|
73
|
+
</SidebarMenuButton>
|
|
74
|
+
</SidebarMenuItem>
|
|
41
75
|
);
|
|
42
76
|
};
|
|
43
77
|
|
|
78
|
+
// Render groups
|
|
79
|
+
const renderContent = () => {
|
|
80
|
+
return sidebar.groups.map((group) => {
|
|
81
|
+
// Skip dynamic groups with no items
|
|
82
|
+
if (group.dynamic && group.items.length === 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<SidebarGroup key={group.label}>
|
|
88
|
+
<SidebarGroupLabel className="font-medium text-[10px]">{group.label}</SidebarGroupLabel>
|
|
89
|
+
<SidebarGroupContent>
|
|
90
|
+
<SidebarMenu>{group.items.map(renderMenuItem)}</SidebarMenu>
|
|
91
|
+
</SidebarGroupContent>
|
|
92
|
+
</SidebarGroup>
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
|
|
44
97
|
return (
|
|
45
98
|
<Sidebar collapsible="icon">
|
|
46
99
|
<SidebarHeader>
|
|
@@ -88,47 +141,7 @@ export function PrivateSidebar({ sidebar }: PrivateSidebarProps) {
|
|
|
88
141
|
</div>
|
|
89
142
|
</SidebarHeader>
|
|
90
143
|
|
|
91
|
-
<SidebarContent>
|
|
92
|
-
<SidebarGroup>
|
|
93
|
-
<SidebarGroupContent>
|
|
94
|
-
<SidebarMenu>
|
|
95
|
-
{sidebar.items.map((item) => {
|
|
96
|
-
const active = isActive(item.href);
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<SidebarMenuItem key={item.href}>
|
|
100
|
-
<SidebarMenuButton
|
|
101
|
-
asChild
|
|
102
|
-
isActive={active}
|
|
103
|
-
tooltip={item.label}
|
|
104
|
-
size={isMobile ? 'lg' : 'default'}
|
|
105
|
-
>
|
|
106
|
-
<Link href={item.href}>
|
|
107
|
-
{item.icon && (
|
|
108
|
-
<LucideIcon
|
|
109
|
-
icon={
|
|
110
|
-
typeof item.icon === 'string'
|
|
111
|
-
? item.icon
|
|
112
|
-
: item.icon
|
|
113
|
-
}
|
|
114
|
-
className={isMobile ? 'h-5 w-5' : 'h-4 w-4'}
|
|
115
|
-
/>
|
|
116
|
-
)}
|
|
117
|
-
<span className={isMobile ? 'text-base' : ''}>
|
|
118
|
-
{item.label}
|
|
119
|
-
</span>
|
|
120
|
-
{item.badge && (
|
|
121
|
-
<SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
|
|
122
|
-
)}
|
|
123
|
-
</Link>
|
|
124
|
-
</SidebarMenuButton>
|
|
125
|
-
</SidebarMenuItem>
|
|
126
|
-
);
|
|
127
|
-
})}
|
|
128
|
-
</SidebarMenu>
|
|
129
|
-
</SidebarGroupContent>
|
|
130
|
-
</SidebarGroup>
|
|
131
|
-
</SidebarContent>
|
|
144
|
+
<SidebarContent>{renderContent()}</SidebarContent>
|
|
132
145
|
</Sidebar>
|
|
133
146
|
);
|
|
134
147
|
}
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { PrivateLayout } from './PrivateLayout';
|
|
6
|
-
export type { PrivateLayoutProps, SidebarItem, SidebarConfig, HeaderConfig } from './PrivateLayout';
|
|
6
|
+
export type { PrivateLayoutProps, SidebarItem, SidebarGroupConfig, SidebarConfig, HeaderConfig } from './PrivateLayout';
|
|
7
7
|
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from '@djangocfg/ui-core/components';
|
|
29
29
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
30
30
|
|
|
31
|
-
import {
|
|
31
|
+
import { SetupStep } from '../AuthLayout/components/steps/SetupStep';
|
|
32
32
|
import { profileLogger } from '../../utils/logger';
|
|
33
33
|
|
|
34
34
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -314,7 +314,7 @@ const ProfileContent = ({
|
|
|
314
314
|
if (show2FASetup) {
|
|
315
315
|
return (
|
|
316
316
|
<div className="container mx-auto px-4 py-8 max-w-lg">
|
|
317
|
-
<
|
|
317
|
+
<SetupStep
|
|
318
318
|
onComplete={() => {
|
|
319
319
|
setShow2FASetup(false);
|
|
320
320
|
fetch2FAStatus();
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
OTPInput,
|
|
23
23
|
} from '@djangocfg/ui-core/components';
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import { SetupStep } from '../../AuthLayout/components/steps/SetupStep';
|
|
26
26
|
|
|
27
27
|
type ViewState = 'status' | 'setup' | 'disable';
|
|
28
28
|
|
|
@@ -102,7 +102,7 @@ export const TwoFactorSection: React.FC = () => {
|
|
|
102
102
|
</CardDescription>
|
|
103
103
|
</CardHeader>
|
|
104
104
|
<CardContent>
|
|
105
|
-
<
|
|
105
|
+
<SetupStep
|
|
106
106
|
onComplete={handleSetupComplete}
|
|
107
107
|
onSkip={handleSetupSkip}
|
|
108
108
|
/>
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { HelpCircle, Mail, MessageCircle } from 'lucide-react';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
import { Button } from '@djangocfg/ui-core/components';
|
|
5
|
-
|
|
6
|
-
import { useAuthFormContext } from '../context';
|
|
7
|
-
|
|
8
|
-
import type { AuthHelpProps } from '../types';
|
|
9
|
-
|
|
10
|
-
export const AuthHelp: React.FC<AuthHelpProps> = ({
|
|
11
|
-
className = '',
|
|
12
|
-
variant = 'default',
|
|
13
|
-
}) => {
|
|
14
|
-
const { supportUrl, channel } = useAuthFormContext();
|
|
15
|
-
|
|
16
|
-
const getChannelIcon = () => {
|
|
17
|
-
return channel === 'phone' ? (
|
|
18
|
-
<MessageCircle className="w-4 h-4 text-muted-foreground" />
|
|
19
|
-
) : (
|
|
20
|
-
<Mail className="w-4 h-4 text-muted-foreground" />
|
|
21
|
-
);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const getHelpText = () => {
|
|
25
|
-
return channel === 'phone' ? 'Check WhatsApp/SMS' : 'Check spam folder';
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const getDetailedHelp = () => {
|
|
29
|
-
if (channel === 'phone') {
|
|
30
|
-
return {
|
|
31
|
-
title: "Didn't receive the code?",
|
|
32
|
-
tips: [
|
|
33
|
-
'• Check your WhatsApp messages',
|
|
34
|
-
'• Look for SMS messages',
|
|
35
|
-
'• Ensure you have signal/internet',
|
|
36
|
-
'• Wait a few minutes for delivery',
|
|
37
|
-
],
|
|
38
|
-
};
|
|
39
|
-
} else {
|
|
40
|
-
return {
|
|
41
|
-
title: "Didn't receive the email?",
|
|
42
|
-
tips: [
|
|
43
|
-
'• Check your spam or junk folder',
|
|
44
|
-
'• Make sure you entered the correct email address',
|
|
45
|
-
'• Wait a few minutes for the email to arrive',
|
|
46
|
-
],
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
if (variant === 'compact') {
|
|
52
|
-
return (
|
|
53
|
-
<div
|
|
54
|
-
className={`flex items-center justify-between p-3 bg-muted/30 rounded-sm border border-border ${className}`}
|
|
55
|
-
>
|
|
56
|
-
<div className="flex items-center gap-2">
|
|
57
|
-
{getChannelIcon()}
|
|
58
|
-
<span className="text-sm text-muted-foreground">{getHelpText()}</span>
|
|
59
|
-
</div>
|
|
60
|
-
{supportUrl && (
|
|
61
|
-
<Button
|
|
62
|
-
asChild
|
|
63
|
-
variant="ghost"
|
|
64
|
-
size="sm"
|
|
65
|
-
className="text-xs"
|
|
66
|
-
>
|
|
67
|
-
<a href={supportUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1">
|
|
68
|
-
<HelpCircle className="w-3 h-3" />
|
|
69
|
-
Need help?
|
|
70
|
-
</a>
|
|
71
|
-
</Button>
|
|
72
|
-
)}
|
|
73
|
-
</div>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const helpData = getDetailedHelp();
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<div
|
|
81
|
-
className={`flex flex-col gap-3 p-3 bg-muted/30 rounded-sm border border-border ${className}`}
|
|
82
|
-
>
|
|
83
|
-
<div className="flex items-start gap-3">
|
|
84
|
-
{getChannelIcon()}
|
|
85
|
-
<div className="flex flex-col gap-1">
|
|
86
|
-
<h4 className="text-sm font-medium text-foreground">{helpData.title}</h4>
|
|
87
|
-
<div className="flex flex-col gap-0.5 text-xs text-muted-foreground">
|
|
88
|
-
{helpData.tips.map((tip, index) => (
|
|
89
|
-
<p key={index}>{tip}</p>
|
|
90
|
-
))}
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
{supportUrl && (
|
|
96
|
-
<div className="flex items-center justify-between pt-2 border-t border-border">
|
|
97
|
-
<span className="text-xs text-muted-foreground">Still having trouble?</span>
|
|
98
|
-
<Button
|
|
99
|
-
asChild
|
|
100
|
-
variant="ghost"
|
|
101
|
-
size="sm"
|
|
102
|
-
className="text-xs h-7 px-2"
|
|
103
|
-
>
|
|
104
|
-
<a href={supportUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1">
|
|
105
|
-
<HelpCircle className="w-3 h-3" />
|
|
106
|
-
Get Help
|
|
107
|
-
</a>
|
|
108
|
-
</Button>
|
|
109
|
-
</div>
|
|
110
|
-
)}
|
|
111
|
-
</div>
|
|
112
|
-
);
|
|
113
|
-
};
|
|
114
|
-
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth Success Component
|
|
3
|
-
*
|
|
4
|
-
* Full-screen success layout shown after successful authentication.
|
|
5
|
-
* Displays a centered logo with a subtle animation, then redirects.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
'use client';
|
|
9
|
-
|
|
10
|
-
import React, { useEffect, useState } from 'react';
|
|
11
|
-
|
|
12
|
-
import { useCfgRouter } from '@djangocfg/api/auth';
|
|
13
|
-
|
|
14
|
-
import { useAuthFormContext } from '../context';
|
|
15
|
-
|
|
16
|
-
export interface AuthSuccessProps {
|
|
17
|
-
className?: string;
|
|
18
|
-
/** Delay before redirect in ms (default: 1500) */
|
|
19
|
-
redirectDelay?: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const AuthSuccess: React.FC<AuthSuccessProps> = ({ className, redirectDelay = 1500 }) => {
|
|
23
|
-
const { logoUrl, redirectUrl } = useAuthFormContext();
|
|
24
|
-
const router = useCfgRouter();
|
|
25
|
-
const [isVisible, setIsVisible] = useState(false);
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
// Trigger animation after mount
|
|
29
|
-
const animTimer = setTimeout(() => setIsVisible(true), 50);
|
|
30
|
-
|
|
31
|
-
// Redirect after delay
|
|
32
|
-
const redirectTimer = setTimeout(() => {
|
|
33
|
-
const finalUrl = redirectUrl || '/dashboard';
|
|
34
|
-
router.hardPush(finalUrl);
|
|
35
|
-
}, redirectDelay);
|
|
36
|
-
|
|
37
|
-
return () => {
|
|
38
|
-
clearTimeout(animTimer);
|
|
39
|
-
clearTimeout(redirectTimer);
|
|
40
|
-
};
|
|
41
|
-
}, [redirectUrl, redirectDelay, router]);
|
|
42
|
-
|
|
43
|
-
if (!logoUrl) {
|
|
44
|
-
// Fallback: simple checkmark if no logo provided
|
|
45
|
-
return (
|
|
46
|
-
<div className={`fixed inset-0 flex items-center justify-center bg-background z-50 ${className || ''}`}>
|
|
47
|
-
<div
|
|
48
|
-
className={`transition-all duration-700 ease-out ${
|
|
49
|
-
isVisible ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
|
|
50
|
-
}`}
|
|
51
|
-
>
|
|
52
|
-
<div className="w-24 h-24 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
|
|
53
|
-
<svg
|
|
54
|
-
className="w-12 h-12 text-green-600 dark:text-green-400"
|
|
55
|
-
fill="none"
|
|
56
|
-
stroke="currentColor"
|
|
57
|
-
viewBox="0 0 24 24"
|
|
58
|
-
>
|
|
59
|
-
<path
|
|
60
|
-
strokeLinecap="round"
|
|
61
|
-
strokeLinejoin="round"
|
|
62
|
-
strokeWidth={2}
|
|
63
|
-
d="M5 13l4 4L19 7"
|
|
64
|
-
/>
|
|
65
|
-
</svg>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<div className={`fixed inset-0 flex items-center justify-center bg-background z-50 ${className || ''}`}>
|
|
74
|
-
<div
|
|
75
|
-
className={`transition-all duration-700 ease-out ${
|
|
76
|
-
isVisible ? 'opacity-100 scale-100' : 'opacity-0 scale-90'
|
|
77
|
-
}`}
|
|
78
|
-
>
|
|
79
|
-
{/* Logo container with max size and animation */}
|
|
80
|
-
<div className="relative">
|
|
81
|
-
{/* Subtle glow effect */}
|
|
82
|
-
<div
|
|
83
|
-
className={`absolute inset-0 blur-3xl transition-opacity duration-1000 ${
|
|
84
|
-
isVisible ? 'opacity-20' : 'opacity-0'
|
|
85
|
-
}`}
|
|
86
|
-
style={{
|
|
87
|
-
background: 'radial-gradient(circle, currentColor 0%, transparent 70%)',
|
|
88
|
-
}}
|
|
89
|
-
/>
|
|
90
|
-
|
|
91
|
-
{/* Logo image */}
|
|
92
|
-
<img
|
|
93
|
-
src={logoUrl}
|
|
94
|
-
alt="Success"
|
|
95
|
-
className="relative w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 object-contain"
|
|
96
|
-
/>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
};
|