@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,242 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { Link } from 'react-router';
3
+ import { LayoutDashboard, X, Users, Building2 } from 'lucide-react';
4
+ import { useAppPath } from '../hooks/useAppPath';
5
+ import { api } from '../utils/api';
6
+ import type { AdminTeam, AdminTeamsResponse } from '@chaaskit/shared';
7
+
8
+ export default function AdminTeamsPage() {
9
+ const appPath = useAppPath();
10
+ const [teams, setTeams] = useState<AdminTeam[]>([]);
11
+ const [total, setTotal] = useState(0);
12
+ const [page, setPage] = useState(1);
13
+ const [pageSize] = useState(20);
14
+ const [search, setSearch] = useState('');
15
+ const [searchInput, setSearchInput] = useState('');
16
+ const [isLoading, setIsLoading] = useState(true);
17
+ const [error, setError] = useState('');
18
+
19
+ const loadTeams = useCallback(async () => {
20
+ setIsLoading(true);
21
+ setError('');
22
+
23
+ try {
24
+ const params = new URLSearchParams({
25
+ page: page.toString(),
26
+ pageSize: pageSize.toString(),
27
+ });
28
+ if (search) {
29
+ params.set('search', search);
30
+ }
31
+
32
+ const response = await api.get<AdminTeamsResponse>(`/api/admin/teams?${params}`);
33
+ setTeams(response.teams);
34
+ setTotal(response.total);
35
+ } catch (err) {
36
+ setError(err instanceof Error ? err.message : 'Failed to load teams');
37
+ } finally {
38
+ setIsLoading(false);
39
+ }
40
+ }, [page, pageSize, search]);
41
+
42
+ useEffect(() => {
43
+ loadTeams();
44
+ }, [loadTeams]);
45
+
46
+ const handleSearch = (e: React.FormEvent) => {
47
+ e.preventDefault();
48
+ setPage(1);
49
+ setSearch(searchInput);
50
+ };
51
+
52
+ const totalPages = Math.ceil(total / pageSize);
53
+
54
+ return (
55
+ <div className="min-h-screen bg-background p-4 sm:p-8">
56
+ <div className="mx-auto max-w-6xl">
57
+ {/* Header */}
58
+ <div className="flex items-center justify-between mb-4">
59
+ <h1 className="text-xl sm:text-2xl font-bold text-text-primary">
60
+ Admin
61
+ </h1>
62
+ <Link
63
+ to={appPath('/')}
64
+ className="flex items-center justify-center rounded-lg p-2 text-text-muted hover:text-text-primary hover:bg-background-secondary"
65
+ aria-label="Close"
66
+ >
67
+ <X size={20} />
68
+ </Link>
69
+ </div>
70
+
71
+ {/* Tab Navigation */}
72
+ <div className="flex items-center gap-2 mb-6 sm:mb-8">
73
+ <Link
74
+ to={appPath('/admin')}
75
+ className="flex items-center gap-1.5 rounded-full bg-background-secondary px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary/80"
76
+ >
77
+ <LayoutDashboard size={16} />
78
+ Overview
79
+ </Link>
80
+ <Link
81
+ to={appPath('/admin/users')}
82
+ className="flex items-center gap-1.5 rounded-full bg-background-secondary px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary/80"
83
+ >
84
+ <Users size={16} />
85
+ Users
86
+ </Link>
87
+ <Link
88
+ to={appPath('/admin/teams')}
89
+ className="flex items-center gap-1.5 rounded-full bg-primary px-4 py-2 text-sm font-medium text-white"
90
+ >
91
+ <Building2 size={16} />
92
+ Teams
93
+ </Link>
94
+ </div>
95
+
96
+ {error && (
97
+ <div className="mb-6 rounded-lg bg-error/10 p-4 text-sm text-error">
98
+ {error}
99
+ </div>
100
+ )}
101
+
102
+ {/* Search */}
103
+ <div className="mb-6 rounded-lg bg-background-secondary p-4">
104
+ <form onSubmit={handleSearch} className="flex gap-2 sm:gap-4">
105
+ <input
106
+ type="text"
107
+ placeholder="Search by team name..."
108
+ value={searchInput}
109
+ onChange={(e) => setSearchInput(e.target.value)}
110
+ className="flex-1 rounded-lg border border-input-border bg-input-background px-3 sm:px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
111
+ />
112
+ <button
113
+ type="submit"
114
+ className="rounded-lg bg-primary px-3 sm:px-4 py-2 font-medium text-white hover:bg-primary-hover"
115
+ >
116
+ Search
117
+ </button>
118
+ </form>
119
+ </div>
120
+
121
+ {/* Teams List */}
122
+ <div className="rounded-lg bg-background-secondary overflow-hidden">
123
+ {/* Header - Hidden on mobile */}
124
+ <div className="hidden sm:block px-4 py-3 bg-background">
125
+ <div className="grid grid-cols-12 gap-4 text-sm font-medium text-text-muted">
126
+ <div className="col-span-4">Team</div>
127
+ <div className="col-span-2">Members</div>
128
+ <div className="col-span-2">Threads</div>
129
+ <div className="col-span-4">Created</div>
130
+ </div>
131
+ </div>
132
+
133
+ {/* Body */}
134
+ <div className="divide-y divide-background">
135
+ {isLoading ? (
136
+ <div className="px-4 py-8 text-center text-text-muted">
137
+ Loading...
138
+ </div>
139
+ ) : teams.length === 0 ? (
140
+ <div className="px-4 py-8 text-center text-text-muted">
141
+ No teams found
142
+ </div>
143
+ ) : (
144
+ teams.map((team) => (
145
+ <Link
146
+ key={team.id}
147
+ to={`/admin/teams/${team.id}`}
148
+ className="block px-4 py-3 hover:bg-background/50"
149
+ >
150
+ {/* Desktop View */}
151
+ <div className="hidden sm:grid grid-cols-12 gap-4 items-center">
152
+ {/* Team Name */}
153
+ <div className="col-span-4">
154
+ <div className="flex items-center gap-3">
155
+ <div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white text-sm font-medium">
156
+ {team.name[0].toUpperCase()}
157
+ </div>
158
+ <div>
159
+ <div className="text-sm font-medium text-text-primary">
160
+ {team.name}
161
+ </div>
162
+ {team.archivedAt && (
163
+ <span className="text-xs text-text-muted">
164
+ (archived)
165
+ </span>
166
+ )}
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ {/* Members */}
172
+ <div className="col-span-2 text-sm text-text-primary">
173
+ {team.memberCount}
174
+ </div>
175
+
176
+ {/* Threads */}
177
+ <div className="col-span-2 text-sm text-text-primary">
178
+ {team.threadCount}
179
+ </div>
180
+
181
+ {/* Created */}
182
+ <div className="col-span-4 text-sm text-text-muted">
183
+ {new Date(team.createdAt).toLocaleDateString()}
184
+ </div>
185
+ </div>
186
+
187
+ {/* Mobile View */}
188
+ <div className="sm:hidden">
189
+ <div className="flex items-center justify-between">
190
+ <div className="flex items-center gap-3">
191
+ <div className="w-10 h-10 rounded-full bg-primary flex items-center justify-center text-white text-sm font-medium">
192
+ {team.name[0].toUpperCase()}
193
+ </div>
194
+ <div>
195
+ <div className="text-sm font-medium text-text-primary">
196
+ {team.name}
197
+ </div>
198
+ <div className="text-xs text-text-muted">
199
+ {team.memberCount} members · {team.threadCount} threads
200
+ {team.archivedAt && ' · archived'}
201
+ </div>
202
+ </div>
203
+ </div>
204
+ <div className="text-xs text-text-muted">
205
+ {new Date(team.createdAt).toLocaleDateString()}
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </Link>
210
+ ))
211
+ )}
212
+ </div>
213
+
214
+ {/* Pagination */}
215
+ {totalPages > 1 && (
216
+ <div className="flex items-center justify-between px-4 py-3 bg-background">
217
+ <div className="text-xs sm:text-sm text-text-muted">
218
+ {(page - 1) * pageSize + 1}-{Math.min(page * pageSize, total)} of {total}
219
+ </div>
220
+ <div className="flex gap-2">
221
+ <button
222
+ onClick={() => setPage((p) => Math.max(1, p - 1))}
223
+ disabled={page === 1}
224
+ className="rounded bg-background-secondary px-3 py-1 text-sm text-text-primary hover:bg-background-secondary/80 disabled:opacity-50"
225
+ >
226
+ Prev
227
+ </button>
228
+ <button
229
+ onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
230
+ disabled={page === totalPages}
231
+ className="rounded bg-background-secondary px-3 py-1 text-sm text-text-primary hover:bg-background-secondary/80 disabled:opacity-50"
232
+ >
233
+ Next
234
+ </button>
235
+ </div>
236
+ </div>
237
+ )}
238
+ </div>
239
+ </div>
240
+ </div>
241
+ );
242
+ }
@@ -0,0 +1,385 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { Link } from 'react-router';
3
+ import { LayoutDashboard, X, Users, Building2 } from 'lucide-react';
4
+ import { useConfig } from '../contexts/ConfigContext';
5
+ import { useAppPath } from '../hooks/useAppPath';
6
+ import { api } from '../utils/api';
7
+ import { useAuth } from '../contexts/AuthContext';
8
+ import type { AdminUser, AdminUsersResponse } from '@chaaskit/shared';
9
+
10
+ export default function AdminUsersPage() {
11
+ const { user: currentUser } = useAuth();
12
+ const config = useConfig();
13
+ const appPath = useAppPath();
14
+ const [users, setUsers] = useState<AdminUser[]>([]);
15
+ const [total, setTotal] = useState(0);
16
+ const [page, setPage] = useState(1);
17
+ const [pageSize] = useState(20);
18
+ const [search, setSearch] = useState('');
19
+ const [searchInput, setSearchInput] = useState('');
20
+ const [isLoading, setIsLoading] = useState(true);
21
+ const [error, setError] = useState('');
22
+ const [success, setSuccess] = useState('');
23
+
24
+ const loadUsers = useCallback(async () => {
25
+ setIsLoading(true);
26
+ setError('');
27
+
28
+ try {
29
+ const params = new URLSearchParams({
30
+ page: page.toString(),
31
+ pageSize: pageSize.toString(),
32
+ });
33
+ if (search) {
34
+ params.set('search', search);
35
+ }
36
+
37
+ const response = await api.get<AdminUsersResponse>(`/api/admin/users?${params}`);
38
+ setUsers(response.users);
39
+ setTotal(response.total);
40
+ } catch (err) {
41
+ setError(err instanceof Error ? err.message : 'Failed to load users');
42
+ } finally {
43
+ setIsLoading(false);
44
+ }
45
+ }, [page, pageSize, search]);
46
+
47
+ useEffect(() => {
48
+ loadUsers();
49
+ }, [loadUsers]);
50
+
51
+ const handleSearch = (e: React.FormEvent) => {
52
+ e.preventDefault();
53
+ setPage(1);
54
+ setSearch(searchInput);
55
+ };
56
+
57
+ const handleToggleAdmin = async (userId: string, currentIsAdmin: boolean) => {
58
+ if (userId === currentUser?.id) {
59
+ setError('Cannot modify your own admin status');
60
+ return;
61
+ }
62
+
63
+ const action = currentIsAdmin ? 'remove admin status from' : 'make';
64
+ if (!confirm(`Are you sure you want to ${action} this user ${currentIsAdmin ? '' : 'an admin'}?`)) {
65
+ return;
66
+ }
67
+
68
+ setError('');
69
+ setSuccess('');
70
+
71
+ try {
72
+ await api.patch(`/api/admin/users/${userId}`, { isAdmin: !currentIsAdmin });
73
+ setSuccess(`User ${currentIsAdmin ? 'demoted' : 'promoted'} successfully`);
74
+ loadUsers();
75
+ } catch (err) {
76
+ setError(err instanceof Error ? err.message : 'Failed to update user');
77
+ }
78
+ };
79
+
80
+ const handleChangePlan = async (userId: string, plan: string) => {
81
+ setError('');
82
+ setSuccess('');
83
+
84
+ try {
85
+ await api.patch(`/api/admin/users/${userId}`, { plan });
86
+ setSuccess('Plan updated successfully');
87
+ loadUsers();
88
+ } catch (err) {
89
+ setError(err instanceof Error ? err.message : 'Failed to update plan');
90
+ }
91
+ };
92
+
93
+ const totalPages = Math.ceil(total / pageSize);
94
+
95
+ return (
96
+ <div className="min-h-screen bg-background p-4 sm:p-8">
97
+ <div className="mx-auto max-w-6xl">
98
+ {/* Header */}
99
+ <div className="flex items-center justify-between mb-4">
100
+ <h1 className="text-xl sm:text-2xl font-bold text-text-primary">
101
+ Admin
102
+ </h1>
103
+ <Link
104
+ to={appPath('/')}
105
+ className="flex items-center justify-center rounded-lg p-2 text-text-muted hover:text-text-primary hover:bg-background-secondary"
106
+ aria-label="Close"
107
+ >
108
+ <X size={20} />
109
+ </Link>
110
+ </div>
111
+
112
+ {/* Tab Navigation */}
113
+ <div className="flex items-center gap-2 mb-6 sm:mb-8">
114
+ <Link
115
+ to={appPath('/admin')}
116
+ className="flex items-center gap-1.5 rounded-full bg-background-secondary px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary/80"
117
+ >
118
+ <LayoutDashboard size={16} />
119
+ Overview
120
+ </Link>
121
+ <Link
122
+ to={appPath('/admin/users')}
123
+ className="flex items-center gap-1.5 rounded-full bg-primary px-4 py-2 text-sm font-medium text-white"
124
+ >
125
+ <Users size={16} />
126
+ Users
127
+ </Link>
128
+ {config.teams?.enabled && (
129
+ <Link
130
+ to={appPath('/admin/teams')}
131
+ className="flex items-center gap-1.5 rounded-full bg-background-secondary px-4 py-2 text-sm font-medium text-text-secondary hover:bg-background-secondary/80"
132
+ >
133
+ <Building2 size={16} />
134
+ Teams
135
+ </Link>
136
+ )}
137
+ </div>
138
+
139
+ {error && (
140
+ <div className="mb-6 rounded-lg bg-error/10 p-4 text-sm text-error">
141
+ {error}
142
+ </div>
143
+ )}
144
+
145
+ {success && (
146
+ <div className="mb-6 rounded-lg bg-success/10 p-4 text-sm text-success">
147
+ {success}
148
+ </div>
149
+ )}
150
+
151
+ {/* Search */}
152
+ <div className="mb-6 rounded-lg bg-background-secondary p-4">
153
+ <form onSubmit={handleSearch} className="flex gap-4">
154
+ <input
155
+ type="text"
156
+ placeholder="Search by email or name..."
157
+ value={searchInput}
158
+ onChange={(e) => setSearchInput(e.target.value)}
159
+ className="flex-1 rounded-lg border border-input-border bg-input-background px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
160
+ />
161
+ <button
162
+ type="submit"
163
+ className="rounded-lg bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover"
164
+ >
165
+ Search
166
+ </button>
167
+ </form>
168
+ </div>
169
+
170
+ {/* Users List */}
171
+ <div className="rounded-lg bg-background-secondary overflow-hidden">
172
+ {/* Header - Hidden on mobile */}
173
+ <div className="hidden md:block px-4 py-3 bg-background">
174
+ <div className="grid grid-cols-12 gap-4 text-sm font-medium text-text-muted">
175
+ <div className="col-span-3">User</div>
176
+ <div className="col-span-1">Plan</div>
177
+ <div className="col-span-1">Messages</div>
178
+ <div className="col-span-3">Teams</div>
179
+ <div className="col-span-1">Admin</div>
180
+ <div className="col-span-3">Joined</div>
181
+ </div>
182
+ </div>
183
+
184
+ {/* Body */}
185
+ <div className="divide-y divide-background">
186
+ {isLoading ? (
187
+ <div className="px-4 py-8 text-center text-text-muted">
188
+ Loading...
189
+ </div>
190
+ ) : users.length === 0 ? (
191
+ <div className="px-4 py-8 text-center text-text-muted">
192
+ No users found
193
+ </div>
194
+ ) : (
195
+ users.map((user) => (
196
+ <div
197
+ key={user.id}
198
+ className="px-4 py-3 hover:bg-background/50"
199
+ >
200
+ {/* Desktop View */}
201
+ <div className="hidden md:grid grid-cols-12 gap-4 items-center">
202
+ {/* User Info */}
203
+ <div className="col-span-3 flex items-center gap-3">
204
+ {user.avatarUrl ? (
205
+ <img
206
+ src={user.avatarUrl}
207
+ alt=""
208
+ className="w-8 h-8 rounded-full"
209
+ />
210
+ ) : (
211
+ <div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-white text-sm font-medium">
212
+ {(user.name || user.email)[0].toUpperCase()}
213
+ </div>
214
+ )}
215
+ <div className="min-w-0">
216
+ <div className="text-sm text-text-primary truncate">
217
+ {user.name || '-'}
218
+ {user.id === currentUser?.id && (
219
+ <span className="ml-2 text-xs text-text-muted">(you)</span>
220
+ )}
221
+ </div>
222
+ <div className="text-xs text-text-muted truncate">
223
+ {user.email}
224
+ {user.oauthProvider && (
225
+ <span className="ml-1 capitalize">({user.oauthProvider})</span>
226
+ )}
227
+ </div>
228
+ </div>
229
+ </div>
230
+
231
+ {/* Plan */}
232
+ <div className="col-span-1">
233
+ <select
234
+ value={user.plan}
235
+ onChange={(e) => handleChangePlan(user.id, e.target.value)}
236
+ className="w-full rounded border border-input-border bg-input-background px-2 py-1 text-xs text-text-primary"
237
+ >
238
+ <option value="free">Free</option>
239
+ <option value="basic">Basic</option>
240
+ <option value="pro">Pro</option>
241
+ <option value="enterprise">Enterprise</option>
242
+ </select>
243
+ </div>
244
+
245
+ {/* Messages */}
246
+ <div className="col-span-1 text-sm text-text-primary">
247
+ {user.messagesThisMonth}
248
+ </div>
249
+
250
+ {/* Teams */}
251
+ <div className="col-span-3">
252
+ {user.teams && user.teams.length > 0 ? (
253
+ <div className="flex flex-wrap gap-1">
254
+ {user.teams.map((team) => (
255
+ <Link
256
+ key={team.id}
257
+ to={`/admin/teams/${team.id}`}
258
+ className="inline-flex items-center rounded-full bg-background px-2 py-0.5 text-xs text-text-secondary hover:bg-primary/10 hover:text-primary"
259
+ title={`Role: ${team.role}`}
260
+ >
261
+ {team.name}
262
+ <span className="ml-1 text-text-muted">
263
+ ({team.role})
264
+ </span>
265
+ </Link>
266
+ ))}
267
+ </div>
268
+ ) : (
269
+ <span className="text-xs text-text-muted">—</span>
270
+ )}
271
+ </div>
272
+
273
+ {/* Admin */}
274
+ <div className="col-span-1">
275
+ <button
276
+ onClick={() => handleToggleAdmin(user.id, user.isAdmin)}
277
+ disabled={user.id === currentUser?.id}
278
+ className={`px-2 py-1 rounded text-xs font-medium ${
279
+ user.isAdmin
280
+ ? 'bg-primary/10 text-primary'
281
+ : 'bg-background text-text-muted'
282
+ } ${user.id === currentUser?.id ? 'cursor-not-allowed opacity-50' : 'hover:opacity-80'}`}
283
+ >
284
+ {user.isAdmin ? 'Admin' : 'User'}
285
+ </button>
286
+ </div>
287
+
288
+ {/* Joined */}
289
+ <div className="col-span-3 text-sm text-text-muted">
290
+ {new Date(user.createdAt).toLocaleDateString()}
291
+ </div>
292
+ </div>
293
+
294
+ {/* Mobile View - Card layout */}
295
+ <div className="md:hidden space-y-3">
296
+ <div className="flex items-center justify-between">
297
+ <div className="flex items-center gap-3">
298
+ {user.avatarUrl ? (
299
+ <img
300
+ src={user.avatarUrl}
301
+ alt=""
302
+ className="w-10 h-10 rounded-full"
303
+ />
304
+ ) : (
305
+ <div className="w-10 h-10 rounded-full bg-primary flex items-center justify-center text-white text-sm font-medium">
306
+ {(user.name || user.email)[0].toUpperCase()}
307
+ </div>
308
+ )}
309
+ <div className="min-w-0">
310
+ <div className="text-sm font-medium text-text-primary truncate">
311
+ {user.name || '-'}
312
+ {user.id === currentUser?.id && (
313
+ <span className="ml-2 text-xs text-text-muted">(you)</span>
314
+ )}
315
+ </div>
316
+ <div className="text-xs text-text-muted truncate">
317
+ {user.email}
318
+ </div>
319
+ </div>
320
+ </div>
321
+ <button
322
+ onClick={() => handleToggleAdmin(user.id, user.isAdmin)}
323
+ disabled={user.id === currentUser?.id}
324
+ className={`px-2 py-1 rounded text-xs font-medium ${
325
+ user.isAdmin
326
+ ? 'bg-primary/10 text-primary'
327
+ : 'bg-background text-text-muted'
328
+ } ${user.id === currentUser?.id ? 'cursor-not-allowed opacity-50' : ''}`}
329
+ >
330
+ {user.isAdmin ? 'Admin' : 'User'}
331
+ </button>
332
+ </div>
333
+ <div className="flex items-center justify-between text-xs">
334
+ <div className="flex items-center gap-3">
335
+ <select
336
+ value={user.plan}
337
+ onChange={(e) => handleChangePlan(user.id, e.target.value)}
338
+ className="rounded border border-input-border bg-input-background px-2 py-1 text-xs text-text-primary"
339
+ >
340
+ <option value="free">Free</option>
341
+ <option value="basic">Basic</option>
342
+ <option value="pro">Pro</option>
343
+ <option value="enterprise">Enterprise</option>
344
+ </select>
345
+ <span className="text-text-muted">{user.messagesThisMonth} msgs</span>
346
+ </div>
347
+ <span className="text-text-muted">
348
+ {new Date(user.createdAt).toLocaleDateString()}
349
+ </span>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ ))
354
+ )}
355
+ </div>
356
+
357
+ {/* Pagination */}
358
+ {totalPages > 1 && (
359
+ <div className="flex items-center justify-between px-4 py-3 bg-background">
360
+ <div className="text-sm text-text-muted">
361
+ Showing {(page - 1) * pageSize + 1} to {Math.min(page * pageSize, total)} of {total} users
362
+ </div>
363
+ <div className="flex gap-2">
364
+ <button
365
+ onClick={() => setPage((p) => Math.max(1, p - 1))}
366
+ disabled={page === 1}
367
+ className="rounded bg-background-secondary px-3 py-1 text-sm text-text-primary hover:bg-background-secondary/80 disabled:opacity-50"
368
+ >
369
+ Previous
370
+ </button>
371
+ <button
372
+ onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
373
+ disabled={page === totalPages}
374
+ className="rounded bg-background-secondary px-3 py-1 text-sm text-text-primary hover:bg-background-secondary/80 disabled:opacity-50"
375
+ >
376
+ Next
377
+ </button>
378
+ </div>
379
+ </div>
380
+ )}
381
+ </div>
382
+ </div>
383
+ </div>
384
+ );
385
+ }