@djangocfg/layouts 1.2.1 → 1.2.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/package.json +5 -5
- package/src/layouts/AppLayout/AppLayout.tsx +16 -3
- package/src/layouts/AppLayout/components/PackageVersions/PackageVersions.tsx +101 -0
- package/src/layouts/AppLayout/components/PackageVersions/index.ts +7 -0
- package/src/layouts/AppLayout/components/PackageVersions/packageVersions.config.ts +65 -0
- package/src/layouts/AppLayout/components/UserMenu.tsx +340 -0
- package/src/layouts/AppLayout/components/index.ts +4 -0
- package/src/layouts/AppLayout/context/AppContext.tsx +21 -16
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +2 -2
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +7 -3
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +9 -2
- package/src/layouts/AppLayout/layouts/PublicLayout/components/{MobileMenu.tsx → MobileDrawer.tsx} +13 -30
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +7 -9
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +15 -6
- package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +0 -136
- package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenuUserCard.tsx +0 -150
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Layout system and components for Unrealon applications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DjangoCFG",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"check": "tsc --noEmit"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@djangocfg/api": "^1.2.
|
|
57
|
-
"@djangocfg/og-image": "^1.2.
|
|
58
|
-
"@djangocfg/ui": "^1.2.
|
|
56
|
+
"@djangocfg/api": "^1.2.2",
|
|
57
|
+
"@djangocfg/og-image": "^1.2.2",
|
|
58
|
+
"@djangocfg/ui": "^1.2.2",
|
|
59
59
|
"@hookform/resolvers": "^5.2.0",
|
|
60
60
|
"consola": "^3.4.2",
|
|
61
61
|
"lucide-react": "^0.468.0",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"vidstack": "0.6.15"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@djangocfg/typescript-config": "^1.2.
|
|
79
|
+
"@djangocfg/typescript-config": "^1.2.2",
|
|
80
80
|
"@types/node": "^24.7.2",
|
|
81
81
|
"@types/react": "19.2.2",
|
|
82
82
|
"@types/react-dom": "19.2.1",
|
|
@@ -51,6 +51,12 @@ export interface AppLayoutProps {
|
|
|
51
51
|
* @example fontFamily="Inter, sans-serif"
|
|
52
52
|
*/
|
|
53
53
|
fontFamily?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Show package versions button in sidebar footer
|
|
56
|
+
* @default false
|
|
57
|
+
* @example showPackageVersions={true}
|
|
58
|
+
*/
|
|
59
|
+
showPackageVersions?: boolean;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
/**
|
|
@@ -115,6 +121,9 @@ function LayoutRouter({
|
|
|
115
121
|
|
|
116
122
|
// Auth routes: render inside AuthLayout
|
|
117
123
|
case 'auth':
|
|
124
|
+
// Check if we're on a private route that requires auth
|
|
125
|
+
const isPrivateRoute = config.routes.detectors.isPrivateRoute(router.pathname);
|
|
126
|
+
|
|
118
127
|
return (
|
|
119
128
|
<AuthLayout
|
|
120
129
|
termsUrl={config.auth?.termsUrl}
|
|
@@ -122,7 +131,8 @@ function LayoutRouter({
|
|
|
122
131
|
supportUrl={config.auth?.supportUrl}
|
|
123
132
|
enablePhoneAuth={config.auth?.enablePhoneAuth}
|
|
124
133
|
>
|
|
125
|
-
{children}
|
|
134
|
+
{/* Don't render children if redirected from private route */}
|
|
135
|
+
{!isPrivateRoute && children}
|
|
126
136
|
</AuthLayout>
|
|
127
137
|
);
|
|
128
138
|
|
|
@@ -136,6 +146,9 @@ function LayoutRouter({
|
|
|
136
146
|
);
|
|
137
147
|
}
|
|
138
148
|
return <PrivateLayout>{children}</PrivateLayout>;
|
|
149
|
+
|
|
150
|
+
default:
|
|
151
|
+
return <PublicLayout>{children}</PublicLayout>;
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
154
|
|
|
@@ -168,7 +181,7 @@ function LayoutRouter({
|
|
|
168
181
|
* </AppLayout>
|
|
169
182
|
* ```
|
|
170
183
|
*/
|
|
171
|
-
export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily }: AppLayoutProps) {
|
|
184
|
+
export function AppLayout({ children, config, disableLayout = false, forceLayout, fontFamily, showPackageVersions }: AppLayoutProps) {
|
|
172
185
|
const router = useRouter();
|
|
173
186
|
|
|
174
187
|
// Check if ErrorBoundary is enabled (default: true)
|
|
@@ -186,7 +199,7 @@ export function AppLayout({ children, config, disableLayout = false, forceLayout
|
|
|
186
199
|
)}
|
|
187
200
|
|
|
188
201
|
<CoreProviders config={config}>
|
|
189
|
-
<AppContextProvider config={config}>
|
|
202
|
+
<AppContextProvider config={config} showPackageVersions={showPackageVersions}>
|
|
190
203
|
{/* SEO Meta Tags */}
|
|
191
204
|
<Seo
|
|
192
205
|
pageConfig={{
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Versions Display
|
|
3
|
+
*
|
|
4
|
+
* Shows all @djangocfg packages versions in a popover
|
|
5
|
+
* Works in both sidebar (PrivateLayout) and footer (PublicLayout)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { Info, Package } from 'lucide-react';
|
|
12
|
+
import {
|
|
13
|
+
Popover,
|
|
14
|
+
PopoverContent,
|
|
15
|
+
PopoverTrigger,
|
|
16
|
+
} from '@djangocfg/ui/components';
|
|
17
|
+
import { Button } from '@djangocfg/ui/components';
|
|
18
|
+
import { getPackageVersions } from './packageVersions.config';
|
|
19
|
+
|
|
20
|
+
export interface PackageVersionsProps {
|
|
21
|
+
/**
|
|
22
|
+
* Display variant
|
|
23
|
+
* - 'sidebar': Adapts to sidebar collapsed state (PrivateLayout)
|
|
24
|
+
* - 'footer': Simple button for footer (PublicLayout)
|
|
25
|
+
* - 'footer-minimal': Only icon, no text (for compact footer)
|
|
26
|
+
*/
|
|
27
|
+
variant?: 'sidebar' | 'footer' | 'footer-minimal';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function PackageVersions({ variant = 'footer' }: PackageVersionsProps) {
|
|
31
|
+
// Try to use sidebar state if available (only in PrivateLayout)
|
|
32
|
+
let isCollapsed = false;
|
|
33
|
+
if (variant === 'sidebar') {
|
|
34
|
+
try {
|
|
35
|
+
// Dynamic import to avoid errors in PublicLayout
|
|
36
|
+
const { useSidebar } = require('@djangocfg/ui/components');
|
|
37
|
+
const { state } = useSidebar();
|
|
38
|
+
isCollapsed = state === 'collapsed';
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// Sidebar not available, use default
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const isSidebarVariant = variant === 'sidebar';
|
|
45
|
+
const isMinimalVariant = variant === 'footer-minimal';
|
|
46
|
+
const popoverAlign = isSidebarVariant ? 'start' : 'center';
|
|
47
|
+
const popoverSide = isSidebarVariant ? 'right' : 'top';
|
|
48
|
+
|
|
49
|
+
// Determine if we should show text
|
|
50
|
+
const showText = !isCollapsed && !isMinimalVariant;
|
|
51
|
+
|
|
52
|
+
// Get package versions dynamically
|
|
53
|
+
const packages = getPackageVersions();
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Popover>
|
|
57
|
+
<PopoverTrigger asChild>
|
|
58
|
+
<Button
|
|
59
|
+
variant="ghost"
|
|
60
|
+
size={isCollapsed || isMinimalVariant ? "icon" : "sm"}
|
|
61
|
+
className={isSidebarVariant
|
|
62
|
+
? "w-full justify-start text-xs text-muted-foreground hover:text-foreground"
|
|
63
|
+
: isMinimalVariant
|
|
64
|
+
? "h-auto w-auto p-1 text-muted-foreground hover:text-primary transition-colors"
|
|
65
|
+
: "text-xs text-muted-foreground hover:text-foreground"
|
|
66
|
+
}
|
|
67
|
+
title={isMinimalVariant ? "Package Versions" : undefined}
|
|
68
|
+
>
|
|
69
|
+
<Info className={isMinimalVariant ? "h-3 w-3" : "h-3.5 w-3.5"} />
|
|
70
|
+
{showText && <span className="ml-2">Package Versions</span>}
|
|
71
|
+
</Button>
|
|
72
|
+
</PopoverTrigger>
|
|
73
|
+
<PopoverContent align={popoverAlign} side={popoverSide} className="w-80">
|
|
74
|
+
<div className="space-y-3">
|
|
75
|
+
<div className="flex items-center gap-2">
|
|
76
|
+
<Package className="h-4 w-4 text-primary" />
|
|
77
|
+
<h4 className="font-semibold text-sm">Package Versions</h4>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="space-y-1.5">
|
|
80
|
+
{packages.map((pkg) => (
|
|
81
|
+
<a
|
|
82
|
+
key={pkg.name}
|
|
83
|
+
href={`https://www.npmjs.com/package/${pkg.name}`}
|
|
84
|
+
target="_blank"
|
|
85
|
+
rel="noopener noreferrer"
|
|
86
|
+
className="flex items-center justify-between py-1.5 px-2 rounded-sm hover:bg-accent/50 transition-colors cursor-pointer group"
|
|
87
|
+
>
|
|
88
|
+
<span className="text-xs font-mono text-muted-foreground group-hover:text-foreground transition-colors">
|
|
89
|
+
{pkg.name}
|
|
90
|
+
</span>
|
|
91
|
+
<span className="text-xs font-semibold bg-primary/10 text-primary px-2 py-0.5 rounded group-hover:bg-primary/20 transition-colors">
|
|
92
|
+
v{pkg.version}
|
|
93
|
+
</span>
|
|
94
|
+
</a>
|
|
95
|
+
))}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</PopoverContent>
|
|
99
|
+
</Popover>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Versions Configuration
|
|
3
|
+
*
|
|
4
|
+
* NOTE: This file is auto-generated by packages/scripts/sync-package-versions.js
|
|
5
|
+
* Do not edit manually! Run 'make build' or 'pnpm sync-versions' to update.
|
|
6
|
+
*
|
|
7
|
+
* Versions are synced from actual package.json files in the monorepo.
|
|
8
|
+
* This ensures compatibility with both monorepo (dev) and npm (production).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface PackageInfo {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Package versions registry
|
|
18
|
+
* Auto-synced from package.json files
|
|
19
|
+
* Last updated: 2025-10-23T06:57:54.947Z
|
|
20
|
+
*/
|
|
21
|
+
const PACKAGE_VERSIONS: PackageInfo[] = [
|
|
22
|
+
{
|
|
23
|
+
"name": "@djangocfg/ui",
|
|
24
|
+
"version": "1.2.2"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "@djangocfg/api",
|
|
28
|
+
"version": "1.2.2"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "@djangocfg/layouts",
|
|
32
|
+
"version": "1.2.2"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "@djangocfg/markdown",
|
|
36
|
+
"version": "1.2.2"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "@djangocfg/og-image",
|
|
40
|
+
"version": "1.2.2"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "@djangocfg/eslint-config",
|
|
44
|
+
"version": "1.2.2"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "@djangocfg/typescript-config",
|
|
48
|
+
"version": "1.2.2"
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get all package versions
|
|
54
|
+
*/
|
|
55
|
+
export function getPackageVersions(): PackageInfo[] {
|
|
56
|
+
return PACKAGE_VERSIONS;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get single package version by name
|
|
61
|
+
*/
|
|
62
|
+
export function getPackageVersion(packageName: string): string | undefined {
|
|
63
|
+
const packages = getPackageVersions();
|
|
64
|
+
return packages.find((pkg) => pkg.name === packageName)?.version;
|
|
65
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal User Menu Component
|
|
3
|
+
*
|
|
4
|
+
* Single unified component for both Desktop and Mobile user menus
|
|
5
|
+
* Uses AppContext and Auth context directly - no prop drilling!
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { useRouter } from 'next/router';
|
|
12
|
+
import { User, ChevronDown, Crown, LogOut, Settings } from 'lucide-react';
|
|
13
|
+
import { Button, ButtonLink, Card, CardContent } from '@djangocfg/ui/components';
|
|
14
|
+
import { ThemeToggle } from '@djangocfg/ui/theme';
|
|
15
|
+
import { useAppContext } from '../context';
|
|
16
|
+
import { useAuth } from '../../../auth';
|
|
17
|
+
|
|
18
|
+
export interface UserMenuProps {
|
|
19
|
+
variant: 'desktop' | 'mobile';
|
|
20
|
+
/** Mobile only: callback when navigation happens */
|
|
21
|
+
onNavigate?: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface MenuItem {
|
|
25
|
+
id: string;
|
|
26
|
+
type: 'link' | 'button';
|
|
27
|
+
icon: React.ReactNode;
|
|
28
|
+
label: string;
|
|
29
|
+
href?: string;
|
|
30
|
+
onClick?: () => void;
|
|
31
|
+
variant?: 'default' | 'ghost' | 'destructive';
|
|
32
|
+
className?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function UserMenu({ variant, onNavigate }: UserMenuProps) {
|
|
36
|
+
const router = useRouter();
|
|
37
|
+
const { config, userMenuOpen, toggleUserMenu, closeUserMenu } = useAppContext();
|
|
38
|
+
const { user, isAuthenticated, logout } = useAuth();
|
|
39
|
+
|
|
40
|
+
const { publicLayout, routes } = config;
|
|
41
|
+
|
|
42
|
+
// Desktop: determine if user is on dashboard
|
|
43
|
+
const isDashboard = variant === 'desktop' && publicLayout.userMenu.dashboardPath
|
|
44
|
+
? router.pathname.includes(publicLayout.userMenu.dashboardPath)
|
|
45
|
+
: false;
|
|
46
|
+
|
|
47
|
+
// === DATA PREPARATION (before rendering) ===
|
|
48
|
+
|
|
49
|
+
// Handle logout
|
|
50
|
+
const handleLogout = React.useCallback(() => {
|
|
51
|
+
logout();
|
|
52
|
+
if (variant === 'desktop') {
|
|
53
|
+
closeUserMenu();
|
|
54
|
+
} else if (onNavigate) {
|
|
55
|
+
onNavigate();
|
|
56
|
+
}
|
|
57
|
+
}, [logout, variant, closeUserMenu, onNavigate]);
|
|
58
|
+
|
|
59
|
+
// Prepare menu items
|
|
60
|
+
const menuItems = React.useMemo<MenuItem[]>(() => {
|
|
61
|
+
const items: MenuItem[] = [];
|
|
62
|
+
|
|
63
|
+
// Profile
|
|
64
|
+
items.push({
|
|
65
|
+
id: 'profile',
|
|
66
|
+
type: 'link',
|
|
67
|
+
icon: <Settings className="size-4" />,
|
|
68
|
+
label: 'Profile',
|
|
69
|
+
href: publicLayout.userMenu.profilePath,
|
|
70
|
+
variant: 'ghost',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Logout
|
|
74
|
+
items.push({
|
|
75
|
+
id: 'logout',
|
|
76
|
+
type: 'button',
|
|
77
|
+
icon: <LogOut className="size-4" />,
|
|
78
|
+
label: 'Sign out',
|
|
79
|
+
onClick: handleLogout,
|
|
80
|
+
variant: 'ghost',
|
|
81
|
+
className: 'text-destructive hover:text-destructive hover:bg-destructive/10',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return items;
|
|
85
|
+
}, [publicLayout.userMenu.profilePath, handleLogout]);
|
|
86
|
+
|
|
87
|
+
// === UNIFIED MENU ITEM RENDERER ===
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Renders a single menu item (link or button)
|
|
91
|
+
* Used for both desktop dropdown and mobile icons
|
|
92
|
+
*/
|
|
93
|
+
const renderMenuItem = React.useCallback((
|
|
94
|
+
item: MenuItem,
|
|
95
|
+
mode: 'desktop' | 'mobile-icon',
|
|
96
|
+
onClick?: () => void
|
|
97
|
+
) => {
|
|
98
|
+
if (mode === 'mobile-icon') {
|
|
99
|
+
// Mobile: render as icon-only button
|
|
100
|
+
const iconElement = item.icon as React.ReactElement;
|
|
101
|
+
const resizedIcon = React.isValidElement(iconElement)
|
|
102
|
+
? React.cloneElement(iconElement, { className: 'h-5 w-5' } as any)
|
|
103
|
+
: iconElement;
|
|
104
|
+
|
|
105
|
+
if (item.type === 'link' && item.href) {
|
|
106
|
+
return (
|
|
107
|
+
<ButtonLink
|
|
108
|
+
key={item.id}
|
|
109
|
+
href={item.href}
|
|
110
|
+
variant={item.variant as any}
|
|
111
|
+
size="icon"
|
|
112
|
+
className="h-9 w-9"
|
|
113
|
+
onClick={onClick}
|
|
114
|
+
aria-label={item.label}
|
|
115
|
+
>
|
|
116
|
+
{resizedIcon}
|
|
117
|
+
</ButtonLink>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Button
|
|
123
|
+
key={item.id}
|
|
124
|
+
onClick={item.onClick}
|
|
125
|
+
variant={item.variant as any}
|
|
126
|
+
size="icon"
|
|
127
|
+
className={`h-9 w-9 ${item.className || ''}`}
|
|
128
|
+
aria-label={item.label}
|
|
129
|
+
>
|
|
130
|
+
{resizedIcon}
|
|
131
|
+
</Button>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Desktop: render with text and icon
|
|
136
|
+
const baseClassName = `w-full justify-start gap-2 ${item.className || ''}`;
|
|
137
|
+
|
|
138
|
+
if (item.type === 'link' && item.href) {
|
|
139
|
+
return (
|
|
140
|
+
<ButtonLink
|
|
141
|
+
key={item.id}
|
|
142
|
+
href={item.href}
|
|
143
|
+
variant={item.variant as any}
|
|
144
|
+
size="sm"
|
|
145
|
+
className={baseClassName}
|
|
146
|
+
onClick={onClick}
|
|
147
|
+
>
|
|
148
|
+
{item.icon}
|
|
149
|
+
{item.label}
|
|
150
|
+
</ButtonLink>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<Button
|
|
156
|
+
key={item.id}
|
|
157
|
+
onClick={item.onClick}
|
|
158
|
+
variant={item.variant as any}
|
|
159
|
+
size="sm"
|
|
160
|
+
className={baseClassName}
|
|
161
|
+
>
|
|
162
|
+
{item.icon}
|
|
163
|
+
{item.label}
|
|
164
|
+
</Button>
|
|
165
|
+
);
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
// === RENDERING ===
|
|
169
|
+
|
|
170
|
+
// Desktop variant
|
|
171
|
+
if (variant === 'desktop') {
|
|
172
|
+
return (
|
|
173
|
+
<div className="flex items-center gap-3">
|
|
174
|
+
{isAuthenticated ? (
|
|
175
|
+
<div className="flex items-center gap-3">
|
|
176
|
+
{/* Dashboard button (only if not on dashboard) */}
|
|
177
|
+
{publicLayout.userMenu.dashboardPath && !isDashboard && (
|
|
178
|
+
<ButtonLink
|
|
179
|
+
href={publicLayout.userMenu.dashboardPath}
|
|
180
|
+
variant="default"
|
|
181
|
+
size="sm"
|
|
182
|
+
className="gap-2"
|
|
183
|
+
>
|
|
184
|
+
<Crown className="size-4" />
|
|
185
|
+
Dashboard
|
|
186
|
+
</ButtonLink>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{/* User Dropdown */}
|
|
190
|
+
<div className="relative">
|
|
191
|
+
<button
|
|
192
|
+
className="flex items-center gap-2 px-3 py-2 rounded-sm text-sm font-medium transition-colors text-foreground hover:text-primary hover:bg-accent/50"
|
|
193
|
+
onClick={toggleUserMenu}
|
|
194
|
+
aria-haspopup="true"
|
|
195
|
+
aria-expanded={userMenuOpen}
|
|
196
|
+
>
|
|
197
|
+
<User className="size-4" />
|
|
198
|
+
<span className="max-w-[120px] truncate">{user?.email}</span>
|
|
199
|
+
<ChevronDown
|
|
200
|
+
className={`size-4 transition-transform ${
|
|
201
|
+
userMenuOpen ? 'rotate-180' : ''
|
|
202
|
+
}`}
|
|
203
|
+
/>
|
|
204
|
+
</button>
|
|
205
|
+
|
|
206
|
+
{userMenuOpen && (
|
|
207
|
+
<>
|
|
208
|
+
{/* Backdrop */}
|
|
209
|
+
<div
|
|
210
|
+
className="fixed inset-0 z-[9995]"
|
|
211
|
+
onClick={closeUserMenu}
|
|
212
|
+
aria-hidden="true"
|
|
213
|
+
/>
|
|
214
|
+
{/* Dropdown */}
|
|
215
|
+
<div
|
|
216
|
+
className="absolute top-full right-0 mt-2 w-48 rounded-sm shadow-sm backdrop-blur-xl z-[9996] bg-popover border border-border"
|
|
217
|
+
role="menu"
|
|
218
|
+
aria-label="User menu"
|
|
219
|
+
>
|
|
220
|
+
<div className="p-2">
|
|
221
|
+
{/* User info */}
|
|
222
|
+
<div className="px-3 py-2 text-sm mb-2 border-b border-border">
|
|
223
|
+
<div className="text-muted-foreground">Signed in as:</div>
|
|
224
|
+
<div className="font-medium truncate text-popover-foreground mt-1">
|
|
225
|
+
{user?.email}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
{/* Menu items - unified rendering */}
|
|
230
|
+
{menuItems.map((item) => renderMenuItem(item, 'desktop', closeUserMenu))}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
) : (
|
|
238
|
+
/* Guest - Sign in button */
|
|
239
|
+
<ButtonLink href={routes.auth} variant="default" size="sm" className="h-9 gap-1.5">
|
|
240
|
+
<User className="w-4 h-4" />
|
|
241
|
+
Sign In
|
|
242
|
+
</ButtonLink>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Mobile variant
|
|
249
|
+
if (isAuthenticated) {
|
|
250
|
+
return (
|
|
251
|
+
<Card className="border-primary/20 shadow-lg !bg-accent/50">
|
|
252
|
+
<CardContent className="p-4">
|
|
253
|
+
{/* User Info Header */}
|
|
254
|
+
<div className="flex items-center gap-3 mb-4 p-3 rounded-sm border border-border bg-accent/70">
|
|
255
|
+
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-primary flex-shrink-0 overflow-hidden relative">
|
|
256
|
+
{user?.avatar ? (
|
|
257
|
+
<img
|
|
258
|
+
src={user.avatar}
|
|
259
|
+
alt={user?.email || 'User'}
|
|
260
|
+
className="w-10 h-10 rounded-full object-cover"
|
|
261
|
+
/>
|
|
262
|
+
) : (
|
|
263
|
+
<User className="w-5 h-5 text-primary-foreground" />
|
|
264
|
+
)}
|
|
265
|
+
{/* Active indicator */}
|
|
266
|
+
<div className="absolute -bottom-0.5 -right-0.5 size-3 rounded-full bg-green-500 border-2 border-background" />
|
|
267
|
+
</div>
|
|
268
|
+
<div className="flex-1 min-w-0">
|
|
269
|
+
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
270
|
+
Signed in as
|
|
271
|
+
</p>
|
|
272
|
+
<p className="text-sm font-semibold truncate text-foreground">
|
|
273
|
+
{user?.email}
|
|
274
|
+
</p>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
{/* Action Buttons */}
|
|
279
|
+
<div className="space-y-3">
|
|
280
|
+
{/* Dashboard link */}
|
|
281
|
+
{publicLayout.userMenu.dashboardPath && (
|
|
282
|
+
<ButtonLink
|
|
283
|
+
href={publicLayout.userMenu.dashboardPath}
|
|
284
|
+
variant="default"
|
|
285
|
+
size="sm"
|
|
286
|
+
className="w-full h-9 gap-2"
|
|
287
|
+
onClick={onNavigate}
|
|
288
|
+
>
|
|
289
|
+
<Crown className="size-4" />
|
|
290
|
+
Dashboard
|
|
291
|
+
</ButtonLink>
|
|
292
|
+
)}
|
|
293
|
+
|
|
294
|
+
{/* Quick Actions - Icons only - unified rendering */}
|
|
295
|
+
<div className="flex items-center justify-center gap-2 pt-3 mt-1 border-t border-border/30">
|
|
296
|
+
{menuItems.map((item) => renderMenuItem(item, 'mobile-icon', onNavigate))}
|
|
297
|
+
|
|
298
|
+
{/* Theme Toggle */}
|
|
299
|
+
<ThemeToggle />
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</CardContent>
|
|
303
|
+
</Card>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Mobile Guest Card
|
|
308
|
+
return (
|
|
309
|
+
<Card className="border-border !bg-accent/50">
|
|
310
|
+
<CardContent className="p-4">
|
|
311
|
+
<div className="text-center space-y-4">
|
|
312
|
+
<div className="w-12 h-12 rounded-full flex items-center justify-center mx-auto bg-muted">
|
|
313
|
+
<User className="w-6 h-6 text-muted-foreground" />
|
|
314
|
+
</div>
|
|
315
|
+
<div>
|
|
316
|
+
<p className="text-sm font-medium mb-1 text-foreground">Welcome!</p>
|
|
317
|
+
<p className="text-xs text-muted-foreground">
|
|
318
|
+
Sign in to access your dashboard
|
|
319
|
+
</p>
|
|
320
|
+
</div>
|
|
321
|
+
<ButtonLink
|
|
322
|
+
href={routes.auth}
|
|
323
|
+
variant="default"
|
|
324
|
+
size="default"
|
|
325
|
+
className="w-full gap-2"
|
|
326
|
+
onClick={onNavigate}
|
|
327
|
+
>
|
|
328
|
+
<User className="w-5 h-5" />
|
|
329
|
+
Sign In
|
|
330
|
+
</ButtonLink>
|
|
331
|
+
|
|
332
|
+
{/* Theme toggle */}
|
|
333
|
+
<div className="flex justify-center pt-2 border-t border-border/30">
|
|
334
|
+
<ThemeToggle />
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</CardContent>
|
|
338
|
+
</Card>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
@@ -5,3 +5,7 @@
|
|
|
5
5
|
export { default as Seo } from './Seo';
|
|
6
6
|
export { default as PageProgress } from './PageProgress';
|
|
7
7
|
export { ErrorBoundary } from './ErrorBoundary';
|
|
8
|
+
export { PackageVersions, getPackageVersions, getPackageVersion } from './PackageVersions';
|
|
9
|
+
export { UserMenu } from './UserMenu';
|
|
10
|
+
export type { PackageInfo } from './PackageVersions';
|
|
11
|
+
export type { UserMenuProps } from './UserMenu';
|
|
@@ -24,11 +24,11 @@ interface AppContextValue {
|
|
|
24
24
|
currentPath: string;
|
|
25
25
|
layoutMode: LayoutMode;
|
|
26
26
|
|
|
27
|
-
// Mobile
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
// Mobile drawer state
|
|
28
|
+
mobileDrawerOpen: boolean;
|
|
29
|
+
openMobileDrawer: () => void;
|
|
30
|
+
closeMobileDrawer: () => void;
|
|
31
|
+
toggleMobileDrawer: () => void;
|
|
32
32
|
|
|
33
33
|
// User menu state (desktop dropdown)
|
|
34
34
|
userMenuOpen: boolean;
|
|
@@ -41,6 +41,9 @@ interface AppContextValue {
|
|
|
41
41
|
collapseSidebar: () => void;
|
|
42
42
|
expandSidebar: () => void;
|
|
43
43
|
toggleSidebar: () => void;
|
|
44
|
+
|
|
45
|
+
// Features
|
|
46
|
+
showPackageVersions?: boolean;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -56,6 +59,7 @@ const AppContext = createContext<AppContextValue | null>(null);
|
|
|
56
59
|
export interface AppContextProviderProps {
|
|
57
60
|
children: ReactNode;
|
|
58
61
|
config: AppLayoutConfig;
|
|
62
|
+
showPackageVersions?: boolean;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
/**
|
|
@@ -64,11 +68,11 @@ export interface AppContextProviderProps {
|
|
|
64
68
|
* Provides unified application context to all child components
|
|
65
69
|
* Manages layout state and exposes configuration
|
|
66
70
|
*/
|
|
67
|
-
export function AppContextProvider({ children, config }: AppContextProviderProps) {
|
|
71
|
+
export function AppContextProvider({ children, config, showPackageVersions }: AppContextProviderProps) {
|
|
68
72
|
const router = useRouter();
|
|
69
73
|
|
|
70
74
|
// UI state
|
|
71
|
-
const [
|
|
75
|
+
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
|
|
72
76
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
|
73
77
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
74
78
|
|
|
@@ -81,10 +85,10 @@ export function AppContextProvider({ children, config }: AppContextProviderProps
|
|
|
81
85
|
return 'public';
|
|
82
86
|
}, [router.pathname, config.routes.detectors]);
|
|
83
87
|
|
|
84
|
-
// Mobile
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
+
// Mobile drawer handlers
|
|
89
|
+
const openMobileDrawer = () => setMobileDrawerOpen(true);
|
|
90
|
+
const closeMobileDrawer = () => setMobileDrawerOpen(false);
|
|
91
|
+
const toggleMobileDrawer = () => setMobileDrawerOpen(prev => !prev);
|
|
88
92
|
|
|
89
93
|
// User menu handlers
|
|
90
94
|
const openUserMenu = () => setUserMenuOpen(true);
|
|
@@ -101,10 +105,10 @@ export function AppContextProvider({ children, config }: AppContextProviderProps
|
|
|
101
105
|
routes: config.routes.detectors,
|
|
102
106
|
currentPath: router.pathname,
|
|
103
107
|
layoutMode,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
mobileDrawerOpen,
|
|
109
|
+
openMobileDrawer,
|
|
110
|
+
closeMobileDrawer,
|
|
111
|
+
toggleMobileDrawer,
|
|
108
112
|
userMenuOpen,
|
|
109
113
|
openUserMenu,
|
|
110
114
|
closeUserMenu,
|
|
@@ -113,6 +117,7 @@ export function AppContextProvider({ children, config }: AppContextProviderProps
|
|
|
113
117
|
collapseSidebar,
|
|
114
118
|
expandSidebar,
|
|
115
119
|
toggleSidebar,
|
|
120
|
+
showPackageVersions,
|
|
116
121
|
};
|
|
117
122
|
|
|
118
123
|
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
|
@@ -129,7 +134,7 @@ export function AppContextProvider({ children, config }: AppContextProviderProps
|
|
|
129
134
|
*
|
|
130
135
|
* @example
|
|
131
136
|
* ```tsx
|
|
132
|
-
* const { config, layoutMode,
|
|
137
|
+
* const { config, layoutMode, toggleMobileDrawer } = useAppContext();
|
|
133
138
|
* ```
|
|
134
139
|
*/
|
|
135
140
|
export function useAppContext(): AppContextValue {
|
|
@@ -152,8 +152,8 @@ export function DashboardHeader() {
|
|
|
152
152
|
|
|
153
153
|
<DropdownMenuSeparator />
|
|
154
154
|
|
|
155
|
-
<DropdownMenuItem onClick={logout}>
|
|
156
|
-
<LogOut className="
|
|
155
|
+
<DropdownMenuItem onClick={logout} className="gap-2">
|
|
156
|
+
<LogOut className="h-4 w-4" />
|
|
157
157
|
Logout
|
|
158
158
|
</DropdownMenuItem>
|
|
159
159
|
</DropdownMenuContent>
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from '@djangocfg/ui/components';
|
|
30
30
|
import { useAppContext } from '../../../context';
|
|
31
31
|
import { useNavigation } from '../../../hooks';
|
|
32
|
+
import { PackageVersions } from '../../../components';
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Dashboard Sidebar Component
|
|
@@ -44,7 +45,7 @@ import { useNavigation } from '../../../hooks';
|
|
|
44
45
|
* All data from context!
|
|
45
46
|
*/
|
|
46
47
|
export function DashboardSidebar() {
|
|
47
|
-
const { config } = useAppContext();
|
|
48
|
+
const { config, showPackageVersions } = useAppContext();
|
|
48
49
|
const { currentPath } = useNavigation();
|
|
49
50
|
const { state, isMobile } = useSidebar();
|
|
50
51
|
|
|
@@ -157,8 +158,11 @@ export function DashboardSidebar() {
|
|
|
157
158
|
))}
|
|
158
159
|
</SidebarContent>
|
|
159
160
|
|
|
160
|
-
{
|
|
161
|
-
|
|
161
|
+
{showPackageVersions && (
|
|
162
|
+
<SidebarFooter>
|
|
163
|
+
<PackageVersions variant="sidebar" />
|
|
164
|
+
</SidebarFooter>
|
|
165
|
+
)}
|
|
162
166
|
</Sidebar>
|
|
163
167
|
);
|
|
164
168
|
}
|
|
@@ -11,6 +11,7 @@ import React from 'react';
|
|
|
11
11
|
import Link from 'next/link';
|
|
12
12
|
import { useIsMobile } from '@djangocfg/ui/hooks';
|
|
13
13
|
import { useAppContext } from '../../../context';
|
|
14
|
+
import { PackageVersions } from '../../../components';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Footer Component
|
|
@@ -27,7 +28,7 @@ import { useAppContext } from '../../../context';
|
|
|
27
28
|
* All data from context!
|
|
28
29
|
*/
|
|
29
30
|
export function Footer() {
|
|
30
|
-
const { config } = useAppContext();
|
|
31
|
+
const { config, showPackageVersions } = useAppContext();
|
|
31
32
|
const isMobile = useIsMobile();
|
|
32
33
|
|
|
33
34
|
const { app, publicLayout } = config;
|
|
@@ -58,7 +59,10 @@ export function Footer() {
|
|
|
58
59
|
</div>
|
|
59
60
|
|
|
60
61
|
{/* Quick Links */}
|
|
61
|
-
<div className="flex flex-wrap justify-center gap-4 mb-6">
|
|
62
|
+
<div className="flex flex-wrap justify-center gap-4 mb-6 items-center">
|
|
63
|
+
{showPackageVersions && (
|
|
64
|
+
<PackageVersions variant="footer-minimal" />
|
|
65
|
+
)}
|
|
62
66
|
{footer.links.docs && (
|
|
63
67
|
<a
|
|
64
68
|
href={footer.links.docs}
|
|
@@ -209,6 +213,9 @@ export function Footer() {
|
|
|
209
213
|
</a>
|
|
210
214
|
</div>
|
|
211
215
|
<div className="flex flex-wrap items-center gap-4">
|
|
216
|
+
{showPackageVersions && (
|
|
217
|
+
<PackageVersions variant="footer-minimal" />
|
|
218
|
+
)}
|
|
212
219
|
{footer.links.docs && (
|
|
213
220
|
<a
|
|
214
221
|
href={footer.links.docs}
|
package/src/layouts/AppLayout/layouts/PublicLayout/components/{MobileMenu.tsx → MobileDrawer.tsx}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mobile
|
|
2
|
+
* Mobile Drawer
|
|
3
3
|
*
|
|
4
|
-
* Full-screen slide-in
|
|
4
|
+
* Full-screen slide-in drawer for mobile navigation
|
|
5
5
|
* Refactored from _old/MainLayout - uses context only!
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -12,29 +12,25 @@ import { createPortal } from 'react-dom';
|
|
|
12
12
|
import Link from 'next/link';
|
|
13
13
|
import { X } from 'lucide-react';
|
|
14
14
|
import { useAppContext } from '../../../context';
|
|
15
|
-
import { useAuth } from '../../../../../auth';
|
|
16
15
|
import { useNavigation } from '../../../hooks';
|
|
17
|
-
import {
|
|
16
|
+
import { UserMenu } from '../../../components';
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
|
-
* Mobile
|
|
19
|
+
* Mobile Drawer Component
|
|
21
20
|
*
|
|
22
21
|
* Features:
|
|
23
22
|
* - Slide-in drawer from right
|
|
24
|
-
* -
|
|
25
|
-
* - Welcome card with sign in (guest)
|
|
23
|
+
* - UserMenu component (authenticated/guest)
|
|
26
24
|
* - Navigation sections
|
|
27
|
-
* - Theme toggle
|
|
28
25
|
* - Backdrop overlay
|
|
29
26
|
*
|
|
30
27
|
* All data from context!
|
|
31
28
|
*/
|
|
32
|
-
export function
|
|
33
|
-
const { config,
|
|
34
|
-
const { user, isAuthenticated, logout } = useAuth();
|
|
29
|
+
export function MobileDrawer() {
|
|
30
|
+
const { config, mobileDrawerOpen, closeMobileDrawer } = useAppContext();
|
|
35
31
|
const { isActive } = useNavigation();
|
|
36
32
|
|
|
37
|
-
const { app, publicLayout
|
|
33
|
+
const { app, publicLayout } = config;
|
|
38
34
|
|
|
39
35
|
// Track if we should render (stays true during close animation)
|
|
40
36
|
const [shouldRender, setShouldRender] = React.useState(false);
|
|
@@ -44,7 +40,7 @@ export function MobileMenu() {
|
|
|
44
40
|
|
|
45
41
|
// Handle opening
|
|
46
42
|
React.useEffect(() => {
|
|
47
|
-
if (
|
|
43
|
+
if (mobileDrawerOpen) {
|
|
48
44
|
setShouldRender(true);
|
|
49
45
|
// Trigger animation after render
|
|
50
46
|
requestAnimationFrame(() => {
|
|
@@ -61,19 +57,14 @@ export function MobileMenu() {
|
|
|
61
57
|
}, 300);
|
|
62
58
|
return () => clearTimeout(timer);
|
|
63
59
|
}
|
|
64
|
-
}, [
|
|
65
|
-
|
|
66
|
-
const handleLogout = () => {
|
|
67
|
-
logout();
|
|
68
|
-
closeMobileMenu();
|
|
69
|
-
};
|
|
60
|
+
}, [mobileDrawerOpen]);
|
|
70
61
|
|
|
71
62
|
const handleClose = () => {
|
|
72
|
-
|
|
63
|
+
closeMobileDrawer();
|
|
73
64
|
};
|
|
74
65
|
|
|
75
66
|
const handleNavigate = () => {
|
|
76
|
-
|
|
67
|
+
closeMobileDrawer();
|
|
77
68
|
};
|
|
78
69
|
|
|
79
70
|
// Prepare menu sections before render
|
|
@@ -137,15 +128,7 @@ export function MobileMenu() {
|
|
|
137
128
|
{/* Scrollable Content */}
|
|
138
129
|
<div className="flex-1 overflow-y-auto p-4 space-y-6">
|
|
139
130
|
{/* User Menu Card */}
|
|
140
|
-
<
|
|
141
|
-
isAuthenticated={isAuthenticated}
|
|
142
|
-
user={user}
|
|
143
|
-
dashboardPath={publicLayout.userMenu.dashboardPath}
|
|
144
|
-
profilePath={publicLayout.userMenu.profilePath}
|
|
145
|
-
authPath={routes.auth}
|
|
146
|
-
onLogout={handleLogout}
|
|
147
|
-
onNavigate={handleNavigate}
|
|
148
|
-
/>
|
|
131
|
+
<UserMenu variant="mobile" onNavigate={handleNavigate} />
|
|
149
132
|
|
|
150
133
|
{/* Navigation Sections */}
|
|
151
134
|
<div className="space-y-6">
|
|
@@ -20,10 +20,9 @@ import { ThemeToggle } from '@djangocfg/ui/theme';
|
|
|
20
20
|
import { cn } from '@djangocfg/ui/lib';
|
|
21
21
|
import { useIsMobile } from '@djangocfg/ui/hooks';
|
|
22
22
|
import { useAppContext } from '../../../context';
|
|
23
|
-
import { useAuth } from '../../../../../auth';
|
|
24
23
|
import { useNavigation } from '../../../hooks';
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
24
|
+
import { UserMenu } from '../../../components';
|
|
25
|
+
import { MobileDrawer } from './MobileDrawer';
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* Navigation Component
|
|
@@ -38,8 +37,7 @@ import { MobileMenu } from './MobileMenu';
|
|
|
38
37
|
* All data from context - zero prop drilling!
|
|
39
38
|
*/
|
|
40
39
|
export function Navigation() {
|
|
41
|
-
const { config,
|
|
42
|
-
const { user, isAuthenticated, logout } = useAuth();
|
|
40
|
+
const { config, toggleMobileDrawer } = useAppContext();
|
|
43
41
|
const { isActive } = useNavigation();
|
|
44
42
|
const isMobile = useIsMobile();
|
|
45
43
|
const [openDropdown, setOpenDropdown] = React.useState<string | null>(null);
|
|
@@ -147,14 +145,14 @@ export function Navigation() {
|
|
|
147
145
|
{!isMobile && (
|
|
148
146
|
<div className="flex items-center gap-2">
|
|
149
147
|
<ThemeToggle />
|
|
150
|
-
<
|
|
148
|
+
<UserMenu variant="desktop" />
|
|
151
149
|
</div>
|
|
152
150
|
)}
|
|
153
151
|
|
|
154
152
|
{/* Mobile Menu Button - Only visible on mobile */}
|
|
155
153
|
{isMobile && (
|
|
156
154
|
<button
|
|
157
|
-
onClick={
|
|
155
|
+
onClick={toggleMobileDrawer}
|
|
158
156
|
className="p-3 rounded-sm border shadow-sm transition-all duration-200 bg-card/50 hover:bg-card border-border/50 hover:border-primary/50 text-foreground hover:text-primary"
|
|
159
157
|
aria-label="Toggle mobile menu"
|
|
160
158
|
>
|
|
@@ -164,8 +162,8 @@ export function Navigation() {
|
|
|
164
162
|
</div>
|
|
165
163
|
</div>
|
|
166
164
|
|
|
167
|
-
{/* Mobile
|
|
168
|
-
<
|
|
165
|
+
{/* Mobile Drawer */}
|
|
166
|
+
<MobileDrawer />
|
|
169
167
|
</nav>
|
|
170
168
|
);
|
|
171
169
|
}
|
|
@@ -22,13 +22,15 @@ interface ProfileLayoutProps {
|
|
|
22
22
|
title?: string;
|
|
23
23
|
description?: string;
|
|
24
24
|
showMemberSince?: boolean;
|
|
25
|
+
showLastLogin?: boolean;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const ProfileContent = ({
|
|
28
29
|
onUnauthenticated,
|
|
29
30
|
title = 'Profile Settings',
|
|
30
31
|
description = 'Manage your account information and preferences',
|
|
31
|
-
showMemberSince = true
|
|
32
|
+
showMemberSince = true,
|
|
33
|
+
showLastLogin = true
|
|
32
34
|
}: ProfileLayoutProps) => {
|
|
33
35
|
const { user, isLoading } = useAuth();
|
|
34
36
|
|
|
@@ -83,11 +85,18 @@ const ProfileContent = ({
|
|
|
83
85
|
<CardTitle className="text-xl">
|
|
84
86
|
{user?.display_username || user?.email}
|
|
85
87
|
</CardTitle>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
<div className="space-y-1">
|
|
89
|
+
{showMemberSince && user?.date_joined && (
|
|
90
|
+
<CardDescription className="text-muted-foreground">
|
|
91
|
+
Member since {formatDate(user.date_joined)}
|
|
92
|
+
</CardDescription>
|
|
93
|
+
)}
|
|
94
|
+
{showLastLogin && user?.last_login && (
|
|
95
|
+
<CardDescription className="text-muted-foreground text-sm">
|
|
96
|
+
Last login: {formatDate(user.last_login)}
|
|
97
|
+
</CardDescription>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
91
100
|
</CardHeader>
|
|
92
101
|
|
|
93
102
|
<CardContent className="space-y-6">
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Desktop User Menu
|
|
3
|
-
*
|
|
4
|
-
* User dropdown menu for desktop navigation
|
|
5
|
-
* Refactored from _old/MainLayout - uses context only!
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
'use client';
|
|
9
|
-
|
|
10
|
-
import React from 'react';
|
|
11
|
-
import { useRouter } from 'next/router';
|
|
12
|
-
import { ChevronDown, LayoutDashboard, LogOut, User } from 'lucide-react';
|
|
13
|
-
import { ButtonLink } from '@djangocfg/ui/components';
|
|
14
|
-
import { useAppContext } from '../../../context';
|
|
15
|
-
import { useAuth } from '../../../../../auth';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Desktop User Menu Component
|
|
19
|
-
*
|
|
20
|
-
* Features:
|
|
21
|
-
* - Sign in button for guests
|
|
22
|
-
* - Dashboard link for authenticated users (if not on dashboard)
|
|
23
|
-
* - User dropdown with email and profile link
|
|
24
|
-
* - Logout button
|
|
25
|
-
*
|
|
26
|
-
* All data from context!
|
|
27
|
-
*/
|
|
28
|
-
export function DesktopUserMenu() {
|
|
29
|
-
const router = useRouter();
|
|
30
|
-
const { config, userMenuOpen, toggleUserMenu, closeUserMenu } = useAppContext();
|
|
31
|
-
const { user, isAuthenticated, logout } = useAuth();
|
|
32
|
-
|
|
33
|
-
const { routes, publicLayout } = config;
|
|
34
|
-
|
|
35
|
-
const handleLogout = () => {
|
|
36
|
-
logout();
|
|
37
|
-
closeUserMenu();
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const isDashboard = publicLayout.userMenu.dashboardPath
|
|
41
|
-
? router.pathname.includes(publicLayout.userMenu.dashboardPath)
|
|
42
|
-
: false;
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<div className="flex items-center gap-3">
|
|
46
|
-
{/* Authenticated user */}
|
|
47
|
-
{isAuthenticated ? (
|
|
48
|
-
<div className="flex items-center gap-3">
|
|
49
|
-
{/* Dashboard button (only if not on dashboard) */}
|
|
50
|
-
{publicLayout.userMenu.dashboardPath && !isDashboard && (
|
|
51
|
-
<ButtonLink
|
|
52
|
-
href={publicLayout.userMenu.dashboardPath}
|
|
53
|
-
variant="default"
|
|
54
|
-
size="sm"
|
|
55
|
-
>
|
|
56
|
-
<LayoutDashboard className="size-4 mr-2" />
|
|
57
|
-
Dashboard
|
|
58
|
-
</ButtonLink>
|
|
59
|
-
)}
|
|
60
|
-
|
|
61
|
-
{/* User Dropdown */}
|
|
62
|
-
<div className="relative">
|
|
63
|
-
<button
|
|
64
|
-
className="flex items-center gap-2 px-3 py-2 rounded-sm text-sm font-medium transition-colors text-foreground hover:text-primary hover:bg-accent/50"
|
|
65
|
-
onClick={toggleUserMenu}
|
|
66
|
-
aria-haspopup="true"
|
|
67
|
-
aria-expanded={userMenuOpen}
|
|
68
|
-
>
|
|
69
|
-
<User className="size-4" />
|
|
70
|
-
<span className="max-w-[120px] truncate">{user?.email}</span>
|
|
71
|
-
<ChevronDown
|
|
72
|
-
className={`size-4 transition-transform ${
|
|
73
|
-
userMenuOpen ? 'rotate-180' : ''
|
|
74
|
-
}`}
|
|
75
|
-
/>
|
|
76
|
-
</button>
|
|
77
|
-
|
|
78
|
-
{userMenuOpen && (
|
|
79
|
-
<>
|
|
80
|
-
{/* Backdrop */}
|
|
81
|
-
<div
|
|
82
|
-
className="fixed inset-0 z-[9995]"
|
|
83
|
-
onClick={closeUserMenu}
|
|
84
|
-
aria-hidden="true"
|
|
85
|
-
/>
|
|
86
|
-
{/* Dropdown */}
|
|
87
|
-
<div
|
|
88
|
-
className="absolute top-full right-0 mt-2 w-48 rounded-sm shadow-sm backdrop-blur-xl z-[9996] bg-popover border border-border"
|
|
89
|
-
role="menu"
|
|
90
|
-
aria-label="User menu"
|
|
91
|
-
>
|
|
92
|
-
<div className="p-2">
|
|
93
|
-
{/* User info */}
|
|
94
|
-
<div className="px-3 py-2 text-sm mb-2 border-b border-border">
|
|
95
|
-
<div className="text-muted-foreground">Signed in as:</div>
|
|
96
|
-
<div className="font-medium truncate text-popover-foreground mt-1">
|
|
97
|
-
{user?.email}
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
|
|
101
|
-
{/* Profile link */}
|
|
102
|
-
<ButtonLink
|
|
103
|
-
href={publicLayout.userMenu.profilePath}
|
|
104
|
-
variant="ghost"
|
|
105
|
-
size="sm"
|
|
106
|
-
className="w-full justify-start"
|
|
107
|
-
onClick={closeUserMenu}
|
|
108
|
-
>
|
|
109
|
-
<User className="size-4 mr-2" />
|
|
110
|
-
Profile
|
|
111
|
-
</ButtonLink>
|
|
112
|
-
|
|
113
|
-
{/* Logout button */}
|
|
114
|
-
<button
|
|
115
|
-
onClick={handleLogout}
|
|
116
|
-
className="w-full flex items-center gap-2 px-3 py-2 text-sm rounded-sm transition-colors text-destructive hover:bg-destructive/[0.1]"
|
|
117
|
-
>
|
|
118
|
-
<LogOut className="size-4" />
|
|
119
|
-
<span>Sign out</span>
|
|
120
|
-
</button>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
</>
|
|
124
|
-
)}
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
127
|
-
) : (
|
|
128
|
-
/* Guest - Sign in button */
|
|
129
|
-
<ButtonLink href={routes.auth} variant="default" size="sm" className="h-9 gap-1.5">
|
|
130
|
-
<User className="w-4 h-4" />
|
|
131
|
-
Sign In
|
|
132
|
-
</ButtonLink>
|
|
133
|
-
)}
|
|
134
|
-
</div>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mobile Menu User Card Component
|
|
3
|
-
*
|
|
4
|
-
* Displays user information and action buttons in mobile menu
|
|
5
|
-
* - Authenticated: shows user info with dashboard, profile, and logout
|
|
6
|
-
* - Guest: shows welcome message with sign in button
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
'use client';
|
|
10
|
-
|
|
11
|
-
import React from 'react';
|
|
12
|
-
import { Crown, LogOut, Settings, User } from 'lucide-react';
|
|
13
|
-
import { Button, ButtonLink, Card, CardContent } from '@djangocfg/ui/components';
|
|
14
|
-
import { ThemeToggle } from '@djangocfg/ui/theme';
|
|
15
|
-
|
|
16
|
-
interface MobileMenuUserCardProps {
|
|
17
|
-
isAuthenticated: boolean;
|
|
18
|
-
user?: {
|
|
19
|
-
email?: string;
|
|
20
|
-
avatar?: string;
|
|
21
|
-
} | null;
|
|
22
|
-
dashboardPath?: string;
|
|
23
|
-
profilePath: string;
|
|
24
|
-
authPath: string;
|
|
25
|
-
onLogout: () => void;
|
|
26
|
-
onNavigate: () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function MobileMenuUserCard({
|
|
30
|
-
isAuthenticated,
|
|
31
|
-
user,
|
|
32
|
-
dashboardPath,
|
|
33
|
-
profilePath,
|
|
34
|
-
authPath,
|
|
35
|
-
onLogout,
|
|
36
|
-
onNavigate,
|
|
37
|
-
}: MobileMenuUserCardProps) {
|
|
38
|
-
if (isAuthenticated) {
|
|
39
|
-
return (
|
|
40
|
-
<Card className="border-primary/20 shadow-lg !bg-accent/50">
|
|
41
|
-
<CardContent className="p-4">
|
|
42
|
-
{/* User Info Header */}
|
|
43
|
-
<div className="flex items-center gap-3 mb-4 p-3 rounded-sm border border-border bg-accent/70">
|
|
44
|
-
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-primary flex-shrink-0 overflow-hidden relative">
|
|
45
|
-
{user?.avatar ? (
|
|
46
|
-
<img
|
|
47
|
-
src={user.avatar}
|
|
48
|
-
alt={user?.email || 'User'}
|
|
49
|
-
className="w-10 h-10 rounded-full object-cover"
|
|
50
|
-
/>
|
|
51
|
-
) : (
|
|
52
|
-
<User className="w-5 h-5 text-primary-foreground" />
|
|
53
|
-
)}
|
|
54
|
-
{/* Active indicator */}
|
|
55
|
-
<div className="absolute -bottom-0.5 -right-0.5 size-3 rounded-full bg-green-500 border-2 border-background" />
|
|
56
|
-
</div>
|
|
57
|
-
<div className="flex-1 min-w-0">
|
|
58
|
-
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
|
59
|
-
Signed in as
|
|
60
|
-
</p>
|
|
61
|
-
<p className="text-sm font-semibold truncate text-foreground">
|
|
62
|
-
{user?.email}
|
|
63
|
-
</p>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
{/* Action Buttons */}
|
|
68
|
-
<div className="space-y-3">
|
|
69
|
-
{/* Dashboard link */}
|
|
70
|
-
{dashboardPath && (
|
|
71
|
-
<ButtonLink
|
|
72
|
-
href={dashboardPath}
|
|
73
|
-
variant="default"
|
|
74
|
-
size="sm"
|
|
75
|
-
className="w-full h-9"
|
|
76
|
-
onClick={onNavigate}
|
|
77
|
-
>
|
|
78
|
-
<Crown className="w-4 h-4 mr-2" />
|
|
79
|
-
Dashboard
|
|
80
|
-
</ButtonLink>
|
|
81
|
-
)}
|
|
82
|
-
|
|
83
|
-
{/* Quick Actions - Icons only */}
|
|
84
|
-
<div className="flex items-center justify-center gap-2 pt-3 mt-1 border-t border-border/30">
|
|
85
|
-
{/* Profile Settings */}
|
|
86
|
-
<ButtonLink
|
|
87
|
-
href={profilePath}
|
|
88
|
-
variant="ghost"
|
|
89
|
-
size="icon"
|
|
90
|
-
className="h-9 w-9"
|
|
91
|
-
onClick={onNavigate}
|
|
92
|
-
aria-label="Profile Settings"
|
|
93
|
-
>
|
|
94
|
-
<Settings className="h-5 w-5" />
|
|
95
|
-
</ButtonLink>
|
|
96
|
-
|
|
97
|
-
{/* Theme Toggle */}
|
|
98
|
-
<ThemeToggle />
|
|
99
|
-
|
|
100
|
-
{/* Sign Out */}
|
|
101
|
-
<Button
|
|
102
|
-
onClick={onLogout}
|
|
103
|
-
variant="ghost"
|
|
104
|
-
size="icon"
|
|
105
|
-
className="h-9 w-9 text-destructive hover:bg-destructive/10 hover:text-destructive"
|
|
106
|
-
aria-label="Sign Out"
|
|
107
|
-
>
|
|
108
|
-
<LogOut className="h-5 w-5" />
|
|
109
|
-
</Button>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
</CardContent>
|
|
113
|
-
</Card>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Guest Card
|
|
118
|
-
return (
|
|
119
|
-
<Card className="border-border !bg-accent/50">
|
|
120
|
-
<CardContent className="p-4">
|
|
121
|
-
<div className="text-center space-y-4">
|
|
122
|
-
<div className="w-12 h-12 rounded-full flex items-center justify-center mx-auto bg-muted">
|
|
123
|
-
<User className="w-6 h-6 text-muted-foreground" />
|
|
124
|
-
</div>
|
|
125
|
-
<div>
|
|
126
|
-
<p className="text-sm font-medium mb-1 text-foreground">Welcome!</p>
|
|
127
|
-
<p className="text-xs text-muted-foreground">
|
|
128
|
-
Sign in to access your dashboard
|
|
129
|
-
</p>
|
|
130
|
-
</div>
|
|
131
|
-
<ButtonLink
|
|
132
|
-
href={authPath}
|
|
133
|
-
variant="default"
|
|
134
|
-
size="default"
|
|
135
|
-
className="w-full"
|
|
136
|
-
onClick={onNavigate}
|
|
137
|
-
>
|
|
138
|
-
<User className="w-5 h-5 mr-2" />
|
|
139
|
-
Sign In
|
|
140
|
-
</ButtonLink>
|
|
141
|
-
|
|
142
|
-
{/* Theme toggle */}
|
|
143
|
-
<div className="flex justify-center pt-2 border-t border-border/30">
|
|
144
|
-
<ThemeToggle />
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
</CardContent>
|
|
148
|
-
</Card>
|
|
149
|
-
);
|
|
150
|
-
}
|