@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,314 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { Link, useNavigate, useSearchParams } from 'react-router';
3
+ import { Check, Loader2, Users, User } from 'lucide-react';
4
+ import { useAuth } from '../contexts/AuthContext';
5
+ import { useConfig } from '../contexts/ConfigContext';
6
+ import { useTeam } from '../contexts/TeamContext';
7
+
8
+ interface Plan {
9
+ id: string;
10
+ name: string;
11
+ description?: string;
12
+ type: 'free' | 'monthly' | 'credits';
13
+ scope: 'personal' | 'team' | 'both';
14
+ priceUSD: number;
15
+ monthlyMessageLimit?: number;
16
+ }
17
+
18
+ export default function PricingPage() {
19
+ const { user } = useAuth();
20
+ const config = useConfig();
21
+ const navigate = useNavigate();
22
+ const [searchParams] = useSearchParams();
23
+ const { teams } = useTeam();
24
+
25
+ const [plans, setPlans] = useState<Plan[]>([]);
26
+ const [isLoading, setIsLoading] = useState(true);
27
+ const [checkoutLoading, setCheckoutLoading] = useState<string | null>(null);
28
+ const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
29
+ const [error, setError] = useState('');
30
+
31
+ // Get teamId from query params if present
32
+ const teamIdFromParams = searchParams.get('teamId');
33
+
34
+ useEffect(() => {
35
+ loadPlans();
36
+ }, []);
37
+
38
+ useEffect(() => {
39
+ if (teamIdFromParams && teams.length > 0) {
40
+ const team = teams.find((t) => t.id === teamIdFromParams);
41
+ if (team && (team.role === 'owner' || team.role === 'admin')) {
42
+ setSelectedTeamId(teamIdFromParams);
43
+ }
44
+ }
45
+ }, [teamIdFromParams, teams]);
46
+
47
+ async function loadPlans() {
48
+ try {
49
+ const response = await fetch('/api/payments/plans');
50
+ if (response.ok) {
51
+ const data = await response.json();
52
+ setPlans(data.plans);
53
+ }
54
+ } catch (err) {
55
+ console.error('Failed to load plans:', err);
56
+ } finally {
57
+ setIsLoading(false);
58
+ }
59
+ }
60
+
61
+ async function handleCheckout(planId: string) {
62
+ if (!user) {
63
+ navigate('/login?redirect=/pricing');
64
+ return;
65
+ }
66
+
67
+ setError('');
68
+ setCheckoutLoading(planId);
69
+
70
+ try {
71
+ const response = await fetch('/api/payments/checkout', {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ credentials: 'include',
75
+ body: JSON.stringify({
76
+ planId,
77
+ teamId: selectedTeamId || undefined,
78
+ }),
79
+ });
80
+
81
+ if (!response.ok) {
82
+ const data = await response.json();
83
+ throw new Error(data.error || 'Failed to start checkout');
84
+ }
85
+
86
+ const { url } = await response.json();
87
+ window.location.href = url;
88
+ } catch (err) {
89
+ setError(err instanceof Error ? err.message : 'Checkout failed');
90
+ } finally {
91
+ setCheckoutLoading(null);
92
+ }
93
+ }
94
+
95
+ // Get available teams for team billing (where user is admin/owner)
96
+ const adminTeams = teams.filter((t) => t.role === 'owner' || t.role === 'admin');
97
+
98
+ // Filter plans based on selected context (personal vs team)
99
+ const filteredPlans = plans.filter((plan) => {
100
+ if (selectedTeamId) {
101
+ return plan.scope === 'team' || plan.scope === 'both';
102
+ }
103
+ return plan.scope === 'personal' || plan.scope === 'both';
104
+ });
105
+
106
+ if (isLoading) {
107
+ return (
108
+ <div className="min-h-screen bg-background flex items-center justify-center">
109
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
110
+ </div>
111
+ );
112
+ }
113
+
114
+ return (
115
+ <div className="min-h-screen bg-background py-12 px-4">
116
+ <div className="mx-auto max-w-5xl">
117
+ {/* Header */}
118
+ <div className="text-center mb-12">
119
+ <h1 className="text-3xl font-bold text-text-primary mb-4">
120
+ Choose Your Plan
121
+ </h1>
122
+ <p className="text-text-secondary max-w-2xl mx-auto">
123
+ Select the plan that best fits your needs. Upgrade or downgrade anytime.
124
+ </p>
125
+
126
+ {/* Team/Personal Toggle */}
127
+ {user && adminTeams.length > 0 && (
128
+ <div className="mt-8 inline-flex items-center gap-4 p-1 bg-background-secondary rounded-lg">
129
+ <button
130
+ onClick={() => setSelectedTeamId(null)}
131
+ className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${
132
+ !selectedTeamId
133
+ ? 'bg-primary text-white'
134
+ : 'text-text-secondary hover:text-text-primary'
135
+ }`}
136
+ >
137
+ <User size={16} />
138
+ Personal
139
+ </button>
140
+ <div className="relative">
141
+ <select
142
+ value={selectedTeamId || ''}
143
+ onChange={(e) => setSelectedTeamId(e.target.value || null)}
144
+ className={`appearance-none flex items-center gap-2 px-4 py-2 pr-8 rounded-md text-sm font-medium transition-colors cursor-pointer ${
145
+ selectedTeamId
146
+ ? 'bg-primary text-white'
147
+ : 'bg-transparent text-text-secondary hover:text-text-primary'
148
+ }`}
149
+ >
150
+ <option value="">Team billing</option>
151
+ {adminTeams.map((team) => (
152
+ <option key={team.id} value={team.id}>
153
+ {team.name}
154
+ </option>
155
+ ))}
156
+ </select>
157
+ <Users
158
+ size={16}
159
+ className={`absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none ${
160
+ selectedTeamId ? 'text-white' : 'text-text-secondary'
161
+ }`}
162
+ />
163
+ </div>
164
+ </div>
165
+ )}
166
+ </div>
167
+
168
+ {error && (
169
+ <div className="mb-8 rounded-lg bg-error/10 p-4 text-center text-sm text-error">
170
+ {error}
171
+ </div>
172
+ )}
173
+
174
+ {/* Plans Grid */}
175
+ <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
176
+ {filteredPlans
177
+ .filter((plan) => plan.type !== 'credits')
178
+ .map((plan) => (
179
+ <div
180
+ key={plan.id}
181
+ className={`rounded-xl border p-6 ${
182
+ plan.id === 'pro'
183
+ ? 'border-primary bg-primary/5'
184
+ : 'border-border bg-background-secondary'
185
+ }`}
186
+ >
187
+ {/* Plan Badge */}
188
+ <div className="flex items-center justify-between mb-4">
189
+ <span
190
+ className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium ${
191
+ plan.scope === 'team'
192
+ ? 'bg-secondary/10 text-secondary'
193
+ : plan.scope === 'both'
194
+ ? 'bg-primary/10 text-primary'
195
+ : 'bg-background text-text-muted'
196
+ }`}
197
+ >
198
+ {plan.scope === 'team' && <Users size={12} />}
199
+ {plan.scope === 'personal' && <User size={12} />}
200
+ {plan.scope === 'both' && (
201
+ <>
202
+ <User size={12} />
203
+ <span>/</span>
204
+ <Users size={12} />
205
+ </>
206
+ )}
207
+ {plan.scope === 'team'
208
+ ? 'Team'
209
+ : plan.scope === 'both'
210
+ ? 'Personal or Team'
211
+ : 'Personal'}
212
+ </span>
213
+ {plan.id === 'pro' && (
214
+ <span className="rounded-full bg-primary px-2.5 py-0.5 text-xs font-medium text-white">
215
+ Popular
216
+ </span>
217
+ )}
218
+ </div>
219
+
220
+ {/* Plan Name & Price */}
221
+ <h3 className="text-xl font-bold text-text-primary">{plan.name}</h3>
222
+ <div className="mt-2 flex items-baseline gap-1">
223
+ <span className="text-3xl font-bold text-text-primary">
224
+ ${plan.priceUSD}
225
+ </span>
226
+ {plan.type === 'monthly' && (
227
+ <span className="text-text-muted">/month</span>
228
+ )}
229
+ </div>
230
+
231
+ {plan.description && (
232
+ <p className="mt-2 text-sm text-text-secondary">{plan.description}</p>
233
+ )}
234
+
235
+ {/* Features */}
236
+ <ul className="mt-6 space-y-3">
237
+ <li className="flex items-start gap-2 text-sm text-text-secondary">
238
+ <Check size={16} className="mt-0.5 text-success shrink-0" />
239
+ <span>
240
+ {plan.monthlyMessageLimit === -1
241
+ ? 'Unlimited messages'
242
+ : `${plan.monthlyMessageLimit?.toLocaleString()} messages/month`}
243
+ </span>
244
+ </li>
245
+ {plan.type === 'monthly' && (
246
+ <li className="flex items-start gap-2 text-sm text-text-secondary">
247
+ <Check size={16} className="mt-0.5 text-success shrink-0" />
248
+ <span>Priority support</span>
249
+ </li>
250
+ )}
251
+ {plan.scope === 'team' && (
252
+ <li className="flex items-start gap-2 text-sm text-text-secondary">
253
+ <Check size={16} className="mt-0.5 text-success shrink-0" />
254
+ <span>Shared message pool for all team members</span>
255
+ </li>
256
+ )}
257
+ </ul>
258
+
259
+ {/* CTA Button */}
260
+ <button
261
+ onClick={() => handleCheckout(plan.id)}
262
+ disabled={plan.type === 'free' || checkoutLoading === plan.id}
263
+ className={`mt-6 w-full rounded-lg py-2.5 text-sm font-medium transition-colors ${
264
+ plan.type === 'free'
265
+ ? 'bg-background-secondary text-text-muted cursor-default'
266
+ : plan.id === 'pro'
267
+ ? 'bg-primary text-white hover:bg-primary-hover'
268
+ : 'bg-background text-text-primary border border-border hover:bg-background-secondary'
269
+ } disabled:opacity-50`}
270
+ >
271
+ {checkoutLoading === plan.id ? (
272
+ <Loader2 className="h-4 w-4 animate-spin mx-auto" />
273
+ ) : plan.type === 'free' ? (
274
+ 'Current Plan'
275
+ ) : (
276
+ 'Get Started'
277
+ )}
278
+ </button>
279
+ </div>
280
+ ))}
281
+ </div>
282
+
283
+ {/* Credits Section */}
284
+ {filteredPlans.some((p) => p.type === 'credits') && (
285
+ <div className="mt-12 rounded-xl border border-border bg-background-secondary p-6">
286
+ <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
287
+ <div>
288
+ <h3 className="text-lg font-semibold text-text-primary">
289
+ Need more messages?
290
+ </h3>
291
+ <p className="text-sm text-text-secondary mt-1">
292
+ Buy credits to use when you exceed your monthly limit.
293
+ </p>
294
+ </div>
295
+ <Link
296
+ to={user ? '/settings' : '/login'}
297
+ className="inline-flex items-center justify-center rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white hover:bg-primary-hover"
298
+ >
299
+ Buy Credits
300
+ </Link>
301
+ </div>
302
+ </div>
303
+ )}
304
+
305
+ {/* Back Link */}
306
+ <div className="mt-8 text-center">
307
+ <Link to="/" className="text-sm text-text-muted hover:text-text-primary">
308
+ Back to {config.app.name}
309
+ </Link>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ );
314
+ }
@@ -0,0 +1,65 @@
1
+ import { useConfig } from '../contexts/ConfigContext';
2
+
3
+ export default function PrivacyPage() {
4
+ const config = useConfig();
5
+
6
+ return (
7
+ <div className="min-h-screen bg-background py-12">
8
+ <div className="mx-auto max-w-3xl px-4">
9
+ <h1 className="mb-8 text-3xl font-bold text-text-primary">
10
+ Privacy Policy
11
+ </h1>
12
+
13
+ <div className="prose prose-gray dark:prose-invert">
14
+ <p className="text-text-secondary">
15
+ Last updated: {new Date().toLocaleDateString()}
16
+ </p>
17
+
18
+ <h2 className="mt-8 text-xl font-semibold text-text-primary">
19
+ 1. Information We Collect
20
+ </h2>
21
+ <p className="text-text-secondary">
22
+ {config.app.name} collects information you provide directly to us,
23
+ such as when you create an account, send messages, or contact us for
24
+ support.
25
+ </p>
26
+
27
+ <h2 className="mt-8 text-xl font-semibold text-text-primary">
28
+ 2. How We Use Your Information
29
+ </h2>
30
+ <p className="text-text-secondary">
31
+ We use the information we collect to provide, maintain, and improve
32
+ our services, and to communicate with you about your account and our
33
+ services.
34
+ </p>
35
+
36
+ <h2 className="mt-8 text-xl font-semibold text-text-primary">
37
+ 3. Data Retention
38
+ </h2>
39
+ <p className="text-text-secondary">
40
+ We retain your information for as long as your account is active or
41
+ as needed to provide you services. You can request deletion of your
42
+ data at any time.
43
+ </p>
44
+
45
+ <h2 className="mt-8 text-xl font-semibold text-text-primary">
46
+ 4. Data Security
47
+ </h2>
48
+ <p className="text-text-secondary">
49
+ We implement appropriate technical and organizational measures to
50
+ protect your personal information against unauthorized access,
51
+ alteration, disclosure, or destruction.
52
+ </p>
53
+
54
+ <h2 className="mt-8 text-xl font-semibold text-text-primary">
55
+ 5. Contact Us
56
+ </h2>
57
+ <p className="text-text-secondary">
58
+ If you have any questions about this Privacy Policy, please contact
59
+ us.
60
+ </p>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,153 @@
1
+ import { useState } from 'react';
2
+ import { Link, useNavigate } from 'react-router';
3
+ import { useAuth } from '../contexts/AuthContext';
4
+ import { useConfig } from '../contexts/ConfigContext';
5
+ import { useTheme } from '../contexts/ThemeContext';
6
+ import { useAppPath } from '../hooks/useAppPath';
7
+
8
+ export default function RegisterPage() {
9
+ const navigate = useNavigate();
10
+ const appPath = useAppPath();
11
+ const { register } = useAuth();
12
+ const config = useConfig();
13
+ const { theme } = useTheme();
14
+
15
+ const [name, setName] = useState('');
16
+ const [email, setEmail] = useState('');
17
+ const [password, setPassword] = useState('');
18
+ const [error, setError] = useState('');
19
+ const [isLoading, setIsLoading] = useState(false);
20
+
21
+ async function handleSubmit(e: React.FormEvent) {
22
+ e.preventDefault();
23
+ setError('');
24
+
25
+ if (password.length < 8) {
26
+ setError('Password must be at least 8 characters');
27
+ return;
28
+ }
29
+
30
+ setIsLoading(true);
31
+
32
+ try {
33
+ const { requiresVerification } = await register(email, password, name || undefined);
34
+ if (requiresVerification) {
35
+ navigate('/verify-email');
36
+ } else {
37
+ navigate(appPath('/'));
38
+ }
39
+ } catch (err) {
40
+ setError(err instanceof Error ? err.message : 'Registration failed');
41
+ } finally {
42
+ setIsLoading(false);
43
+ }
44
+ }
45
+
46
+ return (
47
+ <div className="flex min-h-screen items-center justify-center bg-background p-4">
48
+ <div className="w-full max-w-md">
49
+ <div className="mb-8 text-center">
50
+ {config.ui.logo && (
51
+ <img
52
+ src={typeof config.ui.logo === 'string' ? config.ui.logo : (theme === 'dark' ? config.ui.logo.dark : config.ui.logo.light)}
53
+ alt={config.app.name}
54
+ className="mx-auto mb-4 h-16 w-16 rounded-lg object-contain"
55
+ />
56
+ )}
57
+ <h1 className="text-3xl font-bold text-text-primary">Create account</h1>
58
+ <p className="mt-2 text-text-secondary">
59
+ Get started with {config.app.name}
60
+ </p>
61
+ </div>
62
+
63
+ {error && (
64
+ <div className="mb-4 rounded-lg bg-error/10 p-3 text-sm text-error">
65
+ {error}
66
+ </div>
67
+ )}
68
+
69
+ <form onSubmit={handleSubmit} className="space-y-4">
70
+ <div>
71
+ <label
72
+ htmlFor="name"
73
+ className="block text-sm font-medium text-text-primary"
74
+ >
75
+ Name (optional)
76
+ </label>
77
+ <input
78
+ type="text"
79
+ id="name"
80
+ value={name}
81
+ onChange={(e) => setName(e.target.value)}
82
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
83
+ />
84
+ </div>
85
+
86
+ <div>
87
+ <label
88
+ htmlFor="email"
89
+ className="block text-sm font-medium text-text-primary"
90
+ >
91
+ Email
92
+ </label>
93
+ <input
94
+ type="email"
95
+ id="email"
96
+ value={email}
97
+ onChange={(e) => setEmail(e.target.value)}
98
+ required
99
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
100
+ />
101
+ </div>
102
+
103
+ <div>
104
+ <label
105
+ htmlFor="password"
106
+ className="block text-sm font-medium text-text-primary"
107
+ >
108
+ Password
109
+ </label>
110
+ <input
111
+ type="password"
112
+ id="password"
113
+ value={password}
114
+ onChange={(e) => setPassword(e.target.value)}
115
+ required
116
+ minLength={8}
117
+ className="mt-1 w-full rounded-lg border border-input-border bg-input-background px-4 py-2 text-text-primary focus:border-primary focus:outline-none"
118
+ />
119
+ <p className="mt-1 text-xs text-text-muted">
120
+ Must be at least 8 characters
121
+ </p>
122
+ </div>
123
+
124
+ <button
125
+ type="submit"
126
+ disabled={isLoading}
127
+ className="w-full rounded-lg bg-primary px-4 py-2 font-medium text-white hover:bg-primary-hover disabled:opacity-50"
128
+ >
129
+ {isLoading ? 'Creating account...' : 'Create account'}
130
+ </button>
131
+ </form>
132
+
133
+ <p className="mt-6 text-center text-sm text-text-secondary">
134
+ Already have an account?{' '}
135
+ <Link to="/login" className="text-primary hover:underline">
136
+ Sign in
137
+ </Link>
138
+ </p>
139
+
140
+ <p className="mt-4 text-center text-xs text-text-muted">
141
+ By creating an account, you agree to our{' '}
142
+ <Link to={config.legal.termsOfServiceUrl} className="text-primary hover:underline">
143
+ Terms of Service
144
+ </Link>{' '}
145
+ and{' '}
146
+ <Link to={config.legal.privacyPolicyUrl} className="text-primary hover:underline">
147
+ Privacy Policy
148
+ </Link>
149
+ </p>
150
+ </div>
151
+ </div>
152
+ );
153
+ }