@djangocfg/layouts 1.4.30 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -18
- package/package.json +15 -24
- package/src/auth/context/AuthContext.tsx +5 -5
- package/src/auth/hooks/useAuthGuard.ts +1 -1
- package/src/auth/hooks/useAutoAuth.ts +8 -7
- package/src/components/ErrorBoundary.tsx +78 -0
- package/src/components/JsonLd.tsx +31 -0
- package/src/components/LucideIcon.tsx +91 -0
- package/src/components/PageProgress.tsx +127 -0
- package/src/components/Suspense.tsx +29 -0
- package/src/{layouts/AppLayout/components → components}/UpdateNotifier/UpdateNotifier.tsx +56 -49
- package/src/components/index.ts +10 -0
- package/src/index.ts +25 -7
- package/src/layouts/AdminLayout/AdminLayout.tsx +46 -0
- package/src/layouts/AdminLayout/index.ts +7 -0
- package/src/layouts/AppLayout/AppLayout.tsx +278 -326
- package/src/layouts/AppLayout/index.ts +2 -39
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/AuthContext.tsx +3 -2
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/AuthHelp.tsx +1 -0
- package/src/layouts/AuthLayout/AuthLayout.tsx +61 -0
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/IdentifierForm.tsx +47 -34
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/OTPForm.tsx +2 -3
- package/src/layouts/AuthLayout/index.ts +24 -0
- package/src/layouts/{AppLayout/layouts/AuthLayout → AuthLayout}/types.ts +1 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +144 -0
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +32 -0
- package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +57 -0
- package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +141 -0
- package/src/layouts/PrivateLayout/components/index.ts +8 -0
- package/src/layouts/PrivateLayout/index.ts +7 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +15 -7
- package/src/layouts/PublicLayout/PublicLayout.tsx +121 -0
- package/src/layouts/PublicLayout/components/PublicFooter.tsx +190 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +117 -0
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +101 -0
- package/src/layouts/PublicLayout/components/index.ts +8 -0
- package/src/layouts/PublicLayout/index.ts +7 -0
- package/src/layouts/_components/UserMenu.tsx +160 -0
- package/src/layouts/_components/index.ts +7 -0
- package/src/layouts/index.ts +15 -8
- package/src/snippets/Analytics/AnalyticsProvider.tsx +8 -4
- package/src/snippets/Analytics/useAnalytics.ts +11 -21
- package/src/snippets/Chat/ChatWidget.tsx +4 -4
- package/src/snippets/ContactForm/ContactFormProvider.tsx +32 -19
- package/src/snippets/ContactForm/ContactPage.tsx +2 -4
- package/src/snippets/ContactForm/types.ts +3 -2
- package/src/snippets/index.ts +0 -1
- package/src/layouts/AppLayout/README.md +0 -204
- package/src/layouts/AppLayout/SUMMARY.md +0 -240
- package/src/layouts/AppLayout/USAGE.md +0 -312
- package/src/layouts/AppLayout/components/ErrorBoundary.tsx +0 -112
- package/src/layouts/AppLayout/components/PageProgress.tsx +0 -123
- package/src/layouts/AppLayout/components/Seo.tsx +0 -171
- package/src/layouts/AppLayout/components/UserMenu.tsx +0 -385
- package/src/layouts/AppLayout/components/index.ts +0 -11
- package/src/layouts/AppLayout/context/AppContext.tsx +0 -151
- package/src/layouts/AppLayout/context/index.ts +0 -5
- package/src/layouts/AppLayout/hooks/index.ts +0 -8
- package/src/layouts/AppLayout/hooks/useLayoutMode.ts +0 -26
- package/src/layouts/AppLayout/hooks/useNavigation.ts +0 -51
- package/src/layouts/AppLayout/layouts/AdminLayout/AdminLayout.tsx +0 -224
- package/src/layouts/AppLayout/layouts/AdminLayout/README.md +0 -409
- package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.example.tsx +0 -98
- package/src/layouts/AppLayout/layouts/AdminLayout/components/PagePreloader.tsx +0 -149
- package/src/layouts/AppLayout/layouts/AdminLayout/components/ParentSync.tsx +0 -146
- package/src/layouts/AppLayout/layouts/AdminLayout/components/index.ts +0 -3
- package/src/layouts/AppLayout/layouts/AdminLayout/context/CfgAppContext.tsx +0 -48
- package/src/layouts/AppLayout/layouts/AdminLayout/context/index.ts +0 -2
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/index.ts +0 -6
- package/src/layouts/AppLayout/layouts/AdminLayout/hooks/useApp.ts +0 -279
- package/src/layouts/AppLayout/layouts/AdminLayout/index.ts +0 -24
- package/src/layouts/AppLayout/layouts/AdminLayout/lottie/energizing.json +0 -1
- package/src/layouts/AppLayout/layouts/AdminLayout/types/index.ts +0 -45
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthLayout.tsx +0 -41
- package/src/layouts/AppLayout/layouts/AuthLayout/index.ts +0 -15
- package/src/layouts/AppLayout/layouts/PrivateLayout/PrivateLayout.tsx +0 -82
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardContent.tsx +0 -62
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardHeader.tsx +0 -89
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +0 -181
- package/src/layouts/AppLayout/layouts/PrivateLayout/components/index.ts +0 -9
- package/src/layouts/AppLayout/layouts/PrivateLayout/index.ts +0 -5
- package/src/layouts/AppLayout/layouts/PublicLayout/PublicLayout.tsx +0 -44
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +0 -242
- package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileDrawer.tsx +0 -150
- package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +0 -169
- package/src/layouts/AppLayout/layouts/PublicLayout/index.ts +0 -5
- package/src/layouts/AppLayout/layouts/index.ts +0 -7
- package/src/layouts/AppLayout/providers/CoreProviders.tsx +0 -80
- package/src/layouts/AppLayout/providers/index.ts +0 -5
- package/src/layouts/AppLayout/types/config.ts +0 -79
- package/src/layouts/AppLayout/types/index.ts +0 -11
- package/src/layouts/AppLayout/types/layout.ts +0 -54
- package/src/layouts/AppLayout/types/navigation.ts +0 -43
- package/src/layouts/AppLayout/types/page.ts +0 -80
- package/src/layouts/AppLayout/types/routes.ts +0 -43
- package/src/layouts/AppLayout/utils/index.ts +0 -5
- package/src/layouts/AppLayout/utils/routeDetection.ts +0 -31
- package/src/layouts/ErrorLayout/ErrorLayout.tsx +0 -173
- package/src/layouts/ErrorLayout/errorConfig.tsx +0 -152
- package/src/layouts/ErrorLayout/index.ts +0 -8
- package/src/layouts/SimpleLayout/SimpleLayout.tsx +0 -72
- package/src/layouts/SimpleLayout/index.ts +0 -3
- package/src/snippets/VideoPlayer/README.md +0 -238
- package/src/snippets/VideoPlayer/VideoControls.tsx +0 -137
- package/src/snippets/VideoPlayer/VideoPlayer.tsx +0 -248
- package/src/snippets/VideoPlayer/index.ts +0 -8
- package/src/snippets/VideoPlayer/types.ts +0 -61
- package/src/types/index.ts +0 -2
- package/src/types/pageConfig.ts +0 -100
- /package/src/{validation → components/ErrorsTracker}/README.md +0 -0
- /package/src/{validation → components/ErrorsTracker}/components/ErrorButtons.tsx +0 -0
- /package/src/{validation → components/ErrorsTracker}/components/ErrorToast.tsx +0 -0
- /package/src/{validation → components/ErrorsTracker}/hooks.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/index.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/providers/ErrorTrackingProvider.tsx +0 -0
- /package/src/{validation → components/ErrorsTracker}/types.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/utils/curl-generator.ts +0 -0
- /package/src/{validation → components/ErrorsTracker}/utils/formatters.ts +0 -0
- /package/src/{layouts/AppLayout/components → components}/UpdateNotifier/index.ts +0 -0
|
@@ -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';
|