@djangocfg/layouts 2.0.7 → 2.0.9

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 (29) hide show
  1. package/README.md +65 -6
  2. package/package.json +5 -5
  3. package/src/auth/context/AuthContext.tsx +11 -6
  4. package/src/auth/hooks/index.ts +1 -0
  5. package/src/auth/hooks/useAuthGuard.ts +2 -2
  6. package/src/auth/hooks/useAutoAuth.ts +2 -2
  7. package/src/auth/hooks/useGithubAuth.ts +184 -0
  8. package/src/components/RedirectPage/RedirectPage.tsx +2 -2
  9. package/src/layouts/AuthLayout/AuthContext.tsx +2 -0
  10. package/src/layouts/AuthLayout/AuthLayout.tsx +22 -5
  11. package/src/layouts/AuthLayout/IdentifierForm.tsx +4 -0
  12. package/src/layouts/AuthLayout/OAuthCallback.tsx +172 -0
  13. package/src/layouts/AuthLayout/OAuthProviders.tsx +85 -0
  14. package/src/layouts/AuthLayout/index.ts +4 -0
  15. package/src/layouts/AuthLayout/types.ts +4 -0
  16. package/src/snippets/Analytics/events.ts +5 -0
  17. package/src/snippets/AuthDialog/AuthDialog.tsx +2 -2
  18. package/src/snippets/McpChat/components/AIChatWidget.tsx +3 -39
  19. package/src/snippets/McpChat/components/ChatMessages.tsx +2 -2
  20. package/src/snippets/McpChat/components/ChatPanel.tsx +84 -110
  21. package/src/snippets/McpChat/components/ChatSidebar.tsx +66 -60
  22. package/src/snippets/McpChat/components/ChatWidget.tsx +4 -37
  23. package/src/snippets/McpChat/components/MessageBubble.tsx +5 -5
  24. package/src/snippets/McpChat/components/index.ts +0 -2
  25. package/src/snippets/McpChat/config.ts +42 -0
  26. package/src/snippets/McpChat/context/ChatContext.tsx +5 -7
  27. package/src/snippets/McpChat/hooks/useChatLayout.ts +134 -23
  28. package/src/snippets/McpChat/index.ts +0 -1
  29. package/src/snippets/index.ts +0 -1
@@ -0,0 +1,172 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import { Loader2, AlertCircle, CheckCircle } from 'lucide-react';
6
+
7
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@djangocfg/ui/components';
8
+
9
+ import { useGithubAuth } from '../../auth/hooks';
10
+
11
+ export interface OAuthCallbackProps {
12
+ onSuccess?: (user: any, isNewUser: boolean, provider: string) => void;
13
+ onError?: (error: string) => void;
14
+ redirectUrl?: string;
15
+ }
16
+
17
+ type CallbackStatus = 'processing' | 'success' | 'error';
18
+
19
+ /**
20
+ * OAuth Callback Handler Component
21
+ *
22
+ * Processes OAuth callback from providers (GitHub, etc.).
23
+ * Reads code, state, and provider from URL params and exchanges for tokens.
24
+ *
25
+ * Usage:
26
+ * Place this component on your /auth page to handle OAuth callbacks.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * // app/auth/page.tsx
31
+ * import { OAuthCallback, AuthLayout } from '@djangocfg/layouts';
32
+ *
33
+ * export default function AuthPage() {
34
+ * return (
35
+ * <>
36
+ * <OAuthCallback
37
+ * onSuccess={(user) => console.log('OAuth success:', user)}
38
+ * onError={(error) => console.error('OAuth error:', error)}
39
+ * />
40
+ * <AuthLayout enableGithubAuth>
41
+ * {/* Your auth content *\/}
42
+ * </AuthLayout>
43
+ * </>
44
+ * );
45
+ * }
46
+ * ```
47
+ */
48
+ export const OAuthCallback: React.FC<OAuthCallbackProps> = ({
49
+ onSuccess,
50
+ onError,
51
+ redirectUrl,
52
+ }) => {
53
+ const searchParams = useSearchParams();
54
+ const [status, setStatus] = useState<CallbackStatus | null>(null);
55
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
56
+
57
+ const provider = searchParams.get('provider');
58
+ const code = searchParams.get('code');
59
+ const state = searchParams.get('state');
60
+ const error = searchParams.get('error');
61
+ const errorDescription = searchParams.get('error_description');
62
+
63
+ const {
64
+ handleGithubCallback,
65
+ isLoading,
66
+ error: githubError,
67
+ } = useGithubAuth({
68
+ onSuccess: (user, isNewUser) => {
69
+ setStatus('success');
70
+ onSuccess?.(user, isNewUser, 'github');
71
+ },
72
+ onError: (err) => {
73
+ setStatus('error');
74
+ setErrorMessage(err);
75
+ onError?.(err);
76
+ },
77
+ redirectUrl,
78
+ });
79
+
80
+ // Process OAuth callback on mount
81
+ useEffect(() => {
82
+ // Check if this is an OAuth callback
83
+ if (!provider || !code || !state) {
84
+ // Not an OAuth callback, don't show anything
85
+ return;
86
+ }
87
+
88
+ // Check for error from provider
89
+ if (error) {
90
+ setStatus('error');
91
+ setErrorMessage(errorDescription || error);
92
+ onError?.(errorDescription || error);
93
+ return;
94
+ }
95
+
96
+ // Process based on provider
97
+ const processCallback = async () => {
98
+ setStatus('processing');
99
+
100
+ if (provider === 'github') {
101
+ await handleGithubCallback(code, state);
102
+ } else {
103
+ setStatus('error');
104
+ setErrorMessage(`Unsupported OAuth provider: ${provider}`);
105
+ onError?.(`Unsupported OAuth provider: ${provider}`);
106
+ }
107
+ };
108
+
109
+ processCallback();
110
+ // eslint-disable-next-line react-hooks/exhaustive-deps
111
+ }, [provider, code, state, error]);
112
+
113
+ // Don't render if not an OAuth callback
114
+ if (!provider || (!code && !error)) {
115
+ return null;
116
+ }
117
+
118
+ return (
119
+ <div className="fixed inset-0 bg-background/80 backdrop-blur-sm z-50 flex items-center justify-center">
120
+ <Card className="w-full max-w-md mx-4 shadow-lg">
121
+ <CardHeader className="text-center">
122
+ {status === 'processing' && (
123
+ <>
124
+ <div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4">
125
+ <Loader2 className="w-6 h-6 text-primary animate-spin" />
126
+ </div>
127
+ <CardTitle>Signing you in...</CardTitle>
128
+ <CardDescription>
129
+ Please wait while we complete your {provider} authentication.
130
+ </CardDescription>
131
+ </>
132
+ )}
133
+
134
+ {status === 'success' && (
135
+ <>
136
+ <div className="mx-auto w-12 h-12 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mb-4">
137
+ <CheckCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
138
+ </div>
139
+ <CardTitle>Success!</CardTitle>
140
+ <CardDescription>
141
+ You have been signed in successfully. Redirecting...
142
+ </CardDescription>
143
+ </>
144
+ )}
145
+
146
+ {status === 'error' && (
147
+ <>
148
+ <div className="mx-auto w-12 h-12 bg-destructive/10 rounded-full flex items-center justify-center mb-4">
149
+ <AlertCircle className="w-6 h-6 text-destructive" />
150
+ </div>
151
+ <CardTitle>Authentication Failed</CardTitle>
152
+ <CardDescription className="text-destructive">
153
+ {errorMessage || githubError || 'An error occurred during authentication.'}
154
+ </CardDescription>
155
+ </>
156
+ )}
157
+ </CardHeader>
158
+
159
+ {status === 'error' && (
160
+ <CardContent className="text-center">
161
+ <a
162
+ href="/auth"
163
+ className="text-primary hover:underline text-sm"
164
+ >
165
+ Try again
166
+ </a>
167
+ </CardContent>
168
+ )}
169
+ </Card>
170
+ </div>
171
+ );
172
+ };
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Github, Loader2 } from 'lucide-react';
5
+
6
+ import { Button } from '@djangocfg/ui/components';
7
+
8
+ import { useGithubAuth } from '../../auth/hooks';
9
+ import { useAuthContext } from './AuthContext';
10
+
11
+ /**
12
+ * OAuth Providers Component
13
+ *
14
+ * Shows OAuth login buttons (GitHub, etc.) when enabled.
15
+ * Handles the OAuth flow initiation.
16
+ */
17
+ export const OAuthProviders: React.FC = () => {
18
+ const { enableGithubAuth, sourceUrl, error: contextError, setError } = useAuthContext();
19
+
20
+ const {
21
+ isLoading: isGithubLoading,
22
+ error: githubError,
23
+ startGithubAuth,
24
+ } = useGithubAuth({
25
+ sourceUrl,
26
+ onError: (error) => {
27
+ setError(error);
28
+ },
29
+ });
30
+
31
+ // Don't render if no OAuth providers are enabled
32
+ if (!enableGithubAuth) {
33
+ return null;
34
+ }
35
+
36
+ const error = githubError || contextError;
37
+
38
+ return (
39
+ <div className="space-y-4">
40
+ {/* Divider */}
41
+ <div className="relative">
42
+ <div className="absolute inset-0 flex items-center">
43
+ <div className="w-full border-t border-border" />
44
+ </div>
45
+ <div className="relative flex justify-center text-xs uppercase">
46
+ <span className="bg-card px-2 text-muted-foreground">
47
+ Or continue with
48
+ </span>
49
+ </div>
50
+ </div>
51
+
52
+ {/* OAuth Buttons */}
53
+ <div className="grid gap-3">
54
+ {enableGithubAuth && (
55
+ <Button
56
+ type="button"
57
+ variant="outline"
58
+ className="w-full h-11 text-base font-medium"
59
+ onClick={startGithubAuth}
60
+ disabled={isGithubLoading}
61
+ >
62
+ {isGithubLoading ? (
63
+ <div className="flex items-center gap-2">
64
+ <Loader2 className="w-5 h-5 animate-spin" />
65
+ Connecting...
66
+ </div>
67
+ ) : (
68
+ <div className="flex items-center gap-2">
69
+ <Github className="w-5 h-5" />
70
+ Continue with GitHub
71
+ </div>
72
+ )}
73
+ </Button>
74
+ )}
75
+ </div>
76
+
77
+ {/* Error Message */}
78
+ {error && (
79
+ <div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
80
+ {error}
81
+ </div>
82
+ )}
83
+ </div>
84
+ );
85
+ };
@@ -12,6 +12,10 @@ export { AuthProvider as AuthLayoutProvider, useAuthContext } from './AuthContex
12
12
  export { IdentifierForm } from './IdentifierForm';
13
13
  export { OTPForm } from './OTPForm';
14
14
 
15
+ // OAuth
16
+ export { OAuthProviders } from './OAuthProviders';
17
+ export { OAuthCallback, type OAuthCallbackProps } from './OAuthCallback';
18
+
15
19
  // Help component
16
20
  export { AuthHelp } from './AuthHelp';
17
21
 
@@ -19,6 +19,7 @@ export interface AuthContextType {
19
19
  privacyUrl?: string;
20
20
  sourceUrl: string;
21
21
  enablePhoneAuth?: boolean;
22
+ enableGithubAuth?: boolean;
22
23
 
23
24
  // Form handlers
24
25
  setIdentifier: (identifier: string) => void;
@@ -49,8 +50,11 @@ export interface AuthProps {
49
50
  privacyUrl?: string;
50
51
  className?: string;
51
52
  enablePhoneAuth?: boolean; // Controls whether phone authentication is available
53
+ enableGithubAuth?: boolean; // Controls whether GitHub OAuth is available
54
+ redirectUrl?: string; // URL to redirect after successful auth (default: /dashboard)
52
55
  onIdentifierSuccess?: (identifier: string, channel: 'email' | 'phone') => void;
53
56
  onOTPSuccess?: () => void;
57
+ onOAuthSuccess?: (user: any, isNewUser: boolean, provider: string) => void;
54
58
  onError?: (message: string) => void;
55
59
  }
56
60
 
@@ -30,6 +30,11 @@ export const AnalyticsEvent = {
30
30
  AUTH_TOKEN_REFRESH: 'auth_token_refresh',
31
31
  AUTH_TOKEN_REFRESH_FAIL: 'auth_token_refresh_fail',
32
32
 
33
+ // OAuth Events
34
+ AUTH_OAUTH_START: 'auth_oauth_start',
35
+ AUTH_OAUTH_SUCCESS: 'auth_oauth_success',
36
+ AUTH_OAUTH_FAIL: 'auth_oauth_fail',
37
+
33
38
  // Error Events
34
39
  ERROR_BOUNDARY: 'error_boundary',
35
40
  ERROR_API: 'error_api',
@@ -10,7 +10,7 @@ import {
10
10
  DialogHeader,
11
11
  DialogTitle,
12
12
  } from '@djangocfg/ui/components';
13
- import { useEventListener, useRouter } from '@djangocfg/ui/hooks';
13
+ import { useEventListener, useCfgRouter } from '@djangocfg/ui/hooks';
14
14
 
15
15
  // Re-export events for backwards compatibility
16
16
  export const DIALOG_EVENTS = {
@@ -31,7 +31,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
31
31
  }) => {
32
32
  const [open, setOpen] = useState(false);
33
33
  const [message, setMessage] = useState<string>('Please sign in to continue');
34
- const router = useRouter();
34
+ const router = useCfgRouter();
35
35
 
36
36
  // Listen for open auth dialog event
37
37
  useEventListener(DIALOG_EVENTS.OPEN_AUTH_DIALOG, (payload: any) => {
@@ -66,20 +66,7 @@ export interface AIChatWidgetProps extends ChatWidgetConfig {
66
66
  * Internal AI chat widget that uses context
67
67
  */
68
68
  const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className }) => {
69
- const {
70
- messages,
71
- isLoading,
72
- isMinimized,
73
- config,
74
- displayMode,
75
- isMobile,
76
- sendMessage,
77
- openChat,
78
- closeChat,
79
- toggleMinimize,
80
- setDisplayMode,
81
- stopStreaming,
82
- } = useAIChatContext();
69
+ const { config, displayMode, openChat } = useAIChatContext();
83
70
 
84
71
  // Use layout hook for consistent positioning
85
72
  const { getFabStyles, getFloatingStyles } = useChatLayout();
@@ -173,17 +160,7 @@ const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className })
173
160
  if (displayMode === 'sidebar') {
174
161
  return (
175
162
  <Portal>
176
- <ChatSidebar
177
- messages={messages}
178
- isLoading={isLoading}
179
- onSendMessage={sendMessage}
180
- onClose={closeChat}
181
- onModeChange={setDisplayMode}
182
- onStopStreaming={stopStreaming}
183
- title={config.title}
184
- placeholder={config.placeholder}
185
- greeting={config.greeting}
186
- />
163
+ <ChatSidebar />
187
164
  </Portal>
188
165
  );
189
166
  }
@@ -192,20 +169,7 @@ const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className })
192
169
  return (
193
170
  <Portal>
194
171
  <div style={floatingStyles} className={className || ''}>
195
- <ChatPanel
196
- messages={messages}
197
- isLoading={isLoading}
198
- onSendMessage={sendMessage}
199
- onClose={closeChat}
200
- onMinimize={toggleMinimize}
201
- onModeChange={setDisplayMode}
202
- onStopStreaming={stopStreaming}
203
- isMinimized={isMinimized}
204
- isMobile={isMobile}
205
- title={config.title}
206
- placeholder={config.placeholder}
207
- greeting={config.greeting}
208
- />
172
+ <ChatPanel />
209
173
  </div>
210
174
  </Portal>
211
175
  );
@@ -93,8 +93,8 @@ export const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(
93
93
  const padding = largeGreetingIcon ? 'py-12' : 'py-8';
94
94
 
95
95
  return (
96
- <ScrollArea ref={scrollAreaRef} className="h-full">
97
- <div className={`${isCompact ? 'p-3' : 'p-4'} space-y-4`}>
96
+ <ScrollArea ref={scrollAreaRef} className="h-full w-full">
97
+ <div className={`${isCompact ? 'p-3' : 'p-4'} space-y-4 max-w-full overflow-x-hidden`}>
98
98
  {/* Greeting */}
99
99
  {messages.length === 0 && greeting && (
100
100
  <div className={`text-center ${padding}`}>
@@ -2,125 +2,99 @@
2
2
 
3
3
  import React from 'react';
4
4
  import { Card, CardHeader, CardContent, CardFooter, Button } from '@djangocfg/ui';
5
- import { X, Minimize2, PanelRight, Bot } from 'lucide-react';
6
- import type { ChatDisplayMode, AIChatMessage } from '../types';
5
+ import { X, PanelRight, Bot, RotateCcw } from 'lucide-react';
7
6
  import { ChatMessages } from './ChatMessages';
8
7
  import { AIMessageInput } from './MessageInput';
8
+ import { useAIChatContext } from '../context/AIChatContext';
9
9
 
10
- export interface ChatPanelProps {
11
- messages: AIChatMessage[];
12
- isLoading: boolean;
13
- onSendMessage: (content: string) => void;
14
- onClose?: () => void;
15
- onMinimize?: () => void;
16
- onModeChange?: (mode: ChatDisplayMode) => void;
17
- onStopStreaming?: () => void;
18
- isMinimized?: boolean;
19
- isMobile?: boolean;
20
- title?: string;
21
- placeholder?: string;
22
- greeting?: string;
23
- }
24
-
25
- export const ChatPanel = React.memo<ChatPanelProps>(
26
- ({
10
+ export const ChatPanel = React.memo(() => {
11
+ const {
27
12
  messages,
28
13
  isLoading,
29
- onSendMessage,
30
- onClose,
31
- onMinimize,
32
- onModeChange,
33
- onStopStreaming,
34
- isMinimized = false,
35
- isMobile = false,
36
- title = 'DjangoCFG Assistant',
37
- placeholder,
38
- greeting,
39
- }) => {
40
- if (isMinimized) {
41
- return (
42
- <Button
43
- onClick={onMinimize}
44
- className="rounded-full shadow-lg"
45
- style={{ width: '56px', height: '56px' }}
46
- >
47
- <Bot className="h-6 w-6" />
48
- </Button>
49
- );
50
- }
14
+ config,
15
+ isMobile,
16
+ sendMessage,
17
+ closeChat,
18
+ setDisplayMode,
19
+ stopStreaming,
20
+ clearMessages,
21
+ } = useAIChatContext();
51
22
 
52
- return (
53
- <Card
54
- className="flex flex-col shadow-2xl border-border/50"
55
- style={{
56
- width: '380px',
57
- height: '520px',
58
- maxHeight: 'calc(100vh - 100px)',
59
- }}
60
- >
61
- {/* Header */}
62
- <CardHeader className="flex flex-row items-center justify-between p-3 border-b">
63
- <div className="flex items-center gap-2">
64
- <div
65
- className="rounded-full bg-primary/10 flex items-center justify-center"
66
- style={{ width: '32px', height: '32px' }}
67
- >
68
- <Bot className="h-4 w-4 text-primary" />
69
- </div>
70
- <div>
71
- <h3 className="font-semibold text-sm">{title}</h3>
72
- <p className="text-xs text-muted-foreground">AI-powered documentation assistant</p>
73
- </div>
23
+ return (
24
+ <Card
25
+ className="flex flex-col shadow-2xl border-border/50"
26
+ style={{
27
+ width: '380px',
28
+ height: '520px',
29
+ maxHeight: 'calc(100vh - 100px)',
30
+ }}
31
+ >
32
+ {/* Header */}
33
+ <CardHeader className="flex flex-row items-center justify-between p-3 border-b">
34
+ <div className="flex items-center gap-2">
35
+ <div
36
+ className="rounded-full bg-primary/10 flex items-center justify-center"
37
+ style={{ width: '32px', height: '32px' }}
38
+ >
39
+ <Bot className="h-4 w-4 text-primary" />
74
40
  </div>
75
- <div className="flex gap-1">
76
- {/* Sidebar mode button - only on desktop */}
77
- {onModeChange && !isMobile && (
78
- <Button
79
- variant="ghost"
80
- size="icon"
81
- className="h-8 w-8"
82
- onClick={() => onModeChange('sidebar')}
83
- title="Switch to sidebar mode"
84
- >
85
- <PanelRight className="h-4 w-4" />
86
- </Button>
87
- )}
88
- {onMinimize && (
89
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onMinimize} title="Minimize">
90
- <Minimize2 className="h-4 w-4" />
91
- </Button>
92
- )}
93
- {onClose && (
94
- <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onClose} title="Close">
95
- <X className="h-4 w-4" />
96
- </Button>
97
- )}
41
+ <div>
42
+ <h3 className="font-semibold text-sm">{config.title || 'DjangoCFG AI'}</h3>
43
+ <p className="text-xs text-muted-foreground">documentation assistant</p>
98
44
  </div>
99
- </CardHeader>
45
+ </div>
46
+ <div className="flex gap-1">
47
+ {messages.length > 0 && (
48
+ <Button
49
+ variant="ghost"
50
+ size="icon"
51
+ className="h-8 w-8"
52
+ onClick={clearMessages}
53
+ title="New chat"
54
+ >
55
+ <RotateCcw className="h-4 w-4" />
56
+ </Button>
57
+ )}
58
+ {/* Sidebar mode button - only on desktop */}
59
+ {!isMobile && (
60
+ <Button
61
+ variant="ghost"
62
+ size="icon"
63
+ className="h-8 w-8"
64
+ onClick={() => setDisplayMode('sidebar')}
65
+ title="Switch to sidebar mode"
66
+ >
67
+ <PanelRight className="h-4 w-4" />
68
+ </Button>
69
+ )}
70
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title="Close">
71
+ <X className="h-4 w-4" />
72
+ </Button>
73
+ </div>
74
+ </CardHeader>
100
75
 
101
- {/* Messages */}
102
- <CardContent className="flex-1 p-0 overflow-hidden">
103
- <ChatMessages
104
- messages={messages}
105
- isLoading={isLoading}
106
- greeting={greeting}
107
- onStopStreaming={onStopStreaming}
108
- isCompact
109
- greetingIcon="bot"
110
- />
111
- </CardContent>
76
+ {/* Messages */}
77
+ <CardContent className="flex-1 p-0 overflow-hidden">
78
+ <ChatMessages
79
+ messages={messages}
80
+ isLoading={isLoading}
81
+ greeting={config.greeting}
82
+ onStopStreaming={stopStreaming}
83
+ isCompact
84
+ greetingIcon="bot"
85
+ />
86
+ </CardContent>
112
87
 
113
- {/* Input */}
114
- <CardFooter className="p-3 border-t">
115
- <AIMessageInput
116
- onSend={onSendMessage}
117
- isLoading={isLoading}
118
- placeholder={placeholder}
119
- />
120
- </CardFooter>
121
- </Card>
122
- );
123
- }
124
- );
88
+ {/* Input */}
89
+ <CardFooter className="p-3 border-t">
90
+ <AIMessageInput
91
+ onSend={sendMessage}
92
+ isLoading={isLoading}
93
+ placeholder={config.placeholder}
94
+ />
95
+ </CardFooter>
96
+ </Card>
97
+ );
98
+ });
125
99
 
126
100
  ChatPanel.displayName = 'ChatPanel';