@djangocfg/layouts 1.4.30 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +277 -18
  2. package/package.json +15 -24
  3. package/src/auth/context/AuthContext.tsx +5 -5
  4. package/src/auth/hooks/useAuthGuard.ts +1 -1
  5. package/src/auth/hooks/useAutoAuth.ts +8 -7
  6. package/src/components/ErrorBoundary.tsx +78 -0
  7. package/src/components/JsonLd.tsx +31 -0
  8. package/src/components/LucideIcon.tsx +91 -0
  9. package/src/components/PageProgress.tsx +127 -0
  10. package/src/components/Suspense.tsx +29 -0
  11. package/src/{layouts/AppLayout/components → components}/UpdateNotifier/UpdateNotifier.tsx +56 -49
  12. package/src/components/index.ts +10 -0
  13. package/src/index.ts +25 -7
  14. package/src/layouts/AdminLayout/AdminLayout.tsx +46 -0
  15. package/src/layouts/AdminLayout/index.ts +7 -0
  16. package/src/layouts/AppLayout/AppLayout.tsx +278 -326
  17. package/src/layouts/AppLayout/index.ts +2 -39
  18. package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/AuthContext.tsx +3 -2
  19. package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/AuthHelp.tsx +1 -0
  20. package/src/layouts/AuthLayout/AuthLayout.tsx +61 -0
  21. package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/IdentifierForm.tsx +47 -34
  22. package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/OTPForm.tsx +2 -3
  23. package/src/layouts/AuthLayout/index.ts +24 -0
  24. package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/types.ts +1 -0
  25. package/src/layouts/PrivateLayout/PrivateLayout.tsx +144 -0
  26. package/src/layouts/PrivateLayout/components/PrivateContent.tsx +32 -0
  27. package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +57 -0
  28. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +141 -0
  29. package/src/layouts/PrivateLayout/components/index.ts +8 -0
  30. package/src/layouts/PrivateLayout/index.ts +7 -0
  31. package/src/layouts/ProfileLayout/ProfileLayout.tsx +15 -7
  32. package/src/layouts/PublicLayout/PublicLayout.tsx +121 -0
  33. package/src/layouts/PublicLayout/components/PublicFooter.tsx +190 -0
  34. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +117 -0
  35. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +101 -0
  36. package/src/layouts/PublicLayout/components/index.ts +8 -0
  37. package/src/layouts/PublicLayout/index.ts +7 -0
  38. package/src/layouts/_components/UserMenu.tsx +160 -0
  39. package/src/layouts/_components/index.ts +7 -0
  40. package/src/layouts/index.ts +15 -8
  41. package/src/snippets/Analytics/AnalyticsProvider.tsx +8 -4
  42. package/src/snippets/Analytics/useAnalytics.ts +11 -21
  43. package/src/snippets/Chat/ChatWidget.tsx +4 -4
  44. package/src/snippets/ContactForm/ContactFormProvider.tsx +32 -19
  45. package/src/snippets/ContactForm/ContactPage.tsx +2 -4
  46. package/src/snippets/ContactForm/types.ts +3 -2
  47. package/src/snippets/index.ts +0 -1
  48. package/src/layouts/AppLayout/README.md +0 -204
  49. package/src/layouts/AppLayout/SUMMARY.md +0 -240
  50. package/src/layouts/AppLayout/USAGE.md +0 -312
  51. package/src/layouts/AppLayout/components/ErrorBoundary.tsx +0 -112
  52. package/src/layouts/AppLayout/components/PageProgress.tsx +0 -123
  53. package/src/layouts/AppLayout/components/Seo.tsx +0 -171
  54. package/src/layouts/AppLayout/components/UserMenu.tsx +0 -385
  55. package/src/layouts/AppLayout/components/index.ts +0 -11
  56. package/src/layouts/AppLayout/context/AppContext.tsx +0 -151
  57. package/src/layouts/AppLayout/context/index.ts +0 -5
  58. package/src/layouts/AppLayout/hooks/index.ts +0 -8
  59. package/src/layouts/AppLayout/hooks/useLayoutMode.ts +0 -26
  60. package/src/layouts/AppLayout/hooks/useNavigation.ts +0 -51
  61. package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +0 -224
  62. package/src/layouts/AppLayout/layouts/AdminLayout/README.md +0 -409
  63. package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.example.tsx +0 -98
  64. package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.tsx +0 -149
  65. package/src/layouts/AppLayout/layouts/AdminLayout/components/ParentSync.tsx +0 -146
  66. package/src/layouts/AppLayout/layouts/AdminLayout/components/index.ts +0 -3
  67. package/src/layouts/AppLayout/layouts/AdminLayout/context/CfgAppContext.tsx +0 -48
  68. package/src/layouts/AppLayout/layouts/AdminLayout/context/index.ts +0 -2
  69. package/src/layouts/AppLayout/layouts/AdminLayout/hooks/index.ts +0 -6
  70. package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +0 -279
  71. package/src/layouts/AppLayout/layouts/AdminLayout/index.ts +0 -24
  72. package/src/layouts/AppLayout/layouts/AdminLayout/lottie/energizing.json +0 -1
  73. package/src/layouts/AppLayout/layouts/AdminLayout/types/index.ts +0 -45
  74. package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +0 -41
  75. package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +0 -15
  76. package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +0 -82
  77. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +0 -62
  78. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +0 -89
  79. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +0 -181
  80. package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +0 -9
  81. package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +0 -5
  82. package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +0 -44
  83. package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +0 -242
  84. package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileDrawer.tsx +0 -150
  85. package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +0 -169
  86. package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +0 -5
  87. package/src/layouts/AppLayout/layouts/index.ts +0 -7
  88. package/src/layouts/AppLayout/providers/CoreProviders.tsx +0 -80
  89. package/src/layouts/AppLayout/providers/index.ts +0 -5
  90. package/src/layouts/AppLayout/types/config.ts +0 -79
  91. package/src/layouts/AppLayout/types/index.ts +0 -11
  92. package/src/layouts/AppLayout/types/layout.ts +0 -54
  93. package/src/layouts/AppLayout/types/navigation.ts +0 -43
  94. package/src/layouts/AppLayout/types/page.ts +0 -80
  95. package/src/layouts/AppLayout/types/routes.ts +0 -43
  96. package/src/layouts/AppLayout/utils/index.ts +0 -5
  97. package/src/layouts/AppLayout/utils/routeDetection.ts +0 -31
  98. package/src/layouts/ErrorLayout/ErrorLayout.tsx +0 -173
  99. package/src/layouts/ErrorLayout/errorConfig.tsx +0 -152
  100. package/src/layouts/ErrorLayout/index.ts +0 -8
  101. package/src/layouts/SimpleLayout/SimpleLayout.tsx +0 -72
  102. package/src/layouts/SimpleLayout/index.ts +0 -3
  103. package/src/snippets/VideoPlayer/README.md +0 -238
  104. package/src/snippets/VideoPlayer/VideoControls.tsx +0 -137
  105. package/src/snippets/VideoPlayer/VideoPlayer.tsx +0 -248
  106. package/src/snippets/VideoPlayer/index.ts +0 -8
  107. package/src/snippets/VideoPlayer/types.ts +0 -61
  108. package/src/types/index.ts +0 -2
  109. package/src/types/pageConfig.ts +0 -100
  110. /package/src/{validation → components/ErrorsTracker}/README.md +0 -0
  111. /package/src/{validation → components/ErrorsTracker}/components/ErrorButtons.tsx +0 -0
  112. /package/src/{validation → components/ErrorsTracker}/components/ErrorToast.tsx +0 -0
  113. /package/src/{validation → components/ErrorsTracker}/hooks.ts +0 -0
  114. /package/src/{validation → components/ErrorsTracker}/index.ts +0 -0
  115. /package/src/{validation → components/ErrorsTracker}/providers/ErrorTrackingProvider.tsx +0 -0
  116. /package/src/{validation → components/ErrorsTracker}/types.ts +0 -0
  117. /package/src/{validation → components/ErrorsTracker}/utils/curl-generator.ts +0 -0
  118. /package/src/{validation → components/ErrorsTracker}/utils/formatters.ts +0 -0
  119. /package/src/{layouts/AppLayout/components → components}/UpdateNotifier/index.ts +0 -0
@@ -1,123 +0,0 @@
1
- "use client"
2
-
3
- import { useRouter } from 'next/router';
4
- import { useEffect, useRef, useState } from 'react';
5
-
6
- const PageProgress = () => {
7
- const router = useRouter();
8
- const [loading, setLoading] = useState(false);
9
- const [progress, setProgress] = useState(0);
10
- const [mounted, setMounted] = useState(false);
11
- const progressTimer = useRef<NodeJS.Timeout | null>(null);
12
-
13
- useEffect(() => {
14
- setMounted(true);
15
- }, []);
16
-
17
- // Simulate realistic progress
18
- const startFakeProgress = () => {
19
- // Clear any existing timer
20
- if (progressTimer.current) {
21
- clearInterval(progressTimer.current);
22
- }
23
-
24
- setProgress(0);
25
-
26
- // Quickly go to 20% to show immediate feedback
27
- setTimeout(() => setProgress(20), 50);
28
-
29
- // Then slowly increase to 90% (never reach 100% until actually complete)
30
- progressTimer.current = setInterval(() => {
31
- setProgress((prevProgress) => {
32
- if (prevProgress >= 90) {
33
- if (progressTimer.current) {
34
- clearInterval(progressTimer.current);
35
- }
36
- return 90;
37
- }
38
-
39
- // Slow down as we get closer to 90%
40
- const increment = 90 - prevProgress;
41
- return prevProgress + (increment / 10);
42
- });
43
- }, 300);
44
- };
45
-
46
- const completeProgress = () => {
47
- // Clear any existing timer
48
- if (progressTimer.current) {
49
- clearInterval(progressTimer.current);
50
- progressTimer.current = null;
51
- }
52
-
53
- // Jump to 100% and then hide after showing completion
54
- setProgress(100);
55
- setTimeout(() => {
56
- setLoading(false);
57
- setTimeout(() => {
58
- setProgress(0);
59
- }, 300); // Wait for fade out animation
60
- }, 500); // Show 100% for half a second
61
- };
62
-
63
- useEffect(() => {
64
- const handleRouteChangeStart = (url: string, { shallow }: { shallow: boolean }) => {
65
- if (!shallow) {
66
- setLoading(true);
67
- startFakeProgress();
68
- }
69
- };
70
-
71
- const handleRouteChangeComplete = () => {
72
- completeProgress();
73
- };
74
-
75
- const handleRouteChangeError = () => {
76
- completeProgress();
77
- };
78
-
79
- router.events.on('routeChangeStart', handleRouteChangeStart);
80
- router.events.on('routeChangeComplete', handleRouteChangeComplete);
81
- router.events.on('routeChangeError', handleRouteChangeError);
82
-
83
- return () => {
84
- if (progressTimer.current) {
85
- clearInterval(progressTimer.current);
86
- }
87
- router.events.off('routeChangeStart', handleRouteChangeStart);
88
- router.events.off('routeChangeComplete', handleRouteChangeComplete);
89
- router.events.off('routeChangeError', handleRouteChangeError);
90
- };
91
- }, [router.events]);
92
-
93
- if (!mounted) {
94
- return null;
95
- }
96
-
97
- return (
98
- <div
99
- data-page-progress="root"
100
- data-loading={loading}
101
- data-progress={progress}
102
- className={`fixed top-0 left-0 w-full transition-opacity duration-300 ${
103
- loading ? 'opacity-100' : 'opacity-0'
104
- }`}
105
- style={{
106
- zIndex: 99999,
107
- height: '3px',
108
- }}
109
- >
110
- <div
111
- className="h-full transition-all duration-200 ease-linear"
112
- style={{
113
- width: `${progress}%`,
114
- background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 50%, #3b82f6 100%)',
115
- boxShadow: '0 0 10px rgba(59, 130, 246, 0.6), 0 0 20px rgba(59, 130, 246, 0.4), 0 0 30px rgba(59, 130, 246, 0.2)',
116
- filter: 'drop-shadow(0 0 8px rgba(59, 130, 246, 0.8))',
117
- }}
118
- />
119
- </div>
120
- );
121
- };
122
-
123
- export default PageProgress;
@@ -1,171 +0,0 @@
1
- "use client"
2
-
3
- import { Fragment } from 'react';
4
- import Head from 'next/head';
5
- import { useRouter } from 'next/router';
6
- import { generateOgImageUrl } from '@djangocfg/og-image/utils';
7
-
8
- import { PageConfig } from '../../../types/pageConfig';
9
-
10
- interface SeoProps {
11
- pageConfig: PageConfig;
12
- icons?: {
13
- logo192?: string;
14
- logo384?: string;
15
- logo512?: string;
16
- logoVector?: string;
17
- };
18
- siteUrl?: string;
19
- /** Override canonical URL (defaults to current page URL) */
20
- canonicalUrl?: string;
21
- }
22
-
23
- /**
24
- * Check if URL is absolute (starts with http:// or https://)
25
- */
26
- function isAbsoluteUrl(url: string): boolean {
27
- return /^https?:\/\//i.test(url);
28
- }
29
-
30
- /**
31
- * Get absolute site URL with smart fallbacks
32
- *
33
- * Priority:
34
- * 1. Provided siteUrl (if absolute)
35
- * 2. NEXT_PUBLIC_SITE_URL env var
36
- * 3. window.location.origin (client-side only)
37
- */
38
- function getAbsoluteSiteUrl(siteUrl?: string): string | null {
39
- // 1. Check if provided siteUrl is already absolute
40
- if (siteUrl && isAbsoluteUrl(siteUrl)) {
41
- return siteUrl.replace(/\/$/, ''); // Remove trailing slash
42
- }
43
-
44
- // 2. Check NEXT_PUBLIC_SITE_URL env var
45
- const envSiteUrl = process.env.NEXT_PUBLIC_SITE_URL;
46
- if (envSiteUrl && isAbsoluteUrl(envSiteUrl)) {
47
- return envSiteUrl.replace(/\/$/, '');
48
- }
49
-
50
- // 3. Client-side fallback (not available during SSR)
51
- if (typeof window !== 'undefined') {
52
- return window.location.origin;
53
- }
54
-
55
- return null;
56
- }
57
-
58
- export default function Seo({ pageConfig, icons, siteUrl, canonicalUrl }: SeoProps) {
59
- const router = useRouter();
60
-
61
- const {
62
- title,
63
- description,
64
- keywords,
65
- jsonLd,
66
- ogImage,
67
- openGraph,
68
- twitter,
69
- } = pageConfig;
70
-
71
- const ogTitle = ogImage?.title || title;
72
- const ogSubtitle = ogImage?.subtitle || description;
73
-
74
- // Get absolute site URL with smart fallbacks
75
- const absoluteSiteUrl = getAbsoluteSiteUrl(siteUrl);
76
-
77
- // Build canonical URL: custom > siteUrl + current path
78
- const currentPath = router.asPath.split('?')[0]; // Remove query params
79
- const absoluteCanonicalUrl = canonicalUrl
80
- || (absoluteSiteUrl ? `${absoluteSiteUrl}${currentPath}` : null);
81
-
82
- // Generate OG image URL using @djangocfg/og-image utilities
83
- const ogImageUrl = ogImage
84
- ? generateOgImageUrl('/api/og', {
85
- title: ogTitle || 'Untitled',
86
- subtitle: ogSubtitle || '',
87
- description: ogSubtitle || '',
88
- })
89
- : null;
90
-
91
- // Make absolute URL - only render og:image if we have absolute site URL
92
- const absoluteOgImageUrl = ogImageUrl && absoluteSiteUrl
93
- ? `${absoluteSiteUrl}${ogImageUrl}`
94
- : null;
95
-
96
- return (
97
- <Head>
98
- <title>{title}</title>
99
- <meta name="description" content={description} />
100
- {keywords && <meta name="keywords" content={keywords} />}
101
-
102
- {/* Favicon */}
103
- <link rel="icon" type="image/png" href={icons?.logo192 || '/favicon.png'} />
104
-
105
- {/* Canonical URL - important for SEO */}
106
- {absoluteCanonicalUrl && (
107
- <link rel="canonical" href={absoluteCanonicalUrl} />
108
- )}
109
-
110
- {/* Open Graph */}
111
- <meta property="og:title" content={openGraph?.title || ogTitle} />
112
- <meta property="og:description" content={openGraph?.description || ogSubtitle} />
113
- <meta property="og:type" content={openGraph?.type || 'website'} />
114
-
115
- {/* OG Canonical URL */}
116
- {absoluteCanonicalUrl && (
117
- <meta property="og:url" content={absoluteCanonicalUrl} />
118
- )}
119
-
120
- {/* Site Name */}
121
- {(openGraph?.siteName || pageConfig.projectName) && (
122
- <meta property="og:site_name" content={openGraph?.siteName || pageConfig.projectName} />
123
- )}
124
-
125
- {/* Twitter */}
126
- <meta name="twitter:card" content={twitter?.card || 'summary_large_image'} />
127
- <meta name="twitter:title" content={twitter?.title || ogTitle} />
128
- <meta name="twitter:description" content={twitter?.description || ogSubtitle} />
129
- {twitter?.site && <meta name="twitter:site" content={twitter.site} />}
130
- {twitter?.creator && <meta name="twitter:creator" content={twitter.creator} />}
131
-
132
- {/* OG Images */}
133
- {openGraph?.images?.length ? (
134
- openGraph.images.map((image, index) => (
135
- <Fragment key={index}>
136
- <meta property="og:image" content={image.url} />
137
- {image.width && <meta property="og:image:width" content={String(image.width)} />}
138
- {image.height && <meta property="og:image:height" content={String(image.height)} />}
139
- {image.alt && <meta property="og:image:alt" content={image.alt} />}
140
- </Fragment>
141
- ))
142
- ) : absoluteOgImageUrl ? (
143
- <>
144
- <meta property="og:image" content={absoluteOgImageUrl} />
145
- <meta property="og:image:width" content="1200" />
146
- <meta property="og:image:height" content="630" />
147
- <meta property="og:image:type" content="image/png" />
148
- </>
149
- ) : null}
150
-
151
- {/* Twitter Images */}
152
- {twitter?.images?.length ? (
153
- twitter.images.map((image, index) => (
154
- <meta key={index} name="twitter:image" content={image} />
155
- ))
156
- ) : absoluteOgImageUrl ? (
157
- <meta name="twitter:image" content={absoluteOgImageUrl} />
158
- ) : null}
159
-
160
- {/* JSON-LD */}
161
- {jsonLd && (
162
- <script
163
- type="application/ld+json"
164
- dangerouslySetInnerHTML={{
165
- __html: JSON.stringify(jsonLd),
166
- }}
167
- />
168
- )}
169
- </Head>
170
- );
171
- }
@@ -1,385 +0,0 @@
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, Avatar, AvatarImage, AvatarFallback } 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
- // Fix hydration mismatch by only rendering auth-dependent content after mount
41
- const [mounted, setMounted] = React.useState(false);
42
- React.useEffect(() => {
43
- setMounted(true);
44
- }, []);
45
-
46
- const { publicLayout, routes } = config;
47
-
48
- // Desktop: determine if user is on dashboard
49
- const isDashboard = variant === 'desktop' && publicLayout.userMenu.dashboardPath
50
- ? router.pathname.includes(publicLayout.userMenu.dashboardPath)
51
- : false;
52
-
53
- // === DATA PREPARATION (before rendering) ===
54
-
55
- // Prepare user data
56
- const userAvatar = user?.avatar || '';
57
- const userEmail = user?.email || '';
58
- const displayName = user?.display_username || userEmail || 'User';
59
- const userInitial = displayName.charAt(0).toUpperCase();
60
- const dashboardPath = publicLayout.userMenu.dashboardPath;
61
- const profilePath = publicLayout.userMenu.profilePath;
62
-
63
- // Handle logout
64
- const handleLogout = React.useCallback(() => {
65
- logout();
66
- if (variant === 'desktop') {
67
- closeUserMenu();
68
- } else if (onNavigate) {
69
- onNavigate();
70
- }
71
- }, [logout, variant, closeUserMenu, onNavigate]);
72
-
73
- // Prepare menu items
74
- const menuItems = React.useMemo<MenuItem[]>(() => {
75
- const items: MenuItem[] = [];
76
-
77
- // Profile
78
- items.push({
79
- id: 'profile',
80
- type: 'link',
81
- icon: <Settings className="size-4" />,
82
- label: 'Profile',
83
- href: profilePath,
84
- variant: 'ghost',
85
- });
86
-
87
- // Logout
88
- items.push({
89
- id: 'logout',
90
- type: 'button',
91
- icon: <LogOut className="size-4" />,
92
- label: 'Sign out',
93
- onClick: handleLogout,
94
- variant: 'ghost',
95
- className: 'text-destructive hover:text-destructive hover:bg-destructive/10',
96
- });
97
-
98
- return items;
99
- }, [profilePath, handleLogout]);
100
-
101
- // === UNIFIED MENU ITEM RENDERER ===
102
-
103
- /**
104
- * Renders a single menu item (link or button)
105
- * Used for both desktop dropdown and mobile icons
106
- */
107
- const renderMenuItem = React.useCallback((
108
- item: MenuItem,
109
- mode: 'desktop' | 'mobile-icon',
110
- onClick?: () => void
111
- ) => {
112
- if (mode === 'mobile-icon') {
113
- // Mobile: render as icon-only button
114
- const iconElement = item.icon as React.ReactElement;
115
- const resizedIcon = React.isValidElement(iconElement)
116
- ? React.cloneElement(iconElement, { className: 'h-5 w-5' } as any)
117
- : iconElement;
118
-
119
- if (item.type === 'link' && item.href) {
120
- return (
121
- <ButtonLink
122
- key={item.id}
123
- href={item.href}
124
- variant={item.variant as any}
125
- size="icon"
126
- className="h-9 w-9"
127
- onClick={onClick}
128
- aria-label={item.label}
129
- >
130
- {resizedIcon}
131
- </ButtonLink>
132
- );
133
- }
134
-
135
- return (
136
- <Button
137
- key={item.id}
138
- onClick={item.onClick}
139
- variant={item.variant as any}
140
- size="icon"
141
- className={`h-9 w-9 ${item.className || ''}`}
142
- aria-label={item.label}
143
- >
144
- {resizedIcon}
145
- </Button>
146
- );
147
- }
148
-
149
- // Desktop: render with text and icon
150
- const baseClassName = `w-full justify-start gap-2 ${item.className || ''}`;
151
-
152
- if (item.type === 'link' && item.href) {
153
- return (
154
- <ButtonLink
155
- key={item.id}
156
- href={item.href}
157
- variant={item.variant as any}
158
- size="sm"
159
- className={baseClassName}
160
- onClick={onClick}
161
- >
162
- {item.icon}
163
- {item.label}
164
- </ButtonLink>
165
- );
166
- }
167
-
168
- return (
169
- <Button
170
- key={item.id}
171
- onClick={item.onClick}
172
- variant={item.variant as any}
173
- size="sm"
174
- className={baseClassName}
175
- >
176
- {item.icon}
177
- {item.label}
178
- </Button>
179
- );
180
- }, []);
181
-
182
- // === RENDERING ===
183
-
184
- // Desktop variant
185
- if (variant === 'desktop') {
186
- // Show loading state during hydration
187
- if (!mounted) {
188
- return (
189
- <div className="flex items-center gap-3">
190
- <div className="h-9 w-20 animate-pulse bg-muted rounded-sm" />
191
- </div>
192
- );
193
- }
194
-
195
- return (
196
- <div className="flex items-center gap-3">
197
- {isAuthenticated ? (
198
- <div className="flex items-center gap-3">
199
- {/* Dashboard button (only if not on dashboard) */}
200
- {dashboardPath && !isDashboard && (
201
- <ButtonLink
202
- href={dashboardPath}
203
- variant="default"
204
- size="sm"
205
- className="gap-2"
206
- >
207
- <Crown className="size-4" />
208
- Dashboard
209
- </ButtonLink>
210
- )}
211
-
212
- {/* User Dropdown */}
213
- <div className="relative">
214
- <button
215
- className="flex items-center gap-2 px-2 py-1.5 rounded-sm text-sm font-medium transition-colors text-foreground hover:text-primary hover:bg-accent/50 cursor-pointer"
216
- onClick={toggleUserMenu}
217
- aria-haspopup="true"
218
- aria-expanded={userMenuOpen}
219
- >
220
- <Avatar className="h-7 w-7 ring-1 ring-primary/20">
221
- <AvatarImage
222
- src={userAvatar}
223
- alt={displayName}
224
- />
225
- <AvatarFallback className="bg-primary/10 text-primary text-xs">
226
- {userInitial}
227
- </AvatarFallback>
228
- </Avatar>
229
- <span className="max-w-[120px] truncate">
230
- {displayName}
231
- </span>
232
- <ChevronDown
233
- className={`size-4 transition-transform ${
234
- userMenuOpen ? 'rotate-180' : ''
235
- }`}
236
- />
237
- </button>
238
-
239
- {userMenuOpen && (
240
- <>
241
- {/* Backdrop */}
242
- <div
243
- className="fixed inset-0 z-9995 bg-transparent"
244
- onClick={closeUserMenu}
245
- aria-hidden="true"
246
- style={{ cursor: 'default' }}
247
- />
248
- {/* Dropdown */}
249
- <div
250
- className="absolute top-full right-0 mt-2 w-52 rounded-sm shadow-sm backdrop-blur-xl z-9996 bg-popover border border-border"
251
- role="menu"
252
- aria-label="User menu"
253
- >
254
- <div className="p-2">
255
- {/* User info */}
256
- <div className="px-3 py-2 text-sm mb-2 border-b border-border">
257
- <div className="text-muted-foreground">Signed in as:</div>
258
- <div className="font-medium truncate text-popover-foreground mt-1">
259
- {userEmail}
260
- </div>
261
- </div>
262
-
263
- {/* Menu items - unified rendering */}
264
- {menuItems.map((item) => renderMenuItem(item, 'desktop', closeUserMenu))}
265
- </div>
266
- </div>
267
- </>
268
- )}
269
- </div>
270
- </div>
271
- ) : (
272
- /* Guest - Sign in button */
273
- <ButtonLink href={routes.auth} variant="default" size="sm" className="h-9 gap-1.5">
274
- <User className="w-4 h-4" />
275
- Sign In
276
- </ButtonLink>
277
- )}
278
- </div>
279
- );
280
- }
281
-
282
- // Mobile variant
283
- // Show loading state during hydration
284
- if (!mounted) {
285
- return (
286
- <Card className="border-border !bg-accent/50">
287
- <CardContent className="p-4">
288
- <div className="h-24 animate-pulse bg-muted rounded-sm" />
289
- </CardContent>
290
- </Card>
291
- );
292
- }
293
-
294
- if (isAuthenticated) {
295
- return (
296
- <Card className="border-primary/20 shadow-lg !bg-accent/50">
297
- <CardContent className="p-4">
298
- {/* User Info Header */}
299
- <div className="flex items-center gap-3 mb-4 p-3 rounded-sm border border-border bg-accent/70">
300
- <div className="w-10 h-10 rounded-full flex items-center justify-center bg-primary flex-shrink-0 overflow-hidden relative">
301
- {userAvatar ? (
302
- <img
303
- src={userAvatar}
304
- alt={displayName}
305
- className="w-10 h-10 rounded-full object-cover"
306
- />
307
- ) : (
308
- <User className="w-5 h-5 text-primary-foreground" />
309
- )}
310
- {/* Active indicator */}
311
- <div className="absolute -bottom-0.5 -right-0.5 size-3 rounded-full bg-green-500 border-2 border-background" />
312
- </div>
313
- <div className="flex-1 min-w-0">
314
- <p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
315
- Signed in as
316
- </p>
317
- <p className="text-sm font-semibold truncate text-foreground">
318
- {displayName}
319
- </p>
320
- </div>
321
- </div>
322
-
323
- {/* Action Buttons */}
324
- <div className="space-y-3">
325
- {/* Dashboard link */}
326
- {dashboardPath && (
327
- <ButtonLink
328
- href={dashboardPath}
329
- variant="default"
330
- size="sm"
331
- className="w-full h-9 gap-2"
332
- onClick={onNavigate}
333
- >
334
- <Crown className="size-4" />
335
- Dashboard
336
- </ButtonLink>
337
- )}
338
-
339
- {/* Quick Actions - Icons only - unified rendering */}
340
- <div className="flex items-center justify-center gap-2 pt-3 mt-1 border-t border-border/30">
341
- {menuItems.map((item) => renderMenuItem(item, 'mobile-icon', onNavigate))}
342
-
343
- {/* Theme Toggle */}
344
- <ThemeToggle />
345
- </div>
346
- </div>
347
- </CardContent>
348
- </Card>
349
- );
350
- }
351
-
352
- // Mobile Guest Card
353
- return (
354
- <Card className="border-border !bg-accent/50">
355
- <CardContent className="p-4">
356
- <div className="text-center space-y-4">
357
- <div className="w-12 h-12 rounded-full flex items-center justify-center mx-auto bg-muted">
358
- <User className="w-6 h-6 text-muted-foreground" />
359
- </div>
360
- <div>
361
- <p className="text-sm font-medium mb-1 text-foreground">Welcome!</p>
362
- <p className="text-xs text-muted-foreground">
363
- Sign in to access your dashboard
364
- </p>
365
- </div>
366
- <ButtonLink
367
- href={routes.auth}
368
- variant="default"
369
- size="default"
370
- className="w-full gap-2"
371
- onClick={onNavigate}
372
- >
373
- <User className="w-5 h-5" />
374
- Sign In
375
- </ButtonLink>
376
-
377
- {/* Theme toggle */}
378
- <div className="flex justify-center pt-2 border-t border-border/30">
379
- <ThemeToggle />
380
- </div>
381
- </div>
382
- </CardContent>
383
- </Card>
384
- );
385
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * Components Module
3
- */
4
-
5
- export { default as Seo } from './Seo';
6
- export { default as PageProgress } from './PageProgress';
7
- export { ErrorBoundary } from './ErrorBoundary';
8
- export { UpdateNotifier } from './UpdateNotifier';
9
- export { UserMenu } from './UserMenu';
10
- export type { UpdateNotifierProps } from './UpdateNotifier';
11
- export type { UserMenuProps } from './UserMenu';