@chaaskit/client 0.1.0

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 (135) hide show
  1. package/dist/favicon.svg +11 -0
  2. package/dist/index.html +17 -0
  3. package/dist/lib/LoadingSkeletons-IcIC2JPq.js +132 -0
  4. package/dist/lib/LoadingSkeletons-IcIC2JPq.js.map +1 -0
  5. package/dist/lib/ServerThemeProvider-DNF0LAyk.js +42 -0
  6. package/dist/lib/ServerThemeProvider-DNF0LAyk.js.map +1 -0
  7. package/dist/lib/extensions.js +10 -0
  8. package/dist/lib/extensions.js.map +1 -0
  9. package/dist/lib/favicon.svg +11 -0
  10. package/dist/lib/index.js +74126 -0
  11. package/dist/lib/index.js.map +1 -0
  12. package/dist/lib/logo.svg +12 -0
  13. package/dist/lib/routes/AcceptInviteRoute.js +19 -0
  14. package/dist/lib/routes/AcceptInviteRoute.js.map +1 -0
  15. package/dist/lib/routes/AdminDashboardRoute.js +19 -0
  16. package/dist/lib/routes/AdminDashboardRoute.js.map +1 -0
  17. package/dist/lib/routes/AdminTeamRoute.js +19 -0
  18. package/dist/lib/routes/AdminTeamRoute.js.map +1 -0
  19. package/dist/lib/routes/AdminTeamsRoute.js +19 -0
  20. package/dist/lib/routes/AdminTeamsRoute.js.map +1 -0
  21. package/dist/lib/routes/AdminUsersRoute.js +19 -0
  22. package/dist/lib/routes/AdminUsersRoute.js.map +1 -0
  23. package/dist/lib/routes/ApiKeysRoute.js +19 -0
  24. package/dist/lib/routes/ApiKeysRoute.js.map +1 -0
  25. package/dist/lib/routes/AutomationsRoute.js +19 -0
  26. package/dist/lib/routes/AutomationsRoute.js.map +1 -0
  27. package/dist/lib/routes/ChatRoute.js +19 -0
  28. package/dist/lib/routes/ChatRoute.js.map +1 -0
  29. package/dist/lib/routes/DocumentsRoute.js +19 -0
  30. package/dist/lib/routes/DocumentsRoute.js.map +1 -0
  31. package/dist/lib/routes/OAuthConsentRoute.js +19 -0
  32. package/dist/lib/routes/OAuthConsentRoute.js.map +1 -0
  33. package/dist/lib/routes/PricingRoute.js +19 -0
  34. package/dist/lib/routes/PricingRoute.js.map +1 -0
  35. package/dist/lib/routes/PrivacyRoute.js +19 -0
  36. package/dist/lib/routes/PrivacyRoute.js.map +1 -0
  37. package/dist/lib/routes/TeamSettingsRoute.js +19 -0
  38. package/dist/lib/routes/TeamSettingsRoute.js.map +1 -0
  39. package/dist/lib/routes/TermsRoute.js +19 -0
  40. package/dist/lib/routes/TermsRoute.js.map +1 -0
  41. package/dist/lib/routes/VerifyEmailRoute.js +19 -0
  42. package/dist/lib/routes/VerifyEmailRoute.js.map +1 -0
  43. package/dist/lib/routes.js +79 -0
  44. package/dist/lib/routes.js.map +1 -0
  45. package/dist/lib/ssr-utils.js +29 -0
  46. package/dist/lib/ssr-utils.js.map +1 -0
  47. package/dist/lib/ssr.js +60 -0
  48. package/dist/lib/ssr.js.map +1 -0
  49. package/dist/lib/styles.css +2410 -0
  50. package/dist/lib/useExtensions-B5nX_8XD.js +155 -0
  51. package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -0
  52. package/dist/logo.svg +12 -0
  53. package/package.json +84 -0
  54. package/src/components/AgentSelector.tsx +90 -0
  55. package/src/components/BranchModal.tsx +129 -0
  56. package/src/components/ClientOnly.tsx +27 -0
  57. package/src/components/ExportMenu.tsx +122 -0
  58. package/src/components/LoadingSkeletons.tsx +110 -0
  59. package/src/components/MCPCredentialsSection.tsx +309 -0
  60. package/src/components/MentionChip.tsx +149 -0
  61. package/src/components/MentionDropdown.tsx +175 -0
  62. package/src/components/MentionInput.tsx +293 -0
  63. package/src/components/MessageItem.tsx +300 -0
  64. package/src/components/MessageList.tsx +159 -0
  65. package/src/components/OAuthAppsSection.tsx +124 -0
  66. package/src/components/ProjectFolder.tsx +141 -0
  67. package/src/components/ProjectModal.tsx +296 -0
  68. package/src/components/SSRMessageList.tsx +153 -0
  69. package/src/components/SearchModal.tsx +173 -0
  70. package/src/components/SettingsModal.tsx +412 -0
  71. package/src/components/ShareModal.tsx +280 -0
  72. package/src/components/Sidebar.tsx +491 -0
  73. package/src/components/TeamSwitcher.tsx +273 -0
  74. package/src/components/ToolCallDisplay.tsx +473 -0
  75. package/src/components/ToolConfirmationModal.tsx +130 -0
  76. package/src/components/UsageChart.tsx +177 -0
  77. package/src/components/content/CodeBlock.tsx +69 -0
  78. package/src/components/content/MarkdownRenderer.tsx +64 -0
  79. package/src/components/content/SSRMarkdownRenderer.tsx +158 -0
  80. package/src/contexts/AuthContext.tsx +119 -0
  81. package/src/contexts/ConfigContext.tsx +214 -0
  82. package/src/contexts/ProjectContext.tsx +167 -0
  83. package/src/contexts/ServerConfigProvider.tsx +41 -0
  84. package/src/contexts/ServerThemeProvider.tsx +47 -0
  85. package/src/contexts/TeamContext.tsx +255 -0
  86. package/src/contexts/ThemeContext.tsx +113 -0
  87. package/src/extensions/index.ts +15 -0
  88. package/src/extensions/registry.ts +187 -0
  89. package/src/extensions/useExtensions.ts +52 -0
  90. package/src/hooks/useAppPath.ts +34 -0
  91. package/src/hooks/useBasePath.ts +13 -0
  92. package/src/hooks/useKeyboardShortcuts.ts +50 -0
  93. package/src/hooks/useMentionSearch.ts +106 -0
  94. package/src/index.tsx +116 -0
  95. package/src/layouts/MainLayout.tsx +98 -0
  96. package/src/pages/AcceptInvitePage.tsx +175 -0
  97. package/src/pages/AdminDashboardPage.tsx +362 -0
  98. package/src/pages/AdminTeamPage.tsx +304 -0
  99. package/src/pages/AdminTeamsPage.tsx +242 -0
  100. package/src/pages/AdminUsersPage.tsx +385 -0
  101. package/src/pages/ApiKeysPage.tsx +449 -0
  102. package/src/pages/ChatPage.tsx +310 -0
  103. package/src/pages/DocumentsPage.tsx +577 -0
  104. package/src/pages/LoginPage.tsx +232 -0
  105. package/src/pages/OAuthConsentPage.tsx +234 -0
  106. package/src/pages/PricingPage.tsx +314 -0
  107. package/src/pages/PrivacyPage.tsx +65 -0
  108. package/src/pages/RegisterPage.tsx +153 -0
  109. package/src/pages/ScheduledPromptsPage.tsx +702 -0
  110. package/src/pages/SharedThreadPage.tsx +116 -0
  111. package/src/pages/TeamSettingsPage.tsx +1085 -0
  112. package/src/pages/TermsPage.tsx +82 -0
  113. package/src/pages/VerifyEmailPage.tsx +202 -0
  114. package/src/routes/AcceptInviteRoute.tsx +24 -0
  115. package/src/routes/AdminDashboardRoute.tsx +24 -0
  116. package/src/routes/AdminTeamRoute.tsx +24 -0
  117. package/src/routes/AdminTeamsRoute.tsx +24 -0
  118. package/src/routes/AdminUsersRoute.tsx +24 -0
  119. package/src/routes/ApiKeysRoute.tsx +24 -0
  120. package/src/routes/AutomationsRoute.tsx +24 -0
  121. package/src/routes/ChatRoute.tsx +28 -0
  122. package/src/routes/DocumentsRoute.tsx +24 -0
  123. package/src/routes/OAuthConsentRoute.tsx +24 -0
  124. package/src/routes/PricingRoute.tsx +24 -0
  125. package/src/routes/PrivacyRoute.tsx +24 -0
  126. package/src/routes/TeamSettingsRoute.tsx +24 -0
  127. package/src/routes/TermsRoute.tsx +24 -0
  128. package/src/routes/VerifyEmailRoute.tsx +24 -0
  129. package/src/routes/index.ts +57 -0
  130. package/src/ssr-utils.tsx +84 -0
  131. package/src/ssr.ts +123 -0
  132. package/src/stores/chatStore.ts +670 -0
  133. package/src/styles/index.css +254 -0
  134. package/src/utils/api.ts +78 -0
  135. package/src/vite-env.d.ts +13 -0
@@ -0,0 +1,50 @@
1
+ import { useEffect, useCallback } from 'react';
2
+
3
+ interface ShortcutHandlers {
4
+ onSearch?: () => void;
5
+ onNewThread?: () => void;
6
+ onEscape?: () => void;
7
+ }
8
+
9
+ export function useKeyboardShortcuts(handlers: ShortcutHandlers) {
10
+ const handleKeyDown = useCallback(
11
+ (e: KeyboardEvent) => {
12
+ const isMod = e.metaKey || e.ctrlKey;
13
+
14
+ // Cmd/Ctrl + K - Open search
15
+ if (isMod && e.key === 'k') {
16
+ e.preventDefault();
17
+ handlers.onSearch?.();
18
+ return;
19
+ }
20
+
21
+ // Cmd/Ctrl + N - New thread
22
+ if (isMod && e.key === 'n') {
23
+ e.preventDefault();
24
+ handlers.onNewThread?.();
25
+ return;
26
+ }
27
+
28
+ // Escape - Close modal/cancel
29
+ if (e.key === 'Escape') {
30
+ handlers.onEscape?.();
31
+ return;
32
+ }
33
+ },
34
+ [handlers]
35
+ );
36
+
37
+ useEffect(() => {
38
+ window.addEventListener('keydown', handleKeyDown);
39
+ return () => window.removeEventListener('keydown', handleKeyDown);
40
+ }, [handleKeyDown]);
41
+ }
42
+
43
+ // Display helpers for keyboard shortcut hints
44
+ export function getModifierKey(): string {
45
+ return navigator.platform.includes('Mac') ? '⌘' : 'Ctrl';
46
+ }
47
+
48
+ export function formatShortcut(key: string): string {
49
+ return `${getModifierKey()}+${key.toUpperCase()}`;
50
+ }
@@ -0,0 +1,106 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+ import type { MentionableDocument, DocumentScope } from '@chaaskit/shared';
3
+ import { api } from '../utils/api';
4
+
5
+ interface MentionSearchResult {
6
+ documents: MentionableDocument[];
7
+ grouped: {
8
+ my: MentionableDocument[];
9
+ team: MentionableDocument[];
10
+ project: MentionableDocument[];
11
+ };
12
+ hasMore: boolean;
13
+ }
14
+
15
+ interface UseMentionSearchOptions {
16
+ debounceMs?: number;
17
+ limit?: number;
18
+ }
19
+
20
+ export function useMentionSearch(options: UseMentionSearchOptions = {}) {
21
+ const { debounceMs = 200, limit = 20 } = options;
22
+
23
+ const [isSearching, setIsSearching] = useState(false);
24
+ const [results, setResults] = useState<MentionSearchResult | null>(null);
25
+ const [error, setError] = useState<string | null>(null);
26
+
27
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
28
+ const abortControllerRef = useRef<AbortController | null>(null);
29
+
30
+ const search = useCallback(
31
+ async (query: string, scope?: DocumentScope, teamId?: string, projectId?: string) => {
32
+ // Clear any pending debounce
33
+ if (debounceRef.current) {
34
+ clearTimeout(debounceRef.current);
35
+ }
36
+
37
+ // Cancel any in-flight request
38
+ if (abortControllerRef.current) {
39
+ abortControllerRef.current.abort();
40
+ }
41
+
42
+ // Debounce the search (allow empty query to show all documents)
43
+ return new Promise<void>((resolve) => {
44
+ debounceRef.current = setTimeout(async () => {
45
+ setIsSearching(true);
46
+ setError(null);
47
+
48
+ abortControllerRef.current = new AbortController();
49
+
50
+ try {
51
+ const params = new URLSearchParams();
52
+ if (query) params.set('q', query);
53
+ if (scope) params.set('scope', scope);
54
+ if (teamId) params.set('teamId', teamId);
55
+ if (projectId) params.set('projectId', projectId);
56
+ params.set('limit', limit.toString());
57
+
58
+ const result = await api.get<MentionSearchResult>(
59
+ `/api/mentions/search?${params.toString()}`
60
+ );
61
+
62
+ setResults(result);
63
+ } catch (err) {
64
+ if (err instanceof Error && err.name === 'AbortError') {
65
+ // Ignore aborted requests
66
+ return;
67
+ }
68
+ console.error('Mention search error:', err);
69
+ setError(err instanceof Error ? err.message : 'Search failed');
70
+ setResults(null);
71
+ } finally {
72
+ setIsSearching(false);
73
+ resolve();
74
+ }
75
+ }, debounceMs);
76
+ });
77
+ },
78
+ [debounceMs, limit]
79
+ );
80
+
81
+ const clearResults = useCallback(() => {
82
+ setResults(null);
83
+ setError(null);
84
+ setIsSearching(false);
85
+ }, []);
86
+
87
+ // Cleanup on unmount
88
+ useEffect(() => {
89
+ return () => {
90
+ if (debounceRef.current) {
91
+ clearTimeout(debounceRef.current);
92
+ }
93
+ if (abortControllerRef.current) {
94
+ abortControllerRef.current.abort();
95
+ }
96
+ };
97
+ }, []);
98
+
99
+ return {
100
+ search,
101
+ clearResults,
102
+ results,
103
+ isSearching,
104
+ error,
105
+ };
106
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,116 @@
1
+ // Import styles for CSS extraction during build
2
+ import './styles/index.css';
3
+
4
+ // ClientOnly and Loading Skeletons for SSR-safe rendering
5
+ export { ClientOnly } from './components/ClientOnly';
6
+ export { ChatLoadingSkeleton, SimpleLoadingSkeleton } from './components/LoadingSkeletons';
7
+
8
+ // Context providers
9
+ export { AuthProvider, useAuth } from './contexts/AuthContext';
10
+ export { ThemeProvider, useTheme } from './contexts/ThemeContext';
11
+ export { ConfigProvider, useConfig, useConfigLoaded } from './contexts/ConfigContext';
12
+ export { TeamProvider, useTeam } from './contexts/TeamContext';
13
+ export { ProjectProvider, useProject } from './contexts/ProjectContext';
14
+
15
+ // SSR-safe context providers
16
+ export {
17
+ ServerConfigProvider,
18
+ useServerConfig,
19
+ useServerConfigLoaded,
20
+ } from './contexts/ServerConfigProvider';
21
+ export {
22
+ ServerThemeProvider,
23
+ useServerTheme,
24
+ } from './contexts/ServerThemeProvider';
25
+
26
+ // Stores
27
+ export { useChatStore } from './stores/chatStore';
28
+
29
+ // Hooks
30
+ export { useBasePath } from './hooks/useBasePath';
31
+ export { useAppPath } from './hooks/useAppPath';
32
+
33
+ // Extension registry
34
+ export { clientRegistry } from './extensions/registry';
35
+
36
+ // Layout components for implementing projects
37
+ export { default as MainLayout } from './layouts/MainLayout';
38
+ export { default as Sidebar } from './components/Sidebar';
39
+
40
+ // SSR-safe components for server rendering
41
+ export { SSRMessageList } from './components/SSRMessageList';
42
+ export { SSRMarkdownRenderer } from './components/content/SSRMarkdownRenderer';
43
+
44
+ // Re-export styles path for consumers
45
+ export const styles = '@chaaskit/client/src/styles/index.css';
46
+
47
+ // ============================================
48
+ // Page Components (for React Router v7 routes)
49
+ // ============================================
50
+ export { default as ChatPage } from './pages/ChatPage';
51
+ export { default as LoginPage } from './pages/LoginPage';
52
+ export { default as RegisterPage } from './pages/RegisterPage';
53
+ export { default as VerifyEmailPage } from './pages/VerifyEmailPage';
54
+ export { default as SharedThreadPage } from './pages/SharedThreadPage';
55
+ export { default as PricingPage } from './pages/PricingPage';
56
+ export { default as PrivacyPage } from './pages/PrivacyPage';
57
+ export { default as TermsPage } from './pages/TermsPage';
58
+ export { default as ApiKeysPage } from './pages/ApiKeysPage';
59
+ export { default as DocumentsPage } from './pages/DocumentsPage';
60
+ export { default as ScheduledPromptsPage } from './pages/ScheduledPromptsPage';
61
+ export { default as TeamSettingsPage } from './pages/TeamSettingsPage';
62
+ export { default as AcceptInvitePage } from './pages/AcceptInvitePage';
63
+ export { default as OAuthConsentPage } from './pages/OAuthConsentPage';
64
+ export { default as AdminDashboardPage } from './pages/AdminDashboardPage';
65
+ export { default as AdminUsersPage } from './pages/AdminUsersPage';
66
+ export { default as AdminTeamsPage } from './pages/AdminTeamsPage';
67
+ export { default as AdminTeamPage } from './pages/AdminTeamPage';
68
+
69
+ // ============================================
70
+ // ChatProviders - Wraps the chat app with all required providers
71
+ // Use in React Router routes that render the chat interface
72
+ // ============================================
73
+ import React from 'react';
74
+ import { AuthProvider as Auth } from './contexts/AuthContext';
75
+ import { ThemeProvider as Theme } from './contexts/ThemeContext';
76
+ import { ConfigProvider as Config } from './contexts/ConfigContext';
77
+ import { TeamProvider as Team } from './contexts/TeamContext';
78
+ import { ProjectProvider as Project } from './contexts/ProjectContext';
79
+
80
+ export interface ChatProvidersProps {
81
+ children: React.ReactNode;
82
+ }
83
+
84
+ /**
85
+ * Wraps children with all context providers needed for the chat application.
86
+ * Use this in React Router routes that render chat-related pages.
87
+ *
88
+ * @example
89
+ * ```tsx
90
+ * // app/routes/chat.tsx
91
+ * import { ChatProviders, ChatPage } from '@chaaskit/client';
92
+ *
93
+ * export default function Chat() {
94
+ * return (
95
+ * <ChatProviders>
96
+ * <ChatPage />
97
+ * </ChatProviders>
98
+ * );
99
+ * }
100
+ * ```
101
+ */
102
+ export function ChatProviders({ children }: ChatProvidersProps) {
103
+ return (
104
+ <Config>
105
+ <Theme>
106
+ <Auth>
107
+ <Team>
108
+ <Project>
109
+ {children}
110
+ </Project>
111
+ </Team>
112
+ </Auth>
113
+ </Theme>
114
+ </Config>
115
+ );
116
+ }
@@ -0,0 +1,98 @@
1
+ import { useState, useCallback, type ReactNode } from 'react';
2
+ import { Menu, Search } from 'lucide-react';
3
+ import { useParams, useNavigate } from 'react-router';
4
+ import Sidebar from '../components/Sidebar';
5
+ import SearchModal from '../components/SearchModal';
6
+ import { useConfig } from '../contexts/ConfigContext';
7
+ import { useChatStore } from '../stores/chatStore';
8
+ import { useKeyboardShortcuts, formatShortcut } from '../hooks/useKeyboardShortcuts';
9
+ import { useAppPath } from '../hooks/useAppPath';
10
+
11
+ interface MainLayoutProps {
12
+ children: ReactNode;
13
+ }
14
+
15
+ export default function MainLayout({ children }: MainLayoutProps) {
16
+ const [sidebarOpen, setSidebarOpen] = useState(false);
17
+ const [searchOpen, setSearchOpen] = useState(false);
18
+ const { threadId } = useParams();
19
+ const navigate = useNavigate();
20
+ const appPath = useAppPath();
21
+ const config = useConfig();
22
+ const { currentThread, clearCurrentThread } = useChatStore();
23
+
24
+ // Get display title for mobile header
25
+ const headerTitle = currentThread?.title || config.app.name;
26
+
27
+ // Keyboard shortcuts
28
+ const handleNewThread = useCallback(() => {
29
+ // Clear current thread and navigate to welcome screen
30
+ // Thread will be created when user sends first message with selected agent
31
+ clearCurrentThread();
32
+ navigate(appPath('/'));
33
+ setSidebarOpen(false);
34
+ }, [clearCurrentThread, navigate, appPath]);
35
+
36
+ useKeyboardShortcuts({
37
+ onSearch: () => setSearchOpen(true),
38
+ onNewThread: handleNewThread,
39
+ onEscape: () => {
40
+ if (searchOpen) setSearchOpen(false);
41
+ else if (sidebarOpen) setSidebarOpen(false);
42
+ },
43
+ });
44
+
45
+ return (
46
+ <div className="flex h-screen-safe bg-background" style={{ paddingTop: 'env(safe-area-inset-top)' }}>
47
+ {/* Mobile sidebar backdrop */}
48
+ {sidebarOpen && (
49
+ <div
50
+ className="fixed inset-0 z-20 bg-black/50 lg:hidden"
51
+ onClick={() => setSidebarOpen(false)}
52
+ />
53
+ )}
54
+
55
+ {/* Sidebar */}
56
+ <aside
57
+ className={`
58
+ fixed inset-y-0 left-0 z-30 w-64 transform bg-sidebar transition-transform duration-200 ease-in-out
59
+ lg:relative lg:translate-x-0
60
+ ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
61
+ `}
62
+ style={{ paddingTop: 'env(safe-area-inset-top)' }}
63
+ >
64
+ <Sidebar onClose={() => setSidebarOpen(false)} onOpenSearch={() => setSearchOpen(true)} />
65
+ </aside>
66
+
67
+ {/* Main content */}
68
+ <main className="flex flex-1 flex-col overflow-hidden">
69
+ {/* Mobile header - sticky at top */}
70
+ <header className="sticky top-0 z-10 flex items-center gap-3 border-b border-border bg-background px-4 py-3 lg:hidden">
71
+ <button
72
+ onClick={() => setSidebarOpen(true)}
73
+ className="flex-shrink-0 rounded-lg p-2 text-text-secondary hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary"
74
+ aria-label="Open menu"
75
+ >
76
+ <Menu size={20} />
77
+ </button>
78
+ <h1 className="flex-1 truncate text-base font-medium text-text-primary">
79
+ {headerTitle}
80
+ </h1>
81
+ <button
82
+ onClick={() => setSearchOpen(true)}
83
+ className="flex-shrink-0 rounded-lg p-2 text-text-secondary hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary"
84
+ aria-label="Search"
85
+ >
86
+ <Search size={20} />
87
+ </button>
88
+ </header>
89
+
90
+ {/* Page content */}
91
+ <div className="flex-1 overflow-hidden">{children}</div>
92
+ </main>
93
+
94
+ {/* Search Modal */}
95
+ <SearchModal isOpen={searchOpen} onClose={() => setSearchOpen(false)} />
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,175 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { useParams, useNavigate, Link } from 'react-router';
3
+ import { useAuth } from '../contexts/AuthContext';
4
+ import { useTeam } from '../contexts/TeamContext';
5
+ import { useAppPath } from '../hooks/useAppPath';
6
+ import { api } from '../utils/api';
7
+
8
+ interface InviteDetails {
9
+ email: string;
10
+ role: string;
11
+ teamName: string;
12
+ expiresAt: string;
13
+ }
14
+
15
+ export default function AcceptInvitePage() {
16
+ const { token } = useParams<{ token: string }>();
17
+ const navigate = useNavigate();
18
+ const appPath = useAppPath();
19
+ const { user, isLoading: authLoading } = useAuth();
20
+ const { loadTeams, setCurrentTeamId } = useTeam();
21
+
22
+ const [invite, setInvite] = useState<InviteDetails | null>(null);
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const [isAccepting, setIsAccepting] = useState(false);
25
+ const [error, setError] = useState('');
26
+
27
+ useEffect(() => {
28
+ async function loadInvite() {
29
+ if (!token) return;
30
+
31
+ try {
32
+ const response = await api.get<{ invite: InviteDetails }>(`/api/teams/invite/${token}`);
33
+ setInvite(response.invite);
34
+ } catch (err) {
35
+ setError(err instanceof Error ? err.message : 'Failed to load invite');
36
+ } finally {
37
+ setIsLoading(false);
38
+ }
39
+ }
40
+
41
+ loadInvite();
42
+ }, [token]);
43
+
44
+ const handleAccept = async () => {
45
+ if (!token) return;
46
+
47
+ setIsAccepting(true);
48
+ setError('');
49
+
50
+ try {
51
+ const response = await api.post<{ team: { id: string } }>(`/api/teams/invite/${token}/accept`, {});
52
+ await loadTeams();
53
+ setCurrentTeamId(response.team.id);
54
+ navigate(appPath('/'));
55
+ } catch (err) {
56
+ setError(err instanceof Error ? err.message : 'Failed to accept invite');
57
+ setIsAccepting(false);
58
+ }
59
+ };
60
+
61
+ if (authLoading || isLoading) {
62
+ return (
63
+ <div className="flex min-h-screen items-center justify-center bg-[var(--color-background)]">
64
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[var(--color-primary)]" />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ if (error) {
70
+ return (
71
+ <div className="flex min-h-screen items-center justify-center bg-[var(--color-background)] p-4">
72
+ <div className="w-full max-w-md text-center">
73
+ <div className="mb-4 rounded-lg bg-[var(--color-error)]/10 p-4 text-[var(--color-error)]">
74
+ {error}
75
+ </div>
76
+ <Link
77
+ to="/"
78
+ className="text-[var(--color-primary)] hover:underline"
79
+ >
80
+ Go to homepage
81
+ </Link>
82
+ </div>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ if (!user) {
88
+ return (
89
+ <div className="flex min-h-screen items-center justify-center bg-[var(--color-background)] p-4">
90
+ <div className="w-full max-w-md text-center">
91
+ <h1 className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
92
+ Team Invitation
93
+ </h1>
94
+ {invite && (
95
+ <div className="mb-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-background-secondary)] p-6">
96
+ <p className="text-[var(--color-text-primary)] mb-2">
97
+ You've been invited to join
98
+ </p>
99
+ <p className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
100
+ {invite.teamName}
101
+ </p>
102
+ <p className="text-sm text-[var(--color-text-muted)]">
103
+ as {invite.role}
104
+ </p>
105
+ </div>
106
+ )}
107
+ <p className="text-[var(--color-text-secondary)] mb-6">
108
+ Please sign in or create an account to accept this invitation.
109
+ </p>
110
+ <div className="flex gap-4 justify-center">
111
+ <Link
112
+ to="/login"
113
+ className="rounded-lg bg-[var(--color-primary)] px-6 py-2 font-medium text-white hover:bg-[var(--color-primary-hover)]"
114
+ >
115
+ Sign In
116
+ </Link>
117
+ <Link
118
+ to="/register"
119
+ className="rounded-lg border border-[var(--color-border)] px-6 py-2 font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-background-secondary)]"
120
+ >
121
+ Create Account
122
+ </Link>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ return (
130
+ <div className="flex min-h-screen items-center justify-center bg-[var(--color-background)] p-4">
131
+ <div className="w-full max-w-md text-center">
132
+ <h1 className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
133
+ Team Invitation
134
+ </h1>
135
+ {invite && (
136
+ <>
137
+ <div className="mb-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-background-secondary)] p-6">
138
+ <p className="text-[var(--color-text-primary)] mb-2">
139
+ You've been invited to join
140
+ </p>
141
+ <p className="text-2xl font-bold text-[var(--color-text-primary)] mb-4">
142
+ {invite.teamName}
143
+ </p>
144
+ <p className="text-sm text-[var(--color-text-muted)]">
145
+ as {invite.role}
146
+ </p>
147
+ </div>
148
+
149
+ {user.email.toLowerCase() !== invite.email.toLowerCase() && (
150
+ <div className="mb-6 rounded-lg bg-[var(--color-warning)]/10 p-4 text-sm text-[var(--color-warning)]">
151
+ This invite was sent to {invite.email}. You are signed in as {user.email}.
152
+ </div>
153
+ )}
154
+
155
+ <div className="flex gap-4 justify-center">
156
+ <button
157
+ onClick={handleAccept}
158
+ disabled={isAccepting}
159
+ className="rounded-lg bg-[var(--color-primary)] px-6 py-2 font-medium text-white hover:bg-[var(--color-primary-hover)] disabled:opacity-50"
160
+ >
161
+ {isAccepting ? 'Accepting...' : 'Accept Invitation'}
162
+ </button>
163
+ <Link
164
+ to="/"
165
+ className="rounded-lg border border-[var(--color-border)] px-6 py-2 font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-background-secondary)]"
166
+ >
167
+ Decline
168
+ </Link>
169
+ </div>
170
+ </>
171
+ )}
172
+ </div>
173
+ </div>
174
+ );
175
+ }