astro-tractstack 2.0.31 → 2.0.33

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.
@@ -1,449 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import { useFormState } from '@/hooks/useFormState';
3
- import {
4
- convertToLocalState,
5
- convertToBackendFormat,
6
- validateTenantRegistration,
7
- tenantStateIntercept,
8
- } from '@/utils/api/tenantHelpers';
9
- import { checkTenantCapacity, provisionTenant } from '@/utils/api/tenantConfig';
10
- import { TractStackAPI } from '@/utils/api';
11
- import UnsavedChangesBar from '@/components/form/UnsavedChangesBar';
12
- import StringInput from '@/components/form/StringInput';
13
- import BooleanToggle from '@/components/form/BooleanToggle';
14
- import type { TenantCapacity } from '@/types/multiTenant';
15
- import type { TenantRegistrationState } from '@/types/multiTenant';
16
-
17
- interface RegistrationFormProps {
18
- isInitMode?: boolean;
19
- onSuccess?: (tenantId?: string) => void;
20
- onCapacityFull?: () => void;
21
- }
22
-
23
- export default function RegistrationForm({
24
- isInitMode = false,
25
- onSuccess,
26
- onCapacityFull,
27
- }: RegistrationFormProps) {
28
- const [capacity, setCapacity] = useState<TenantCapacity | null>(null);
29
- const [loadingCapacity, setLoadingCapacity] = useState(!isInitMode);
30
- const [capacityError, setCapacityError] = useState<string | null>(null);
31
-
32
- // Modal state for tenant preparation
33
- const [showPreparationModal, setShowPreparationModal] = useState(false);
34
- const [activationToken, setActivationToken] = useState<string>('');
35
- const [preparingTenantId, setPreparingTenantId] = useState<string>('');
36
-
37
- // Load capacity information on mount
38
- useEffect(() => {
39
- if (isInitMode) {
40
- return;
41
- }
42
-
43
- const loadCapacity = async () => {
44
- try {
45
- const capacityData = await checkTenantCapacity('default');
46
- setCapacity(capacityData);
47
-
48
- // Check if at capacity
49
- if (!capacityData.available) {
50
- onCapacityFull?.();
51
- }
52
- } catch (error) {
53
- setCapacityError(
54
- error instanceof Error ? error.message : 'Failed to load capacity'
55
- );
56
- } finally {
57
- setLoadingCapacity(false);
58
- }
59
- };
60
-
61
- loadCapacity();
62
- }, [onCapacityFull, isInitMode]);
63
-
64
- const initialState: TenantRegistrationState = convertToLocalState();
65
-
66
- const formState = useFormState({
67
- initialData: initialState,
68
- validator: (data) =>
69
- validateTenantRegistration(data, undefined, isInitMode),
70
- interceptor: tenantStateIntercept,
71
- onSave: async (data) => {
72
- try {
73
- if (isInitMode) {
74
- const api = new TractStackAPI('default');
75
- const setupData = {
76
- adminEmail: data.email.trim(),
77
- adminPassword: data.adminPassword.trim(),
78
- ...(data.tursoEnabled && {
79
- tursoDatabaseURL: data.tursoDatabaseURL.trim(),
80
- tursoAuthToken: data.tursoAuthToken.trim(),
81
- }),
82
- };
83
-
84
- const response = await api.post(
85
- '/api/v1/setup/initialize',
86
- setupData
87
- );
88
- if (!response.success) {
89
- throw new Error(response.error || 'Setup failed');
90
- }
91
- window.location.href = '/storykeep';
92
- return data;
93
- } else {
94
- const backendData = convertToBackendFormat(data);
95
- const result = await provisionTenant('default', backendData);
96
-
97
- // Store the activation token and tenant ID for the modal
98
- setActivationToken(result.token);
99
- setPreparingTenantId(data.tenantId);
100
- setShowPreparationModal(true);
101
-
102
- return data;
103
- }
104
- } catch (error) {
105
- console.error('Tenant provisioning failed:', error);
106
- throw error;
107
- }
108
- },
109
- });
110
-
111
- const { state, updateField, errors } = formState;
112
-
113
- // Handle activation button click in modal
114
- const handleActivate = () => {
115
- if (activationToken && preparingTenantId) {
116
- // Check if we're in dev mode
117
- const isDev = import.meta.env.DEV;
118
-
119
- if (isDev) {
120
- // In dev mode, navigate to local activation page with tenant ID param
121
- window.location.href = `/sandbox/activate?token=${activationToken}&tenantId=${preparingTenantId}`;
122
- } else {
123
- // In production, use the full subdomain URL
124
- window.location.href = `https://${preparingTenantId}.sandbox.freewebpress.com/sandbox/activate?token=${activationToken}`;
125
- }
126
- }
127
- };
128
-
129
- // Handle modal close
130
- const handleModalClose = () => {
131
- setShowPreparationModal(false);
132
- onSuccess?.(preparingTenantId);
133
- };
134
-
135
- if (loadingCapacity) {
136
- return (
137
- <div className="mx-auto max-w-2xl p-6">
138
- <div className="animate-pulse">
139
- <div className="mb-4 h-8 rounded bg-gray-200"></div>
140
- <div className="mb-2 h-4 rounded bg-gray-200"></div>
141
- <div className="mb-4 h-4 rounded bg-gray-200"></div>
142
- </div>
143
- </div>
144
- );
145
- }
146
-
147
- if (capacityError) {
148
- return (
149
- <div className="mx-auto max-w-2xl p-6">
150
- <div className="rounded-lg border border-red-200 bg-red-50 p-4">
151
- <h3 className="mb-2 text-lg font-bold text-red-800">
152
- Unable to Load Registration
153
- </h3>
154
- <p className="text-red-700">{capacityError}</p>
155
- </div>
156
- </div>
157
- );
158
- }
159
-
160
- if (!capacity && !isInitMode) {
161
- return (
162
- <div className="mx-auto max-w-2xl p-6">
163
- <div className="rounded-lg border border-yellow-200 bg-yellow-50 p-4">
164
- <h3 className="mb-2 text-lg font-bold text-yellow-800">
165
- Registration Currently Unavailable
166
- </h3>
167
- <p className="text-yellow-700">
168
- We've reached our current capacity. Please check back later for
169
- availability.
170
- </p>
171
- </div>
172
- </div>
173
- );
174
- }
175
-
176
- return (
177
- <>
178
- <div className="mx-auto max-w-2xl p-6" style={{ paddingBottom: '112px' }}>
179
- <div className="rounded-lg bg-white p-8 shadow-lg">
180
- <div className="mb-8">
181
- <div className="h-16">
182
- <img
183
- src="/brand/logo.svg"
184
- className="pointer-events-none mx-auto h-full"
185
- alt="Logo"
186
- />
187
- </div>
188
-
189
- <h2 className="mb-2 mt-8 text-2xl font-bold text-gray-900">
190
- {isInitMode ? `Install Tract Stack` : `Try Tract Stack`}
191
- </h2>
192
- {!isInitMode && (
193
- <p className="text-gray-600">
194
- Set up your free sandbox environment to try TractStack.
195
- </p>
196
- )}
197
- {!isInitMode && capacity && (
198
- <div className="mt-4 rounded-lg bg-orange-50 p-4">
199
- <div className="flex">
200
- <div className="flex-shrink-0">
201
- <svg
202
- className="h-5 w-5 text-orange-400"
203
- viewBox="0 0 20 20"
204
- fill="currentColor"
205
- >
206
- <path
207
- fillRule="evenodd"
208
- d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
209
- clipRule="evenodd"
210
- />
211
- </svg>
212
- </div>
213
- <div className="ml-3">
214
- <p className="text-sm text-orange-700">
215
- {capacity.availableSlots} of {capacity.maxTenants} slots
216
- remaining
217
- </p>
218
- </div>
219
- </div>
220
- </div>
221
- )}
222
- </div>
223
-
224
- <div className="space-y-6">
225
- {/* Tenant ID field - hidden in init mode */}
226
- {!isInitMode && (
227
- <div>
228
- <label className="mb-1 block text-sm font-bold text-gray-700">
229
- Tenant ID *
230
- </label>
231
- <StringInput
232
- value={state.tenantId}
233
- onChange={(value) => updateField('tenantId', value)}
234
- placeholder="my-awesome-tenant"
235
- error={errors.tenantId}
236
- />
237
- <p className="mt-1 text-sm text-gray-500">
238
- 3-12 characters, lowercase letters, numbers, and dashes only
239
- </p>
240
- </div>
241
- )}
242
-
243
- {/* Admin Password */}
244
- <div>
245
- <label className="mb-1 block text-sm font-bold text-gray-700">
246
- Admin Password *
247
- </label>
248
- <StringInput
249
- value={state.adminPassword}
250
- onChange={(value) => updateField('adminPassword', value)}
251
- type="password"
252
- placeholder="Strong password for admin access"
253
- error={errors.adminPassword}
254
- />
255
- <p className="mt-1 text-sm text-gray-500">Minimum 8 characters</p>
256
- </div>
257
-
258
- {/* Confirm Password */}
259
- <div>
260
- <label className="mb-1 block text-sm font-bold text-gray-700">
261
- Confirm Password *
262
- </label>
263
- <StringInput
264
- value={state.confirmPassword}
265
- onChange={(value) => updateField('confirmPassword', value)}
266
- type="password"
267
- placeholder="Confirm your admin password"
268
- error={errors.confirmPassword}
269
- />
270
- </div>
271
-
272
- {/* Name */}
273
- <div>
274
- <label className="mb-1 block text-sm font-bold text-gray-700">
275
- Your Name *
276
- </label>
277
- <StringInput
278
- value={state.name}
279
- onChange={(value) => updateField('name', value)}
280
- placeholder="John Doe"
281
- error={errors.name}
282
- />
283
- </div>
284
-
285
- {/* Email */}
286
- <div>
287
- <label className="mb-1 block text-sm font-bold text-gray-700">
288
- Email Address *
289
- </label>
290
- <StringInput
291
- value={state.email}
292
- onChange={(value) => updateField('email', value)}
293
- type="email"
294
- placeholder="susie@amazing.com"
295
- error={errors.email}
296
- />
297
- <p className="mt-1 text-sm text-gray-500">
298
- {isInitMode
299
- ? `Used for password reset, etc.`
300
- : `You'll receive an activation email at this address`}
301
- </p>
302
- </div>
303
-
304
- {/* Database Configuration */}
305
- <div className="rounded-lg border border-gray-200 p-4">
306
- <div className="mb-4">
307
- <BooleanToggle
308
- value={state.tursoEnabled}
309
- onChange={(value) => updateField('tursoEnabled', value)}
310
- label="Enable Turso Database"
311
- />
312
- <p className="mt-2 text-sm text-gray-500">
313
- By default, your tenant will use SQLite3. Enable this option
314
- to use your own Turso database instead.
315
- </p>
316
- </div>
317
-
318
- {state.tursoEnabled && (
319
- <div className="space-y-4 rounded-lg bg-gray-50 p-4">
320
- <div>
321
- <label className="mb-1 block text-sm font-bold text-gray-700">
322
- Turso Database URL *
323
- </label>
324
- <StringInput
325
- value={state.tursoDatabaseURL}
326
- onChange={(value) =>
327
- updateField('tursoDatabaseURL', value)
328
- }
329
- placeholder="libsql://your-database.turso.io"
330
- error={errors.tursoDatabaseURL}
331
- />
332
- <p className="mt-1 text-sm text-gray-500">
333
- Must start with libsql://
334
- </p>
335
- </div>
336
-
337
- <div>
338
- <label className="mb-1 block text-sm font-bold text-gray-700">
339
- Turso Auth Token *
340
- </label>
341
- <StringInput
342
- value={state.tursoAuthToken}
343
- onChange={(value) => updateField('tursoAuthToken', value)}
344
- type="password"
345
- placeholder="Your Turso auth token"
346
- error={errors.tursoAuthToken}
347
- />
348
- </div>
349
- </div>
350
- )}
351
- </div>
352
- </div>
353
-
354
- <UnsavedChangesBar
355
- formState={formState}
356
- message={
357
- isInitMode
358
- ? `Install Tract Stack`
359
- : `Complete your tenant registration`
360
- }
361
- saveLabel={isInitMode ? `Install` : `Create Tenant`}
362
- cancelLabel="Clear Form"
363
- />
364
- </div>
365
- </div>
366
-
367
- {/* Tenant Preparation Modal */}
368
- {showPreparationModal && (
369
- <div className="fixed inset-0 z-50 overflow-y-auto">
370
- <div className="flex min-h-screen items-center justify-center p-4">
371
- <div
372
- className="fixed inset-0 bg-black opacity-25"
373
- onClick={handleModalClose}
374
- />
375
-
376
- <div className="relative w-full max-w-lg rounded-lg bg-white shadow-xl">
377
- <div className="p-6">
378
- <div className="mb-6 text-center">
379
- <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
380
- <svg
381
- className="h-6 w-6 text-green-600"
382
- fill="none"
383
- viewBox="0 0 24 24"
384
- strokeWidth="1.5"
385
- stroke="currentColor"
386
- >
387
- <path
388
- strokeLinecap="round"
389
- strokeLinejoin="round"
390
- d="M4.5 12.75l6 6 9-13.5"
391
- />
392
- </svg>
393
- </div>
394
- <h3 className="text-xl font-bold text-gray-900">
395
- Your TractStack is Being Prepared
396
- </h3>
397
- <p className="mt-2 text-sm text-gray-600">
398
- We've successfully provisioned your tenant{' '}
399
- <strong>{preparingTenantId}</strong>. Click the button below
400
- to activate your sandbox environment.
401
- </p>
402
- </div>
403
-
404
- <div className="mb-6 rounded-lg bg-orange-50 p-4">
405
- <div className="flex">
406
- <div className="flex-shrink-0">
407
- <svg
408
- className="h-5 w-5 text-orange-400"
409
- viewBox="0 0 20 20"
410
- fill="currentColor"
411
- >
412
- <path
413
- fillRule="evenodd"
414
- d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
415
- clipRule="evenodd"
416
- />
417
- </svg>
418
- </div>
419
- <div className="ml-3">
420
- <p className="text-sm text-orange-700">
421
- You'll also receive an email with this activation link
422
- at <strong>{state.email}</strong>
423
- </p>
424
- </div>
425
- </div>
426
- </div>
427
-
428
- <div className="flex justify-end space-x-3">
429
- <button
430
- onClick={handleModalClose}
431
- className="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-bold text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-cyan-500"
432
- >
433
- I'll Use the Email Link
434
- </button>
435
- <button
436
- onClick={handleActivate}
437
- className="rounded-md border border-transparent bg-cyan-600 px-4 py-2 text-sm font-bold text-white hover:bg-cyan-700 focus:outline-none focus:ring-2 focus:ring-cyan-500"
438
- >
439
- Activate Now
440
- </button>
441
- </div>
442
- </div>
443
- </div>
444
- </div>
445
- </div>
446
- )}
447
- </>
448
- );
449
- }