@djangocfg/layouts 2.1.19 → 2.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +5 -5
  2. package/src/layouts/AdminLayout/AdminLayout.tsx +1 -1
  3. package/src/layouts/AppLayout/AppLayout.tsx +29 -27
  4. package/src/layouts/AppLayout/BaseApp.tsx +36 -38
  5. package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
  6. package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +1 -1
  7. package/src/layouts/PublicLayout/PublicLayout.tsx +9 -43
  8. package/src/layouts/PublicLayout/components/PublicFooter/DjangoCFGLogo.tsx +45 -0
  9. package/src/layouts/PublicLayout/components/PublicFooter/FooterBottom.tsx +114 -0
  10. package/src/layouts/PublicLayout/components/PublicFooter/FooterMenuSections.tsx +53 -0
  11. package/src/layouts/PublicLayout/components/PublicFooter/FooterProjectInfo.tsx +77 -0
  12. package/src/layouts/PublicLayout/components/PublicFooter/FooterSocialLinks.tsx +82 -0
  13. package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +129 -0
  14. package/src/layouts/PublicLayout/components/PublicFooter/index.ts +17 -0
  15. package/src/layouts/PublicLayout/components/PublicFooter/types.ts +57 -0
  16. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +3 -6
  17. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +3 -6
  18. package/src/layouts/PublicLayout/index.ts +12 -1
  19. package/src/layouts/_components/UserMenu.tsx +161 -40
  20. package/src/layouts/index.ts +4 -1
  21. package/src/layouts/shared/README.md +86 -0
  22. package/src/layouts/shared/index.ts +21 -0
  23. package/src/layouts/shared/types.ts +215 -0
  24. package/src/snippets/McpChat/components/AIChatWidget.tsx +150 -53
  25. package/src/snippets/McpChat/components/AskAIButton.tsx +2 -5
  26. package/src/snippets/McpChat/components/ChatMessages.tsx +30 -9
  27. package/src/snippets/McpChat/components/ChatPanel.tsx +1 -1
  28. package/src/snippets/McpChat/components/ChatSidebar.tsx +1 -1
  29. package/src/snippets/McpChat/components/MessageBubble.tsx +46 -34
  30. package/src/snippets/McpChat/context/AIChatContext.tsx +23 -6
  31. package/src/layouts/PublicLayout/components/PublicFooter.tsx +0 -190
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Footer Project Info Component
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import React from 'react';
8
+ import type { LucideIcon } from 'lucide-react';
9
+ import { DjangoCFGLogo } from './DjangoCFGLogo';
10
+ import { FooterSocialLinksComponent } from './FooterSocialLinks';
11
+ import type { FooterSocialLinks } from './types';
12
+
13
+ export interface FooterProjectInfoProps {
14
+ siteName: string;
15
+ description?: string;
16
+ logo?: string;
17
+ badge?: {
18
+ icon: LucideIcon;
19
+ text: string;
20
+ };
21
+ socialLinks?: FooterSocialLinks;
22
+ variant?: 'mobile' | 'desktop';
23
+ }
24
+
25
+ export function FooterProjectInfo({
26
+ siteName,
27
+ description,
28
+ logo,
29
+ badge,
30
+ socialLinks,
31
+ variant = 'desktop',
32
+ }: FooterProjectInfoProps) {
33
+ const isMobile = variant === 'mobile';
34
+
35
+ return (
36
+ <div className={isMobile ? 'text-center space-y-4 mb-6' : 'space-y-4 lg:flex-shrink-0 lg:w-80'}>
37
+ <div className={isMobile ? 'flex items-center justify-center gap-2' : 'flex items-center gap-2'}>
38
+ {logo ? (
39
+ <div className={isMobile ? 'w-6 h-6 flex items-center justify-center' : 'w-8 h-8 flex items-center justify-center'}>
40
+ <img
41
+ src={logo}
42
+ alt={`${siteName} Logo`}
43
+ className="w-full h-full object-contain"
44
+ />
45
+ </div>
46
+ ) : (
47
+ <DjangoCFGLogo size={isMobile ? 24 : 32} className="text-foreground" />
48
+ )}
49
+ <span className={isMobile ? 'text-lg font-bold text-foreground' : 'text-xl font-bold text-foreground'}>
50
+ {siteName}
51
+ </span>
52
+ </div>
53
+
54
+ {description && (
55
+ <p className={isMobile ? 'text-muted-foreground text-sm leading-relaxed max-w-md mx-auto' : 'text-muted-foreground text-sm leading-relaxed'}>
56
+ {description}
57
+ </p>
58
+ )}
59
+
60
+ {badge && !isMobile && (
61
+ <div className="pt-2">
62
+ <span className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-gradient-to-r from-primary/80 to-secondary/60 border border-primary/30 shadow-brand text-xs font-semibold text-primary-foreground">
63
+ <badge.icon className="w-4 h-4" />
64
+ {badge.text}
65
+ </span>
66
+ </div>
67
+ )}
68
+
69
+ {socialLinks && (
70
+ <FooterSocialLinksComponent
71
+ socialLinks={socialLinks}
72
+ className={isMobile ? 'flex justify-center space-x-6' : 'flex space-x-4 pt-4'}
73
+ />
74
+ )}
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Footer Social Links Component
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import React from 'react';
8
+ import {
9
+ Github,
10
+ Linkedin,
11
+ Twitter,
12
+ MessageCircle,
13
+ Youtube,
14
+ Facebook,
15
+ Instagram,
16
+ Mail,
17
+ MessageSquare,
18
+ } from 'lucide-react';
19
+ import type { FooterSocialLinks } from './types';
20
+
21
+ export interface FooterSocialLinksProps {
22
+ socialLinks?: FooterSocialLinks;
23
+ className?: string;
24
+ iconClassName?: string;
25
+ }
26
+
27
+ const socialIconsMap = {
28
+ github: { icon: Github, title: 'GitHub' },
29
+ linkedin: { icon: Linkedin, title: 'LinkedIn' },
30
+ twitter: { icon: Twitter, title: 'Twitter' },
31
+ telegram: { icon: MessageCircle, title: 'Telegram' },
32
+ youtube: { icon: Youtube, title: 'YouTube' },
33
+ facebook: { icon: Facebook, title: 'Facebook' },
34
+ instagram: { icon: Instagram, title: 'Instagram' },
35
+ whatsapp: { icon: MessageSquare, title: 'WhatsApp' },
36
+ email: { icon: Mail, title: 'Email' },
37
+ } as const;
38
+
39
+ export function FooterSocialLinksComponent({
40
+ socialLinks,
41
+ className = 'flex space-x-4',
42
+ iconClassName = 'w-5 h-5',
43
+ }: FooterSocialLinksProps) {
44
+ // Prepare social links data BEFORE render
45
+ const socialLinksData = socialLinks
46
+ ? Object.entries(socialLinks)
47
+ .filter(([_, url]) => url)
48
+ .map(([platform, url]) => {
49
+ const social = socialIconsMap[platform as keyof typeof socialIconsMap];
50
+ if (!social) return null;
51
+ return {
52
+ platform,
53
+ url: url!,
54
+ icon: social.icon,
55
+ title: social.title,
56
+ };
57
+ })
58
+ .filter((item): item is NonNullable<typeof item> => item !== null)
59
+ : [];
60
+
61
+ if (socialLinksData.length === 0) return null;
62
+
63
+ return (
64
+ <div className={className}>
65
+ {socialLinksData.map((social) => {
66
+ const Icon = social.icon;
67
+ return (
68
+ <a
69
+ key={social.platform}
70
+ href={social.url}
71
+ target="_blank"
72
+ rel="noopener noreferrer"
73
+ className="text-muted-foreground hover:text-primary transition-colors"
74
+ title={social.title}
75
+ >
76
+ <Icon className={iconClassName} />
77
+ </a>
78
+ );
79
+ })}
80
+ </div>
81
+ );
82
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Public Layout Footer
3
+ *
4
+ * Professional, flexible footer component for PublicLayout
5
+ * Supports desktop/mobile responsive layouts, social links, menu sections
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React from 'react';
11
+ import Link from 'next/link';
12
+ import { useIsMobile } from '@djangocfg/ui-nextjs/hooks';
13
+ import { FooterProjectInfo } from './FooterProjectInfo';
14
+ import { FooterMenuSections } from './FooterMenuSections';
15
+ import { FooterBottom } from './FooterBottom';
16
+ import type { PublicFooterProps } from './types';
17
+
18
+ export function PublicFooter({
19
+ siteName,
20
+ description,
21
+ logo,
22
+ badge,
23
+ socialLinks,
24
+ links = [],
25
+ menuSections = [],
26
+ copyright: copyrightProp,
27
+ credits: creditsProp,
28
+ variant = 'full',
29
+ }: PublicFooterProps) {
30
+ const isMobile = useIsMobile();
31
+
32
+ // Prepare data BEFORE render
33
+ const currentYear = new Date().getFullYear();
34
+ const copyright = copyrightProp || `© ${currentYear} ${siteName}. All rights reserved.`;
35
+ const credits = creditsProp || {
36
+ text: 'Built with DjangoCFG',
37
+ url: 'https://djangocfg.com',
38
+ };
39
+
40
+ // Simple variant - minimal footer
41
+ if (variant === 'simple') {
42
+ return (
43
+ <footer className="bg-background border-t border-border mt-auto">
44
+ <div className="w-full px-4 py-4">
45
+ <div className="text-center">
46
+ <div className="text-sm text-muted-foreground">{copyright}</div>
47
+ </div>
48
+ </div>
49
+ </footer>
50
+ );
51
+ }
52
+
53
+ // Mobile Footer
54
+ if (isMobile) {
55
+ return (
56
+ <footer className="lg:hidden bg-background border-t border-border mt-auto">
57
+ <div className="w-full px-4 py-8">
58
+ <FooterProjectInfo
59
+ siteName={siteName}
60
+ description={description}
61
+ logo={logo}
62
+ socialLinks={socialLinks}
63
+ variant="mobile"
64
+ />
65
+
66
+ {/* Quick Links */}
67
+ {links.length > 0 && (
68
+ <div className="flex flex-wrap justify-center gap-3 mb-6">
69
+ {links.map((link) =>
70
+ link.external ? (
71
+ <a
72
+ key={link.path}
73
+ href={link.path}
74
+ target="_blank"
75
+ rel="noopener noreferrer"
76
+ className="text-xs text-muted-foreground hover:text-primary transition-colors"
77
+ >
78
+ {link.label}
79
+ </a>
80
+ ) : (
81
+ <Link
82
+ key={link.path}
83
+ href={link.path}
84
+ className="text-xs text-muted-foreground hover:text-primary transition-colors"
85
+ >
86
+ {link.label}
87
+ </Link>
88
+ )
89
+ )}
90
+ </div>
91
+ )}
92
+
93
+ <FooterBottom
94
+ copyright={copyright}
95
+ credits={credits}
96
+ variant="mobile"
97
+ />
98
+ </div>
99
+ </footer>
100
+ );
101
+ }
102
+
103
+ // Desktop Footer
104
+ return (
105
+ <footer className="max-lg:hidden bg-background border-t border-border mt-auto">
106
+ <div className="w-full px-4 sm:px-6 lg:px-8 py-12">
107
+ <div className="flex flex-col lg:flex-row gap-8 lg:gap-12">
108
+ <FooterProjectInfo
109
+ siteName={siteName}
110
+ description={description}
111
+ logo={logo}
112
+ badge={badge}
113
+ socialLinks={socialLinks}
114
+ variant="desktop"
115
+ />
116
+
117
+ <FooterMenuSections menuSections={menuSections} />
118
+ </div>
119
+
120
+ <FooterBottom
121
+ copyright={copyright}
122
+ credits={credits}
123
+ links={links}
124
+ variant="desktop"
125
+ />
126
+ </div>
127
+ </footer>
128
+ );
129
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Public Footer Exports
3
+ */
4
+
5
+ export { PublicFooter } from './PublicFooter';
6
+ export { FooterProjectInfo } from './FooterProjectInfo';
7
+ export { FooterMenuSections } from './FooterMenuSections';
8
+ export { FooterBottom } from './FooterBottom';
9
+ export { FooterSocialLinksComponent } from './FooterSocialLinks';
10
+ export { DjangoCFGLogo } from './DjangoCFGLogo';
11
+
12
+ export type {
13
+ PublicFooterProps,
14
+ FooterLink,
15
+ FooterMenuSection,
16
+ FooterSocialLinks,
17
+ } from './types';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Public Footer Types
3
+ */
4
+
5
+ import type { LucideIcon } from 'lucide-react';
6
+
7
+ export interface FooterLink {
8
+ label: string;
9
+ path: string;
10
+ external?: boolean;
11
+ }
12
+
13
+ export interface FooterMenuSection {
14
+ title: string;
15
+ items: FooterLink[];
16
+ }
17
+
18
+ export interface FooterSocialLinks {
19
+ github?: string;
20
+ linkedin?: string;
21
+ twitter?: string;
22
+ telegram?: string;
23
+ youtube?: string;
24
+ facebook?: string;
25
+ instagram?: string;
26
+ whatsapp?: string;
27
+ email?: string;
28
+ }
29
+
30
+ export interface PublicFooterProps {
31
+ /** Project name */
32
+ siteName: string;
33
+ /** Project description */
34
+ description?: string;
35
+ /** Logo path or URL */
36
+ logo?: string;
37
+ /** Optional badge */
38
+ badge?: {
39
+ icon: LucideIcon;
40
+ text: string;
41
+ };
42
+ /** Social media links */
43
+ socialLinks?: FooterSocialLinks;
44
+ /** Quick links (bottom bar) */
45
+ links?: FooterLink[];
46
+ /** Footer menu sections (desktop grid) */
47
+ menuSections?: FooterMenuSection[];
48
+ /** Copyright text (auto-generated if not provided) */
49
+ copyright?: string;
50
+ /** Credits */
51
+ credits?: {
52
+ text: string;
53
+ url?: string;
54
+ };
55
+ /** Variant: full (with all sections) or simple (minimal) */
56
+ variant?: 'full' | 'simple';
57
+ }
@@ -20,7 +20,7 @@ import {
20
20
  import { ThemeToggle } from '@djangocfg/ui-nextjs/theme';
21
21
  import { useAuth } from '@djangocfg/api/auth';
22
22
  import { UserMenu } from '../../_components/UserMenu';
23
- import type { NavigationItem } from '../PublicLayout';
23
+ import type { NavigationItem, UserMenuConfig } from '../../shared/types';
24
24
 
25
25
  interface PublicMobileDrawerProps {
26
26
  isOpen: boolean;
@@ -28,11 +28,7 @@ interface PublicMobileDrawerProps {
28
28
  logo?: string;
29
29
  siteName: string;
30
30
  navigation: NavigationItem[];
31
- userMenu?: {
32
- profilePath?: string;
33
- dashboardPath?: string;
34
- authPath?: string;
35
- };
31
+ userMenu?: UserMenuConfig;
36
32
  }
37
33
 
38
34
  export function PublicMobileDrawer({
@@ -83,6 +79,7 @@ export function PublicMobileDrawer({
83
79
  {/* User Menu */}
84
80
  <UserMenu
85
81
  variant="mobile"
82
+ groups={userMenu?.groups}
86
83
  profilePath={userMenu?.profilePath}
87
84
  dashboardPath={userMenu?.dashboardPath}
88
85
  authPath={userMenu?.authPath}
@@ -15,17 +15,13 @@ import { cn } from '@djangocfg/ui-nextjs/lib';
15
15
  import { useIsMobile } from '@djangocfg/ui-nextjs/hooks';
16
16
  import { useAuth } from '@djangocfg/api/auth';
17
17
  import { UserMenu } from '../../_components/UserMenu';
18
- import type { NavigationItem } from '../PublicLayout';
18
+ import type { NavigationItem, UserMenuConfig } from '../../shared/types';
19
19
 
20
20
  interface PublicNavigationProps {
21
21
  logo?: string;
22
22
  siteName: string;
23
23
  navigation: NavigationItem[];
24
- userMenu?: {
25
- profilePath?: string;
26
- dashboardPath?: string;
27
- authPath?: string;
28
- };
24
+ userMenu?: UserMenuConfig;
29
25
  onMobileMenuClick: () => void;
30
26
  }
31
27
 
@@ -74,6 +70,7 @@ export function PublicNavigation({
74
70
  {/* User Menu */}
75
71
  <UserMenu
76
72
  variant="desktop"
73
+ groups={userMenu?.groups}
77
74
  profilePath={userMenu?.profilePath}
78
75
  dashboardPath={userMenu?.dashboardPath}
79
76
  authPath={userMenu?.authPath}
@@ -3,5 +3,16 @@
3
3
  */
4
4
 
5
5
  export { PublicLayout } from './PublicLayout';
6
- export type { PublicLayoutProps, NavigationItem, FooterConfig } from './PublicLayout';
6
+ export type { PublicLayoutProps } from './PublicLayout';
7
+ export {
8
+ PublicFooter,
9
+ FooterProjectInfo,
10
+ FooterMenuSections,
11
+ FooterBottom,
12
+ FooterSocialLinksComponent,
13
+ DjangoCFGLogo,
14
+ } from './components/PublicFooter';
15
+ export type {
16
+ PublicFooterProps,
17
+ } from './components/PublicFooter';
7
18
 
@@ -1,15 +1,38 @@
1
1
  /**
2
2
  * User Menu Component for Layouts
3
3
  *
4
- * Simplified user menu component that works without AppContext
5
- * Uses only useAuth() hook
4
+ * Flexible user menu component with group-based structure (like footer)
5
+ * Uses useAuth() hook for authentication state
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { Settings, LogOut, User } from 'lucide-react';
10
+ *
11
+ * <UserMenu
12
+ * groups={[
13
+ * {
14
+ * title: 'Account',
15
+ * items: [
16
+ * { label: 'Profile', href: '/profile', icon: Settings },
17
+ * { label: 'Dashboard', href: '/dashboard', icon: User }
18
+ * ]
19
+ * },
20
+ * {
21
+ * items: [
22
+ * { label: 'Sign Out', onClick: () => logout(), icon: LogOut, variant: 'destructive' }
23
+ * ]
24
+ * }
25
+ * ]}
26
+ * authPath="/auth"
27
+ * />
28
+ * ```
6
29
  */
7
30
 
8
31
  'use client';
9
32
 
10
33
  import React from 'react';
11
34
  import Link from 'next/link';
12
- import { User, LogOut, Settings } from 'lucide-react';
35
+ import { LogOut, Settings } from 'lucide-react';
13
36
  import {
14
37
  DropdownMenu,
15
38
  DropdownMenuContent,
@@ -24,18 +47,26 @@ import {
24
47
  Button,
25
48
  } from '@djangocfg/ui-nextjs/components';
26
49
  import { useAuth } from '@djangocfg/api/auth';
50
+ import type { UserMenuGroup } from '../shared/types';
27
51
 
28
52
  export interface UserMenuProps {
53
+ /** Display variant */
29
54
  variant?: 'desktop' | 'mobile';
55
+ /** Menu groups for authenticated users */
56
+ groups?: UserMenuGroup[];
57
+ /** Auth page path (for sign in button) */
58
+ authPath?: string;
59
+ /** @deprecated Use groups instead - Profile page path (backward compatibility) */
30
60
  profilePath?: string;
61
+ /** @deprecated Use groups instead - Dashboard page path (backward compatibility) */
31
62
  dashboardPath?: string;
32
- authPath?: string;
33
63
  }
34
64
 
35
65
  export function UserMenu({
36
66
  variant = 'desktop',
37
- profilePath = '/profile',
38
- dashboardPath = '/dashboard',
67
+ groups,
68
+ profilePath = '/private/profile',
69
+ dashboardPath = '/private',
39
70
  authPath = '/auth',
40
71
  }: UserMenuProps) {
41
72
  const { user, isAuthenticated, logout } = useAuth();
@@ -49,6 +80,41 @@ export function UserMenu({
49
80
  return null;
50
81
  }
51
82
 
83
+ // Prepare menu groups (new groups prop or fallback to legacy props)
84
+ const menuGroups: UserMenuGroup[] = React.useMemo(() => {
85
+ if (groups && groups.length > 0) {
86
+ return groups;
87
+ }
88
+
89
+ // Fallback to legacy behavior for backward compatibility
90
+ const legacyGroups: UserMenuGroup[] = [];
91
+
92
+ if (profilePath) {
93
+ legacyGroups.push({
94
+ items: [
95
+ {
96
+ label: 'Profile',
97
+ href: profilePath,
98
+ icon: Settings,
99
+ },
100
+ ],
101
+ });
102
+ }
103
+
104
+ legacyGroups.push({
105
+ items: [
106
+ {
107
+ label: 'Sign Out',
108
+ onClick: () => logout(),
109
+ icon: LogOut,
110
+ variant: 'destructive',
111
+ },
112
+ ],
113
+ });
114
+
115
+ return legacyGroups;
116
+ }, [groups, profilePath, logout]);
117
+
52
118
  if (!isAuthenticated || !user) {
53
119
  // Guest user - show sign in button
54
120
  if (variant === 'mobile') {
@@ -92,22 +158,50 @@ export function UserMenu({
92
158
  </div>
93
159
  </div>
94
160
  <div className="space-y-1">
95
- {profilePath && (
96
- <Link
97
- href={profilePath}
98
- className="flex items-center gap-3 px-4 py-3 text-sm font-medium text-foreground hover:bg-accent hover:text-accent-foreground rounded-sm transition-colors"
99
- >
100
- <Settings className="h-4 w-4" />
101
- Profile
102
- </Link>
103
- )}
104
- <button
105
- onClick={() => logout()}
106
- className="flex items-center gap-3 px-4 py-3 text-sm font-medium text-destructive hover:bg-destructive/10 rounded-sm transition-colors w-full text-left"
107
- >
108
- <LogOut className="h-4 w-4" />
109
- Sign Out
110
- </button>
161
+ {menuGroups.map((group, groupIndex) => (
162
+ <div key={groupIndex}>
163
+ {group.title && (
164
+ <div className="px-4 py-2">
165
+ <p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
166
+ {group.title}
167
+ </p>
168
+ </div>
169
+ )}
170
+ {group.items.map((item, itemIndex) => {
171
+ const Icon = item.icon;
172
+ const isDestructive = item.variant === 'destructive';
173
+ const baseClasses = `flex items-center gap-3 px-4 py-3 text-sm font-medium rounded-sm transition-colors w-full text-left ${
174
+ isDestructive
175
+ ? 'text-destructive hover:bg-destructive/10'
176
+ : 'text-foreground hover:bg-accent hover:text-accent-foreground'
177
+ }`;
178
+
179
+ if (item.onClick) {
180
+ return (
181
+ <button
182
+ key={itemIndex}
183
+ onClick={item.onClick}
184
+ className={baseClasses}
185
+ >
186
+ {Icon && <Icon className="h-4 w-4" />}
187
+ {item.label}
188
+ </button>
189
+ );
190
+ }
191
+
192
+ if (item.href) {
193
+ return (
194
+ <Link key={itemIndex} href={item.href} className={baseClasses}>
195
+ {Icon && <Icon className="h-4 w-4" />}
196
+ {item.label}
197
+ </Link>
198
+ );
199
+ }
200
+
201
+ return null;
202
+ })}
203
+ </div>
204
+ ))}
111
205
  </div>
112
206
  </div>
113
207
  );
@@ -135,24 +229,51 @@ export function UserMenu({
135
229
  </div>
136
230
  </DropdownMenuLabel>
137
231
  <DropdownMenuSeparator />
138
- <DropdownMenuGroup>
139
- {profilePath && (
140
- <DropdownMenuItem asChild>
141
- <Link href={profilePath} className="flex items-center">
142
- <Settings className="mr-2 h-4 w-4" />
143
- <span>Profile</span>
144
- </Link>
145
- </DropdownMenuItem>
146
- )}
147
- </DropdownMenuGroup>
148
- <DropdownMenuSeparator />
149
- <DropdownMenuItem
150
- onClick={() => logout()}
151
- className="text-destructive focus:text-destructive"
152
- >
153
- <LogOut className="mr-2 h-4 w-4" />
154
- <span>Sign Out</span>
155
- </DropdownMenuItem>
232
+ {menuGroups.map((group, groupIndex) => (
233
+ <React.Fragment key={groupIndex}>
234
+ {groupIndex > 0 && <DropdownMenuSeparator />}
235
+ <DropdownMenuGroup>
236
+ {group.title && (
237
+ <DropdownMenuLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
238
+ {group.title}
239
+ </DropdownMenuLabel>
240
+ )}
241
+ {group.items.map((item, itemIndex) => {
242
+ const Icon = item.icon;
243
+ const isDestructive = item.variant === 'destructive';
244
+
245
+ if (item.onClick) {
246
+ return (
247
+ <DropdownMenuItem
248
+ key={itemIndex}
249
+ onClick={item.onClick}
250
+ className={isDestructive ? 'text-destructive focus:text-destructive' : ''}
251
+ >
252
+ {Icon && <Icon className="mr-2 h-4 w-4" />}
253
+ <span>{item.label}</span>
254
+ </DropdownMenuItem>
255
+ );
256
+ }
257
+
258
+ if (item.href) {
259
+ return (
260
+ <DropdownMenuItem key={itemIndex} asChild>
261
+ <Link
262
+ href={item.href}
263
+ className={`flex items-center ${isDestructive ? 'text-destructive' : ''}`}
264
+ >
265
+ {Icon && <Icon className="mr-2 h-4 w-4" />}
266
+ <span>{item.label}</span>
267
+ </Link>
268
+ </DropdownMenuItem>
269
+ );
270
+ }
271
+
272
+ return null;
273
+ })}
274
+ </DropdownMenuGroup>
275
+ </React.Fragment>
276
+ ))}
156
277
  </DropdownMenuContent>
157
278
  </DropdownMenu>
158
279
  );