@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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User Menu Component for Layouts
|
|
3
|
+
*
|
|
4
|
+
* Simplified user menu component that works without AppContext
|
|
5
|
+
* Uses only useAuth() hook
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import Link from 'next/link';
|
|
12
|
+
import { User, LogOut, Settings } from 'lucide-react';
|
|
13
|
+
import {
|
|
14
|
+
DropdownMenu,
|
|
15
|
+
DropdownMenuContent,
|
|
16
|
+
DropdownMenuGroup,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuLabel,
|
|
19
|
+
DropdownMenuSeparator,
|
|
20
|
+
DropdownMenuTrigger,
|
|
21
|
+
Avatar,
|
|
22
|
+
AvatarFallback,
|
|
23
|
+
AvatarImage,
|
|
24
|
+
Button,
|
|
25
|
+
} from '@djangocfg/ui/components';
|
|
26
|
+
import { useAuth } from '../../auth';
|
|
27
|
+
|
|
28
|
+
export interface UserMenuProps {
|
|
29
|
+
variant?: 'desktop' | 'mobile';
|
|
30
|
+
profilePath?: string;
|
|
31
|
+
dashboardPath?: string;
|
|
32
|
+
authPath?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function UserMenu({
|
|
36
|
+
variant = 'desktop',
|
|
37
|
+
profilePath = '/profile',
|
|
38
|
+
dashboardPath = '/dashboard',
|
|
39
|
+
authPath = '/auth',
|
|
40
|
+
}: UserMenuProps) {
|
|
41
|
+
const { user, isAuthenticated, logout } = useAuth();
|
|
42
|
+
const [mounted, setMounted] = React.useState(false);
|
|
43
|
+
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
setMounted(true);
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
if (!mounted) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!isAuthenticated || !user) {
|
|
53
|
+
// Guest user - show sign in button
|
|
54
|
+
if (variant === 'mobile') {
|
|
55
|
+
return (
|
|
56
|
+
<Link href={authPath}>
|
|
57
|
+
<Button variant="default" className="w-full">
|
|
58
|
+
Sign In
|
|
59
|
+
</Button>
|
|
60
|
+
</Link>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return (
|
|
64
|
+
<Link href={authPath}>
|
|
65
|
+
<Button variant="default" size="sm">
|
|
66
|
+
Sign In
|
|
67
|
+
</Button>
|
|
68
|
+
</Link>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Authenticated user
|
|
73
|
+
const displayName = user.display_username || user.email || 'User';
|
|
74
|
+
const userInitial = displayName.charAt(0).toUpperCase();
|
|
75
|
+
const userAvatar = user.avatar || '';
|
|
76
|
+
|
|
77
|
+
if (variant === 'mobile') {
|
|
78
|
+
return (
|
|
79
|
+
<div className="space-y-3">
|
|
80
|
+
<div className="flex items-center gap-3 px-4 py-3">
|
|
81
|
+
<Avatar className="h-10 w-10">
|
|
82
|
+
<AvatarImage src={userAvatar} alt={displayName} />
|
|
83
|
+
<AvatarFallback>{userInitial}</AvatarFallback>
|
|
84
|
+
</Avatar>
|
|
85
|
+
<div className="flex-1 min-w-0">
|
|
86
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
87
|
+
{displayName}
|
|
88
|
+
</p>
|
|
89
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
90
|
+
{user.email}
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<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>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Desktop variant
|
|
117
|
+
return (
|
|
118
|
+
<DropdownMenu>
|
|
119
|
+
<DropdownMenuTrigger asChild>
|
|
120
|
+
<Button variant="ghost" size="icon" className="rounded-full">
|
|
121
|
+
<Avatar className="h-8 w-8">
|
|
122
|
+
<AvatarImage src={userAvatar} alt={displayName} />
|
|
123
|
+
<AvatarFallback>{userInitial}</AvatarFallback>
|
|
124
|
+
</Avatar>
|
|
125
|
+
<span className="sr-only">User menu</span>
|
|
126
|
+
</Button>
|
|
127
|
+
</DropdownMenuTrigger>
|
|
128
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
129
|
+
<DropdownMenuLabel>
|
|
130
|
+
<div className="flex flex-col space-y-1">
|
|
131
|
+
<p className="text-sm font-medium leading-none">{displayName}</p>
|
|
132
|
+
<p className="text-xs leading-none text-muted-foreground">
|
|
133
|
+
{user.email}
|
|
134
|
+
</p>
|
|
135
|
+
</div>
|
|
136
|
+
</DropdownMenuLabel>
|
|
137
|
+
<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>
|
|
156
|
+
</DropdownMenuContent>
|
|
157
|
+
</DropdownMenu>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
package/src/layouts/index.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Layouts exports
|
|
3
|
+
*
|
|
4
|
+
* Simple, straightforward layout components
|
|
5
|
+
* Import and use directly with props - no complex configs needed!
|
|
6
|
+
*/
|
|
4
7
|
|
|
8
|
+
// Smart layout router
|
|
9
|
+
export * from './AppLayout';
|
|
10
|
+
|
|
11
|
+
// Basic layouts
|
|
12
|
+
export * from './PublicLayout';
|
|
13
|
+
export * from './PrivateLayout';
|
|
14
|
+
export * from './AuthLayout';
|
|
15
|
+
export * from './AdminLayout';
|
|
5
16
|
|
|
17
|
+
// Additional layouts
|
|
6
18
|
export * from './ProfileLayout';
|
|
7
19
|
export * from './SupportLayout';
|
|
8
20
|
export * from './PaymentsLayout';
|
|
9
|
-
export * from './AppLayout';
|
|
10
|
-
export * from './ErrorLayout';
|
|
11
|
-
export * from './SimpleLayout';
|
|
12
21
|
|
|
13
|
-
// Note: CfgLayout is now part of AppLayout exports
|
|
14
|
-
// Import it via: import { CfgLayout, useCfgApp } from '@djangocfg/layouts';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* AnalyticsProvider Component
|
|
3
3
|
*
|
|
4
4
|
* Initializes Google Analytics and auto-tracks pageviews.
|
|
5
|
-
*
|
|
5
|
+
* Can work standalone with trackingId prop, or use AppContext if available.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
'use client';
|
|
@@ -12,17 +12,21 @@ import { useAnalytics } from './useAnalytics';
|
|
|
12
12
|
|
|
13
13
|
interface AnalyticsProviderProps {
|
|
14
14
|
children: ReactNode;
|
|
15
|
+
/** Google Analytics tracking ID (optional if using AppContext) */
|
|
16
|
+
trackingId?: string;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Analytics Provider that initializes tracking
|
|
19
21
|
* Automatically:
|
|
20
|
-
* - Initializes GA4 with tracking ID from config
|
|
22
|
+
* - Initializes GA4 with tracking ID from prop or config
|
|
21
23
|
* - Sets user ID when authenticated
|
|
22
24
|
* - Tracks page views on route changes
|
|
23
25
|
*/
|
|
24
|
-
export function AnalyticsProvider({ children }: AnalyticsProviderProps) {
|
|
25
|
-
|
|
26
|
+
export function AnalyticsProvider({ children, trackingId }: AnalyticsProviderProps) {
|
|
27
|
+
// If trackingId is provided as prop, use it directly
|
|
28
|
+
// Otherwise, useAnalytics will try to get it from AppContext
|
|
29
|
+
useAnalytics(trackingId);
|
|
26
30
|
return <>{children}</>;
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
'use client';
|
|
10
10
|
|
|
11
11
|
import { useEffect } from 'react';
|
|
12
|
-
import {
|
|
12
|
+
import { usePathname } from 'next/navigation';
|
|
13
13
|
import ReactGA from 'react-ga4';
|
|
14
|
-
import { useAppContext } from '../../layouts/AppLayout/context';
|
|
15
14
|
import { useAuth } from '../../auth';
|
|
16
15
|
|
|
17
16
|
// Check if we're in production
|
|
@@ -99,12 +98,12 @@ export const Analytics = {
|
|
|
99
98
|
* event('button_click', { category: 'engagement', label: 'signup' });
|
|
100
99
|
* ```
|
|
101
100
|
*/
|
|
102
|
-
export function useAnalytics() {
|
|
103
|
-
const
|
|
104
|
-
const { config } = useAppContext();
|
|
101
|
+
export function useAnalytics(trackingIdProp?: string) {
|
|
102
|
+
const pathname = usePathname();
|
|
105
103
|
const { user, isAuthenticated } = useAuth();
|
|
106
104
|
|
|
107
|
-
|
|
105
|
+
// Use trackingId from prop (passed from AppLayout)
|
|
106
|
+
const trackingId = trackingIdProp;
|
|
108
107
|
const isEnabled = isProduction && Boolean(trackingId);
|
|
109
108
|
|
|
110
109
|
// Initialize GA4
|
|
@@ -119,23 +118,14 @@ export function useAnalytics() {
|
|
|
119
118
|
Analytics.setUser(String(user.id));
|
|
120
119
|
}, [isEnabled, isAuthenticated, user?.id]);
|
|
121
120
|
|
|
122
|
-
// Auto-track page views on route change
|
|
121
|
+
// Auto-track page views on route change (App Router)
|
|
123
122
|
useEffect(() => {
|
|
124
|
-
if (!isEnabled) return;
|
|
123
|
+
if (!isEnabled || typeof window === 'undefined') return;
|
|
125
124
|
|
|
126
|
-
// Track
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const handleRouteChange = (url: string) => {
|
|
131
|
-
Analytics.pageview(url);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
router.events.on('routeChangeComplete', handleRouteChange);
|
|
135
|
-
return () => {
|
|
136
|
-
router.events.off('routeChangeComplete', handleRouteChange);
|
|
137
|
-
};
|
|
138
|
-
}, [router.events, isEnabled]);
|
|
125
|
+
// Track page view when pathname changes
|
|
126
|
+
const url = pathname + (window.location.search || '');
|
|
127
|
+
Analytics.pageview(url);
|
|
128
|
+
}, [pathname, isEnabled]);
|
|
139
129
|
|
|
140
130
|
return {
|
|
141
131
|
isEnabled,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
9
9
|
import { createPortal } from 'react-dom';
|
|
10
|
-
import {
|
|
10
|
+
import { usePathname } from 'next/navigation';
|
|
11
11
|
import { Card, CardContent, CardHeader, Button, useIsMobile, useLocalStorage } from '@djangocfg/ui';
|
|
12
12
|
import {
|
|
13
13
|
MessageSquare,
|
|
@@ -49,7 +49,7 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
49
49
|
toggleTimestamps,
|
|
50
50
|
} = useChatUI();
|
|
51
51
|
|
|
52
|
-
const
|
|
52
|
+
const pathname = usePathname();
|
|
53
53
|
const isMobile = useIsMobile();
|
|
54
54
|
|
|
55
55
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -66,8 +66,8 @@ export const ChatWidget: React.FC<ChatWidgetProps> = ({
|
|
|
66
66
|
[]
|
|
67
67
|
);
|
|
68
68
|
|
|
69
|
-
const isSupport =
|
|
70
|
-
const isContact =
|
|
69
|
+
const isSupport = pathname.startsWith('/private/support');
|
|
70
|
+
const isContact = pathname.startsWith('/private/contact');
|
|
71
71
|
|
|
72
72
|
// Mount portal target
|
|
73
73
|
useEffect(() => {
|
|
@@ -50,31 +50,44 @@ export function ContactFormProvider({ children, apiUrl }: ContactFormProviderPro
|
|
|
50
50
|
const [error, setError] = useState<Error | null>(null);
|
|
51
51
|
const [lastResponse, setLastResponse] = useState<LeadSubmissionResult | null>(null);
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
configureAPI({ baseUrl: apiUrl });
|
|
56
|
-
}, [apiUrl]);
|
|
53
|
+
// Always use local API route to avoid CORS issues
|
|
54
|
+
// Pass apiUrl in request body so route.ts knows where to proxy
|
|
57
55
|
|
|
58
|
-
// Submit function
|
|
56
|
+
// Submit function - always uses local API route
|
|
59
57
|
const submit = useCallback(async (data: LeadSubmissionData): Promise<LeadSubmissionResult> => {
|
|
60
58
|
setIsSubmitting(true);
|
|
61
59
|
setError(null);
|
|
62
60
|
|
|
63
61
|
try {
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
62
|
+
// Always use local Next.js API route to avoid CORS issues
|
|
63
|
+
// Pass apiUrl from ContactPage in the request body so route.ts knows where to proxy
|
|
64
|
+
const res = await fetch('/api/contact', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: {
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
},
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
_apiUrl: apiUrl || '', // Pass apiUrl to route.ts for proxying (can be empty for fallback)
|
|
71
|
+
name: data.name,
|
|
72
|
+
email: data.email,
|
|
73
|
+
message: data.message,
|
|
74
|
+
company: data.company,
|
|
75
|
+
company_site: data.company_site,
|
|
76
|
+
contact_type: data.contact_type,
|
|
77
|
+
contact_value: data.contact_value,
|
|
78
|
+
subject: data.subject,
|
|
79
|
+
extra: data.extra,
|
|
80
|
+
site_url: data.site_url || (typeof window !== 'undefined' ? window.location.origin : undefined),
|
|
81
|
+
}),
|
|
76
82
|
});
|
|
77
83
|
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const errorData = await res.json().catch(() => ({ message: 'Failed to submit contact form' }));
|
|
86
|
+
throw new Error(errorData.message || `HTTP ${res.status}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const response: LeadSubmissionResult = await res.json();
|
|
90
|
+
|
|
78
91
|
const result: LeadSubmissionResult = {
|
|
79
92
|
success: response.success,
|
|
80
93
|
message: response.message,
|
|
@@ -90,7 +103,7 @@ export function ContactFormProvider({ children, apiUrl }: ContactFormProviderPro
|
|
|
90
103
|
} finally {
|
|
91
104
|
setIsSubmitting(false);
|
|
92
105
|
}
|
|
93
|
-
}, []);
|
|
106
|
+
}, [apiUrl]);
|
|
94
107
|
|
|
95
108
|
// Reset error state
|
|
96
109
|
const resetError = useCallback(() => {
|
|
@@ -98,7 +111,7 @@ export function ContactFormProvider({ children, apiUrl }: ContactFormProviderPro
|
|
|
98
111
|
}, []);
|
|
99
112
|
|
|
100
113
|
const value: ContactFormContextValue = {
|
|
101
|
-
apiUrl,
|
|
114
|
+
apiUrl: '/api/contact', // Always use local endpoint
|
|
102
115
|
submit,
|
|
103
116
|
isSubmitting,
|
|
104
117
|
error,
|
|
@@ -10,10 +10,8 @@ import type { ContactDetail, LeadSubmissionResult } from './types';
|
|
|
10
10
|
// Config
|
|
11
11
|
// ============================================================================
|
|
12
12
|
|
|
13
|
-
const isDev = process.env.NODE_ENV === 'development';
|
|
14
|
-
|
|
15
13
|
const DEFAULT_CONFIG = {
|
|
16
|
-
apiUrl:
|
|
14
|
+
apiUrl: 'https://api.reforms.ai',
|
|
17
15
|
email: 'markolofsen@gmail.com',
|
|
18
16
|
whatsapp: '+62 813 39646301',
|
|
19
17
|
calendly: 'https://calendly.com/markolofsen/meeting',
|
|
@@ -24,7 +22,7 @@ const DEFAULT_CONFIG = {
|
|
|
24
22
|
// ============================================================================
|
|
25
23
|
|
|
26
24
|
export interface ContactPageProps {
|
|
27
|
-
/** Override API URL */
|
|
25
|
+
/** Override API URL (defaults to DEFAULT_CONFIG.apiUrl) */
|
|
28
26
|
apiUrl?: string;
|
|
29
27
|
/** Override email */
|
|
30
28
|
email?: string;
|
|
@@ -19,9 +19,10 @@ export interface ContactFormProviderProps {
|
|
|
19
19
|
children: ReactNode;
|
|
20
20
|
/**
|
|
21
21
|
* API base URL for lead submission
|
|
22
|
-
*
|
|
22
|
+
* If not provided or empty, will use local Next.js API route (/api/contact)
|
|
23
|
+
* @example "https://api.example.com" or undefined to use /api/contact
|
|
23
24
|
*/
|
|
24
|
-
apiUrl
|
|
25
|
+
apiUrl?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
// ============================================================================
|
package/src/snippets/index.ts
CHANGED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
# AppLayout - Unified Application Layout System
|
|
2
|
-
|
|
3
|
-
Умный самодостаточный компонент для управления всеми layout'ами приложения.
|
|
4
|
-
|
|
5
|
-
## 📁 Структура
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
AppLayout/
|
|
9
|
-
├── types/ # Все типы
|
|
10
|
-
│ ├── config.ts # AppLayoutConfig
|
|
11
|
-
│ ├── layout.ts # PublicLayoutConfig, PrivateLayoutConfig
|
|
12
|
-
│ ├── navigation.ts # NavigationItem, DashboardMenuItem
|
|
13
|
-
│ ├── routes.ts # RouteConfig, RouteDetectors
|
|
14
|
-
│ └── index.ts
|
|
15
|
-
│
|
|
16
|
-
├── context/ # Unified App Context
|
|
17
|
-
│ ├── AppContext.tsx # Главный контекст приложения
|
|
18
|
-
│ └── index.ts
|
|
19
|
-
│
|
|
20
|
-
├── hooks/ # Custom hooks
|
|
21
|
-
│ ├── useLayoutMode.ts # Определение текущего режима
|
|
22
|
-
│ ├── useNavigation.ts # Навигационные хуки
|
|
23
|
-
│ └── index.ts
|
|
24
|
-
│
|
|
25
|
-
├── providers/ # Provider components
|
|
26
|
-
│ ├── ThemeProvider.tsx # Тема
|
|
27
|
-
│ ├── AuthProvider.tsx # Аутентификация
|
|
28
|
-
│ └── index.ts
|
|
29
|
-
│
|
|
30
|
-
├── layouts/ # Layout renderers
|
|
31
|
-
│ ├── PublicLayout/ # Публичный layout
|
|
32
|
-
│ ├── PrivateLayout/ # Приватный layout (Dashboard)
|
|
33
|
-
│ ├── AuthLayout/ # Auth layout (минимальный)
|
|
34
|
-
│ └── index.ts
|
|
35
|
-
│
|
|
36
|
-
├── components/ # UI components
|
|
37
|
-
│ ├── Seo.tsx # SEO meta tags
|
|
38
|
-
│ ├── PageProgress.tsx # Loading bar
|
|
39
|
-
│ └── index.ts
|
|
40
|
-
│
|
|
41
|
-
├── utils/ # Utilities
|
|
42
|
-
│ ├── routeDetection.ts # Определение типа маршрута
|
|
43
|
-
│ └── index.ts
|
|
44
|
-
│
|
|
45
|
-
├── AppLayout.tsx # Главный компонент
|
|
46
|
-
├── index.ts # Public exports
|
|
47
|
-
└── README.md # Документация
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## 🎯 Основная идея
|
|
51
|
-
|
|
52
|
-
**Единая точка входа** - один компонент `<AppLayout>` управляет всем:
|
|
53
|
-
- Автоматически определяет тип страницы (public/private/auth)
|
|
54
|
-
- Применяет нужный layout
|
|
55
|
-
- Управляет состоянием через единый контекст
|
|
56
|
-
- Предоставляет хуки для доступа к функционалу
|
|
57
|
-
|
|
58
|
-
## 🚀 Использование
|
|
59
|
-
|
|
60
|
-
### В _app.tsx (единственное место подключения)
|
|
61
|
-
|
|
62
|
-
```tsx
|
|
63
|
-
import { AppLayout } from '@djangocfg/layouts';
|
|
64
|
-
import { appLayoutConfig } from '@/core/appLayoutConfig';
|
|
65
|
-
|
|
66
|
-
export default function App({ Component, pageProps }: AppProps) {
|
|
67
|
-
return (
|
|
68
|
-
<AppLayout config={appLayoutConfig}>
|
|
69
|
-
<Component {...pageProps} />
|
|
70
|
-
</AppLayout>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### Конфигурация
|
|
76
|
-
|
|
77
|
-
```tsx
|
|
78
|
-
// core/appLayoutConfig.ts
|
|
79
|
-
import type { AppLayoutConfig } from '@djangocfg/layouts';
|
|
80
|
-
|
|
81
|
-
export const appLayoutConfig: AppLayoutConfig = {
|
|
82
|
-
app: {
|
|
83
|
-
name: 'My App',
|
|
84
|
-
logoPath: '/logo.svg',
|
|
85
|
-
},
|
|
86
|
-
api: {
|
|
87
|
-
baseUrl: process.env.NEXT_PUBLIC_API_URL,
|
|
88
|
-
},
|
|
89
|
-
routes: {
|
|
90
|
-
auth: '/auth',
|
|
91
|
-
defaultCallback: '/dashboard',
|
|
92
|
-
detectors: {
|
|
93
|
-
isPublicRoute: (path) => !path.startsWith('/private'),
|
|
94
|
-
isPrivateRoute: (path) => path.startsWith('/private'),
|
|
95
|
-
isAuthRoute: (path) => path.startsWith('/auth'),
|
|
96
|
-
getUnauthenticatedRedirect: (path) =>
|
|
97
|
-
path.startsWith('/private') ? '/auth' : null,
|
|
98
|
-
getPageTitle: (path) => 'My App',
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
publicLayout: {
|
|
102
|
-
navigation: { /* ... */ },
|
|
103
|
-
userMenu: { /* ... */ },
|
|
104
|
-
footer: { /* ... */ },
|
|
105
|
-
},
|
|
106
|
-
privateLayout: {
|
|
107
|
-
menuGroups: [ /* ... */ ],
|
|
108
|
-
showChat: true,
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## 🎨 Использование в компонентах
|
|
114
|
-
|
|
115
|
-
### Доступ к контексту
|
|
116
|
-
|
|
117
|
-
```tsx
|
|
118
|
-
import { useAppContext } from '@djangocfg/layouts';
|
|
119
|
-
|
|
120
|
-
function MyComponent() {
|
|
121
|
-
const {
|
|
122
|
-
config,
|
|
123
|
-
layoutMode,
|
|
124
|
-
mobileMenuOpen,
|
|
125
|
-
toggleMobileMenu
|
|
126
|
-
} = useAppContext();
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<button onClick={toggleMobileMenu}>
|
|
130
|
-
{layoutMode === 'private' ? 'Dashboard' : 'Home'}
|
|
131
|
-
</button>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Использование хуков
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
import { useLayoutMode, useNavigation } from '@djangocfg/layouts';
|
|
140
|
-
|
|
141
|
-
function MyComponent() {
|
|
142
|
-
const mode = useLayoutMode(); // 'public' | 'private' | 'auth'
|
|
143
|
-
const { isActive } = useNavigation();
|
|
144
|
-
|
|
145
|
-
if (mode === 'private') {
|
|
146
|
-
return <DashboardView />;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return <PublicView />;
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## 🏗️ Архитектурные принципы
|
|
154
|
-
|
|
155
|
-
### 1. Single Source of Truth
|
|
156
|
-
Вся конфигурация в одном месте - `AppLayoutConfig`
|
|
157
|
-
|
|
158
|
-
### 2. Context над Props
|
|
159
|
-
Никакого prop drilling - всё через `useAppContext()`
|
|
160
|
-
|
|
161
|
-
### 3. Декомпозиция
|
|
162
|
-
Каждая папка отвечает за свою область:
|
|
163
|
-
- `types/` - только типы
|
|
164
|
-
- `context/` - состояние и контекст
|
|
165
|
-
- `hooks/` - переиспользуемая логика
|
|
166
|
-
- `layouts/` - рендеринг layout'ов
|
|
167
|
-
- `components/` - UI компоненты
|
|
168
|
-
|
|
169
|
-
### 4. Автоматизация
|
|
170
|
-
Layout определяется автоматически на основе маршрута
|
|
171
|
-
|
|
172
|
-
### 5. Расширяемость
|
|
173
|
-
Легко добавить новый layout или функционал
|
|
174
|
-
|
|
175
|
-
## 📦 Экспорты
|
|
176
|
-
|
|
177
|
-
```tsx
|
|
178
|
-
// Главный компонент
|
|
179
|
-
export { AppLayout } from './AppLayout';
|
|
180
|
-
|
|
181
|
-
// Контекст и хуки
|
|
182
|
-
export { useAppContext } from './context';
|
|
183
|
-
export { useLayoutMode, useNavigation } from './hooks';
|
|
184
|
-
|
|
185
|
-
// Типы
|
|
186
|
-
export type {
|
|
187
|
-
AppLayoutConfig,
|
|
188
|
-
PublicLayoutConfig,
|
|
189
|
-
PrivateLayoutConfig,
|
|
190
|
-
RouteConfig,
|
|
191
|
-
NavigationItem,
|
|
192
|
-
DashboardMenuItem,
|
|
193
|
-
} from './types';
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
## 🎯 Преимущества
|
|
197
|
-
|
|
198
|
-
✅ **Одно место подключения** - только в `_app.tsx`
|
|
199
|
-
✅ **Нет prop drilling** - всё через контекст
|
|
200
|
-
✅ **Автоматический роутинг** - layout определяется сам
|
|
201
|
-
✅ **Типобезопасность** - TypeScript везде
|
|
202
|
-
✅ **Легкая настройка** - один конфиг объект
|
|
203
|
-
✅ **Декомпозиция** - чистая структура папок
|
|
204
|
-
✅ **Переиспользование** - хуки для всего
|