@chaaskit/client 0.1.1 → 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/dist/lib/index.js +970 -80
  3. package/dist/lib/index.js.map +1 -1
  4. package/dist/lib/routes/AdminPromoCodesRoute.js +19 -0
  5. package/dist/lib/routes/AdminPromoCodesRoute.js.map +1 -0
  6. package/dist/lib/routes/AdminWaitlistRoute.js +19 -0
  7. package/dist/lib/routes/AdminWaitlistRoute.js.map +1 -0
  8. package/dist/lib/routes.js +47 -37
  9. package/dist/lib/routes.js.map +1 -1
  10. package/dist/lib/ssr-utils.js +36 -16
  11. package/dist/lib/ssr-utils.js.map +1 -1
  12. package/dist/lib/styles.css +37 -0
  13. package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -1
  14. package/package.json +20 -12
  15. package/src/components/MessageItem.tsx +35 -4
  16. package/src/components/MessageList.tsx +51 -5
  17. package/src/components/OAuthAppsSection.tsx +1 -1
  18. package/src/components/Sidebar.tsx +1 -3
  19. package/src/components/ToolCallDisplay.tsx +102 -11
  20. package/src/components/tool-renderers/DocumentListRenderer.tsx +44 -0
  21. package/src/components/tool-renderers/DocumentReadRenderer.tsx +33 -0
  22. package/src/components/tool-renderers/DocumentSaveRenderer.tsx +32 -0
  23. package/src/components/tool-renderers/DocumentSearchRenderer.tsx +33 -0
  24. package/src/components/tool-renderers/index.ts +36 -0
  25. package/src/components/tool-renderers/utils.ts +7 -0
  26. package/src/contexts/AuthContext.tsx +16 -6
  27. package/src/contexts/ConfigContext.tsx +22 -28
  28. package/src/extensions/registry.ts +2 -1
  29. package/src/hooks/__tests__/basePath.test.ts +42 -0
  30. package/src/index.tsx +5 -2
  31. package/src/pages/AdminDashboardPage.tsx +15 -1
  32. package/src/pages/AdminPromoCodesPage.tsx +378 -0
  33. package/src/pages/AdminTeamPage.tsx +29 -1
  34. package/src/pages/AdminTeamsPage.tsx +15 -1
  35. package/src/pages/AdminUsersPage.tsx +15 -1
  36. package/src/pages/AdminWaitlistPage.tsx +156 -0
  37. package/src/pages/RegisterPage.tsx +91 -9
  38. package/src/routes/AdminPromoCodesRoute.tsx +24 -0
  39. package/src/routes/AdminWaitlistRoute.tsx +24 -0
  40. package/src/routes/index.ts +2 -0
  41. package/src/ssr-utils.tsx +32 -12
  42. package/src/stores/chatStore.ts +5 -0
  43. package/dist/favicon.svg +0 -11
  44. package/dist/index.html +0 -17
  45. package/dist/logo.svg +0 -12
@@ -0,0 +1,42 @@
1
+ let mockConfig: { app?: { basePath?: string | null } } | null = null;
2
+
3
+ async function importUseBasePath() {
4
+ vi.resetModules();
5
+ vi.doMock('../../contexts/ConfigContext.js', () => ({
6
+ useConfig: () => mockConfig,
7
+ }));
8
+ return import('../useBasePath.js');
9
+ }
10
+
11
+ async function importUseAppPath() {
12
+ vi.resetModules();
13
+ vi.doMock('react', () => ({
14
+ useCallback: (fn: unknown) => fn,
15
+ }));
16
+ vi.doMock('../../contexts/ConfigContext.js', () => ({
17
+ useConfig: () => mockConfig,
18
+ }));
19
+ return import('../useAppPath.js');
20
+ }
21
+
22
+ test('useBasePath defaults to /chat when empty', async () => {
23
+ mockConfig = { app: { basePath: '' } };
24
+ const { useBasePath } = await importUseBasePath();
25
+
26
+ expect(useBasePath()).toBe('/chat');
27
+ });
28
+
29
+ test('useBasePath returns configured basePath', async () => {
30
+ mockConfig = { app: { basePath: '/custom' } };
31
+ const { useBasePath } = await importUseBasePath();
32
+
33
+ expect(useBasePath()).toBe('/custom');
34
+ });
35
+
36
+ test('useAppPath builds paths with /chat default', async () => {
37
+ mockConfig = { app: { basePath: '' } };
38
+ const { useAppPath } = await importUseAppPath();
39
+
40
+ const appPath = useAppPath();
41
+ expect(appPath('/settings')).toBe('/chat/settings');
42
+ });
package/src/index.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  // Import styles for CSS extraction during build
2
2
  import './styles/index.css';
3
+ import './components/tool-renderers';
3
4
 
4
5
  // ClientOnly and Loading Skeletons for SSR-safe rendering
5
6
  export { ClientOnly } from './components/ClientOnly';
@@ -65,13 +66,15 @@ export { default as AdminDashboardPage } from './pages/AdminDashboardPage';
65
66
  export { default as AdminUsersPage } from './pages/AdminUsersPage';
66
67
  export { default as AdminTeamsPage } from './pages/AdminTeamsPage';
67
68
  export { default as AdminTeamPage } from './pages/AdminTeamPage';
69
+ export { default as AdminWaitlistPage } from './pages/AdminWaitlistPage';
70
+ export { default as AdminPromoCodesPage } from './pages/AdminPromoCodesPage';
68
71
 
69
72
  // ============================================
70
73
  // ChatProviders - Wraps the chat app with all required providers
71
74
  // Use in React Router routes that render the chat interface
72
75
  // ============================================
73
76
  import React from 'react';
74
- import type { AppConfig } from '@chaaskit/shared';
77
+ import type { PublicAppConfig } from '@chaaskit/shared';
75
78
  import { AuthProvider as Auth } from './contexts/AuthContext';
76
79
  import { ThemeProvider as Theme } from './contexts/ThemeContext';
77
80
  import { ConfigProvider as Config } from './contexts/ConfigContext';
@@ -84,7 +87,7 @@ export interface ChatProvidersProps {
84
87
  * Initial config to use immediately, avoiding a flash of default values.
85
88
  * Pass the config from SSR loaders to prevent "Welcome to AI Chat" flash.
86
89
  */
87
- initialConfig?: AppConfig;
90
+ initialConfig?: PublicAppConfig;
88
91
  }
89
92
 
90
93
  /**
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import { Link, useLocation } from 'react-router';
3
- import { Users, Building2, X, LayoutDashboard } from 'lucide-react';
3
+ import { Users, Building2, X, LayoutDashboard, Mail, Tag } from 'lucide-react';
4
4
  import { api } from '../utils/api';
5
5
  import { useConfig } from '../contexts/ConfigContext';
6
6
  import { useAppPath } from '../hooks/useAppPath';
@@ -145,6 +145,20 @@ export default function AdminDashboardPage() {
145
145
  Teams
146
146
  </Link>
147
147
  )}
148
+ <Link
149
+ to={appPath('/admin/waitlist')}
150
+ 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"
151
+ >
152
+ <Mail size={16} />
153
+ Waitlist
154
+ </Link>
155
+ <Link
156
+ to={appPath('/admin/promo-codes')}
157
+ 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"
158
+ >
159
+ <Tag size={16} />
160
+ Promo Codes
161
+ </Link>
148
162
  </div>
149
163
 
150
164
  {/* Stats Cards */}
@@ -0,0 +1,378 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Link } from 'react-router';
3
+ import { LayoutDashboard, Users, Building2, X, Mail, Tag } from 'lucide-react';
4
+ import { useConfig } from '../contexts/ConfigContext';
5
+ import { useAppPath } from '../hooks/useAppPath';
6
+ import { api } from '../utils/api';
7
+ import type {
8
+ AdminPromoCode,
9
+ AdminPromoCodesResponse,
10
+ AdminCreatePromoCodeRequest,
11
+ AdminCreatePromoCodeResponse,
12
+ AdminUpdatePromoCodeRequest,
13
+ AdminUpdatePromoCodeResponse,
14
+ } from '@chaaskit/shared';
15
+
16
+ export default function AdminPromoCodesPage() {
17
+ const config = useConfig();
18
+ const appPath = useAppPath();
19
+ const [promoCodes, setPromoCodes] = useState<AdminPromoCode[]>([]);
20
+ const [isLoading, setIsLoading] = useState(true);
21
+ const [error, setError] = useState('');
22
+ const [success, setSuccess] = useState('');
23
+ const [search, setSearch] = useState('');
24
+ const [statusFilter, setStatusFilter] = useState<'all' | 'active' | 'expired' | 'scheduled'>('all');
25
+ const [copiedCode, setCopiedCode] = useState<string | null>(null);
26
+
27
+ const [form, setForm] = useState({
28
+ code: '',
29
+ credits: 10,
30
+ maxUses: 100,
31
+ startsAt: '',
32
+ endsAt: '',
33
+ creditsExpiresAt: '',
34
+ });
35
+
36
+ async function loadPromoCodes() {
37
+ setIsLoading(true);
38
+ setError('');
39
+
40
+ try {
41
+ const params = new URLSearchParams();
42
+ if (search.trim()) {
43
+ params.set('search', search.trim());
44
+ }
45
+ if (statusFilter !== 'all') {
46
+ params.set('status', statusFilter);
47
+ }
48
+ const query = params.toString();
49
+ const response = await api.get<AdminPromoCodesResponse>(`/api/admin/promo-codes${query ? `?${query}` : ''}`);
50
+ setPromoCodes(response.promoCodes);
51
+ } catch (err) {
52
+ setError(err instanceof Error ? err.message : 'Failed to load promo codes');
53
+ } finally {
54
+ setIsLoading(false);
55
+ }
56
+ }
57
+
58
+ useEffect(() => {
59
+ loadPromoCodes();
60
+ }, [search, statusFilter]);
61
+
62
+ async function handleCreatePromoCode(e: React.FormEvent) {
63
+ e.preventDefault();
64
+ setError('');
65
+ setSuccess('');
66
+
67
+ try {
68
+ const payload: AdminCreatePromoCodeRequest = {
69
+ code: form.code.trim(),
70
+ credits: Number(form.credits),
71
+ maxUses: Number(form.maxUses),
72
+ startsAt: form.startsAt || undefined,
73
+ endsAt: form.endsAt || undefined,
74
+ creditsExpiresAt: form.creditsExpiresAt || undefined,
75
+ };
76
+
77
+ const response = await api.post<AdminCreatePromoCodeResponse>('/api/admin/promo-codes', payload);
78
+ setPromoCodes((prev) => [response.promoCode, ...prev]);
79
+ setSuccess(`Promo code ${response.promoCode.code} created`);
80
+ setForm({
81
+ code: '',
82
+ credits: 10,
83
+ maxUses: 100,
84
+ startsAt: '',
85
+ endsAt: '',
86
+ creditsExpiresAt: '',
87
+ });
88
+ } catch (err) {
89
+ setError(err instanceof Error ? err.message : 'Failed to create promo code');
90
+ }
91
+ }
92
+
93
+ const promoEnabled = config.credits?.enabled && config.credits?.promoEnabled !== false;
94
+
95
+ async function handleDeactivate(promo: AdminPromoCode) {
96
+ setError('');
97
+ setSuccess('');
98
+ try {
99
+ const response = await api.patch<AdminUpdatePromoCodeResponse>(`/api/admin/promo-codes/${promo.id}`, {
100
+ endsAt: new Date().toISOString(),
101
+ } satisfies AdminUpdatePromoCodeRequest);
102
+ setPromoCodes((prev) => prev.map((p) => (p.id === promo.id ? response.promoCode : p)));
103
+ setSuccess(`Promo code ${promo.code} deactivated`);
104
+ } catch (err) {
105
+ setError(err instanceof Error ? err.message : 'Failed to deactivate promo code');
106
+ }
107
+ }
108
+
109
+ async function handleReactivate(promo: AdminPromoCode) {
110
+ setError('');
111
+ setSuccess('');
112
+ try {
113
+ const response = await api.patch<AdminUpdatePromoCodeResponse>(`/api/admin/promo-codes/${promo.id}`, {
114
+ endsAt: null,
115
+ } satisfies AdminUpdatePromoCodeRequest);
116
+ setPromoCodes((prev) => prev.map((p) => (p.id === promo.id ? response.promoCode : p)));
117
+ setSuccess(`Promo code ${promo.code} reactivated`);
118
+ } catch (err) {
119
+ setError(err instanceof Error ? err.message : 'Failed to reactivate promo code');
120
+ }
121
+ }
122
+
123
+ async function handleCopy(code: string) {
124
+ try {
125
+ await navigator.clipboard.writeText(code);
126
+ setCopiedCode(code);
127
+ setTimeout(() => setCopiedCode((current) => (current === code ? null : current)), 1500);
128
+ } catch (err) {
129
+ setError(err instanceof Error ? err.message : 'Failed to copy code');
130
+ }
131
+ }
132
+
133
+ return (
134
+ <div className="min-h-screen bg-background p-4 sm:p-8">
135
+ <div className="mx-auto max-w-6xl">
136
+ <div className="flex items-center justify-between mb-4">
137
+ <h1 className="text-xl sm:text-2xl font-bold text-text-primary">Admin</h1>
138
+ <Link
139
+ to={appPath('/')}
140
+ className="flex items-center justify-center rounded-lg p-2 text-text-muted hover:text-text-primary hover:bg-background-secondary"
141
+ aria-label="Close"
142
+ >
143
+ <X size={20} />
144
+ </Link>
145
+ </div>
146
+
147
+ <div className="flex items-center gap-2 mb-6 sm:mb-8">
148
+ <Link
149
+ to={appPath('/admin')}
150
+ 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"
151
+ >
152
+ <LayoutDashboard size={16} />
153
+ Overview
154
+ </Link>
155
+ <Link
156
+ to={appPath('/admin/users')}
157
+ 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"
158
+ >
159
+ <Users size={16} />
160
+ Users
161
+ </Link>
162
+ {config.teams?.enabled && (
163
+ <Link
164
+ to={appPath('/admin/teams')}
165
+ 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"
166
+ >
167
+ <Building2 size={16} />
168
+ Teams
169
+ </Link>
170
+ )}
171
+ <Link
172
+ to={appPath('/admin/waitlist')}
173
+ 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"
174
+ >
175
+ <Mail size={16} />
176
+ Waitlist
177
+ </Link>
178
+ <span className="flex items-center gap-1.5 rounded-full bg-primary px-4 py-2 text-sm font-medium text-white">
179
+ <Tag size={16} />
180
+ Promo Codes
181
+ </span>
182
+ </div>
183
+
184
+ {!promoEnabled && (
185
+ <div className="mb-6 rounded-lg bg-warning/10 p-4 text-sm text-warning">
186
+ Promo codes are disabled in config. Enable `credits.promoEnabled` to use this page.
187
+ </div>
188
+ )}
189
+
190
+ {error && (
191
+ <div className="mb-6 rounded-lg bg-error/10 p-4 text-sm text-error">
192
+ {error}
193
+ </div>
194
+ )}
195
+
196
+ {success && (
197
+ <div className="mb-6 rounded-lg bg-success/10 p-4 text-sm text-success">
198
+ {success}
199
+ </div>
200
+ )}
201
+
202
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
203
+ <div className="lg:col-span-1 rounded-lg bg-background-secondary p-4">
204
+ <h2 className="text-base font-semibold text-text-primary mb-4">Create Promo Code</h2>
205
+ <form onSubmit={handleCreatePromoCode} className="space-y-3">
206
+ <div>
207
+ <label className="block text-sm font-medium text-text-primary" htmlFor="promo-code">
208
+ Code
209
+ </label>
210
+ <input
211
+ id="promo-code"
212
+ value={form.code}
213
+ onChange={(e) => setForm((prev) => ({ ...prev, code: e.target.value }))}
214
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
215
+ placeholder="WELCOME10"
216
+ required
217
+ />
218
+ </div>
219
+ <div className="grid grid-cols-2 gap-3">
220
+ <div>
221
+ <label className="block text-sm font-medium text-text-primary" htmlFor="promo-credits">
222
+ Credits
223
+ </label>
224
+ <input
225
+ id="promo-credits"
226
+ type="number"
227
+ min={1}
228
+ value={form.credits}
229
+ onChange={(e) => setForm((prev) => ({ ...prev, credits: Number(e.target.value) }))}
230
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
231
+ required
232
+ />
233
+ </div>
234
+ <div>
235
+ <label className="block text-sm font-medium text-text-primary" htmlFor="promo-uses">
236
+ Max Uses
237
+ </label>
238
+ <input
239
+ id="promo-uses"
240
+ type="number"
241
+ min={1}
242
+ value={form.maxUses}
243
+ onChange={(e) => setForm((prev) => ({ ...prev, maxUses: Number(e.target.value) }))}
244
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
245
+ required
246
+ />
247
+ </div>
248
+ </div>
249
+ <div>
250
+ <label className="block text-sm font-medium text-text-primary" htmlFor="promo-start">
251
+ Starts At (optional)
252
+ </label>
253
+ <input
254
+ id="promo-start"
255
+ type="datetime-local"
256
+ value={form.startsAt}
257
+ onChange={(e) => setForm((prev) => ({ ...prev, startsAt: e.target.value }))}
258
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
259
+ />
260
+ </div>
261
+ <div>
262
+ <label className="block text-sm font-medium text-text-primary" htmlFor="promo-end">
263
+ Ends At (optional)
264
+ </label>
265
+ <input
266
+ id="promo-end"
267
+ type="datetime-local"
268
+ value={form.endsAt}
269
+ onChange={(e) => setForm((prev) => ({ ...prev, endsAt: e.target.value }))}
270
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
271
+ />
272
+ </div>
273
+ <div>
274
+ <label className="block text-sm font-medium text-text-primary" htmlFor="promo-expires">
275
+ Credits Expire At (optional)
276
+ </label>
277
+ <input
278
+ id="promo-expires"
279
+ type="datetime-local"
280
+ value={form.creditsExpiresAt}
281
+ onChange={(e) => setForm((prev) => ({ ...prev, creditsExpiresAt: e.target.value }))}
282
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-3 py-2 text-text-primary focus:border-primary focus:outline-none"
283
+ />
284
+ </div>
285
+ <button
286
+ type="submit"
287
+ disabled={!promoEnabled}
288
+ className="w-full rounded-lg bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover disabled:opacity-50"
289
+ >
290
+ Create promo code
291
+ </button>
292
+ </form>
293
+ </div>
294
+
295
+ <div className="lg:col-span-2 rounded-lg bg-background-secondary overflow-hidden">
296
+ <div className="px-4 py-3 bg-background flex flex-wrap items-center gap-3">
297
+ <input
298
+ type="text"
299
+ placeholder="Search code..."
300
+ value={search}
301
+ onChange={(e) => setSearch(e.target.value)}
302
+ className="flex-1 min-w-[160px] rounded-lg border border-input-border bg-input-background px-3 py-2 text-sm text-text-primary focus:border-primary focus:outline-none"
303
+ />
304
+ <select
305
+ value={statusFilter}
306
+ onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
307
+ className="rounded-lg border border-input-border bg-input-background px-3 py-2 text-sm text-text-primary focus:border-primary focus:outline-none"
308
+ >
309
+ <option value="all">All</option>
310
+ <option value="active">Active</option>
311
+ <option value="scheduled">Scheduled</option>
312
+ <option value="expired">Expired</option>
313
+ </select>
314
+ </div>
315
+ <div className="hidden md:block px-4 py-3 bg-background">
316
+ <div className="grid grid-cols-12 gap-4 text-sm font-medium text-text-muted">
317
+ <div className="col-span-3">Code</div>
318
+ <div className="col-span-2">Credits</div>
319
+ <div className="col-span-2">Uses</div>
320
+ <div className="col-span-2">Active</div>
321
+ <div className="col-span-3">Actions</div>
322
+ </div>
323
+ </div>
324
+
325
+ <div className="divide-y divide-background">
326
+ {isLoading ? (
327
+ <div className="px-4 py-8 text-center text-text-muted">Loading...</div>
328
+ ) : promoCodes.length === 0 ? (
329
+ <div className="px-4 py-8 text-center text-text-muted">No promo codes yet</div>
330
+ ) : (
331
+ promoCodes.map((promo) => {
332
+ const now = new Date();
333
+ const startsAt = promo.startsAt ? new Date(promo.startsAt) : null;
334
+ const endsAt = promo.endsAt ? new Date(promo.endsAt) : null;
335
+ const active = (!startsAt || startsAt <= now) && (!endsAt || endsAt >= now);
336
+
337
+ return (
338
+ <div key={promo.id} className="px-4 py-3 hover:bg-background/50">
339
+ <div className="grid grid-cols-1 md:grid-cols-12 gap-2 md:gap-4 text-sm">
340
+ <div className="md:col-span-3 font-medium text-text-primary flex items-center gap-2">
341
+ <span>{promo.code}</span>
342
+ <button
343
+ type="button"
344
+ onClick={() => handleCopy(promo.code)}
345
+ className="text-xs text-primary hover:underline"
346
+ >
347
+ {copiedCode === promo.code ? 'Copied' : 'Copy'}
348
+ </button>
349
+ </div>
350
+ <div className="md:col-span-2 text-text-secondary">{promo.credits}</div>
351
+ <div className="md:col-span-2 text-text-secondary">
352
+ {promo.redeemedCount} / {promo.maxUses}
353
+ </div>
354
+ <div className={`md:col-span-2 ${active ? 'text-success' : 'text-text-muted'}`}>
355
+ {active ? 'Active' : 'Inactive'}
356
+ </div>
357
+ <div className="md:col-span-3 text-text-muted flex items-center gap-2">
358
+ <span>{new Date(promo.createdAt).toLocaleDateString()}</span>
359
+ <button
360
+ type="button"
361
+ onClick={() => (active ? handleDeactivate(promo) : handleReactivate(promo))}
362
+ className="rounded-lg border border-input-border px-2 py-1 text-xs text-text-primary hover:bg-background"
363
+ >
364
+ {active ? 'Deactivate' : 'Reactivate'}
365
+ </button>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ );
370
+ })
371
+ )}
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </div>
376
+ </div>
377
+ );
378
+ }
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import { Link, useParams } from 'react-router';
3
- import { LayoutDashboard, X, Users, Building2 } from 'lucide-react';
3
+ import { LayoutDashboard, X, Users, Building2, Mail, Tag } from 'lucide-react';
4
4
  import { useAppPath } from '../hooks/useAppPath';
5
5
  import { api } from '../utils/api';
6
6
  import type { AdminTeamDetails } from '@chaaskit/shared';
@@ -86,6 +86,20 @@ export default function AdminTeamPage() {
86
86
  <Building2 size={16} />
87
87
  Teams
88
88
  </Link>
89
+ <Link
90
+ to={appPath('/admin/waitlist')}
91
+ 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"
92
+ >
93
+ <Mail size={16} />
94
+ Waitlist
95
+ </Link>
96
+ <Link
97
+ to={appPath('/admin/promo-codes')}
98
+ 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"
99
+ >
100
+ <Tag size={16} />
101
+ Promo Codes
102
+ </Link>
89
103
  </div>
90
104
 
91
105
  <div className="rounded-lg bg-error/10 p-4 text-sm text-error">
@@ -142,6 +156,20 @@ export default function AdminTeamPage() {
142
156
  <Building2 size={16} />
143
157
  Teams
144
158
  </Link>
159
+ <Link
160
+ to={appPath('/admin/waitlist')}
161
+ 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"
162
+ >
163
+ <Mail size={16} />
164
+ Waitlist
165
+ </Link>
166
+ <Link
167
+ to={appPath('/admin/promo-codes')}
168
+ 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"
169
+ >
170
+ <Tag size={16} />
171
+ Promo Codes
172
+ </Link>
145
173
  </div>
146
174
 
147
175
  {/* Team Header */}
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useCallback } from 'react';
2
2
  import { Link } from 'react-router';
3
- import { LayoutDashboard, X, Users, Building2 } from 'lucide-react';
3
+ import { LayoutDashboard, X, Users, Building2, Mail, Tag } from 'lucide-react';
4
4
  import { useAppPath } from '../hooks/useAppPath';
5
5
  import { api } from '../utils/api';
6
6
  import type { AdminTeam, AdminTeamsResponse } from '@chaaskit/shared';
@@ -91,6 +91,20 @@ export default function AdminTeamsPage() {
91
91
  <Building2 size={16} />
92
92
  Teams
93
93
  </Link>
94
+ <Link
95
+ to={appPath('/admin/waitlist')}
96
+ 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"
97
+ >
98
+ <Mail size={16} />
99
+ Waitlist
100
+ </Link>
101
+ <Link
102
+ to={appPath('/admin/promo-codes')}
103
+ 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"
104
+ >
105
+ <Tag size={16} />
106
+ Promo Codes
107
+ </Link>
94
108
  </div>
95
109
 
96
110
  {error && (
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useCallback } from 'react';
2
2
  import { Link } from 'react-router';
3
- import { LayoutDashboard, X, Users, Building2 } from 'lucide-react';
3
+ import { LayoutDashboard, X, Users, Building2, Mail, Tag } from 'lucide-react';
4
4
  import { useConfig } from '../contexts/ConfigContext';
5
5
  import { useAppPath } from '../hooks/useAppPath';
6
6
  import { api } from '../utils/api';
@@ -134,6 +134,20 @@ export default function AdminUsersPage() {
134
134
  Teams
135
135
  </Link>
136
136
  )}
137
+ <Link
138
+ to={appPath('/admin/waitlist')}
139
+ 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"
140
+ >
141
+ <Mail size={16} />
142
+ Waitlist
143
+ </Link>
144
+ <Link
145
+ to={appPath('/admin/promo-codes')}
146
+ 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"
147
+ >
148
+ <Tag size={16} />
149
+ Promo Codes
150
+ </Link>
137
151
  </div>
138
152
 
139
153
  {error && (