this repo has no description
1<script lang="ts">
2 import { register, getAuthState } from '../lib/auth.svelte'
3 import { navigate } from '../lib/router.svelte'
4 import { api, ApiError, type VerificationChannel } from '../lib/api'
5
6 const STORAGE_KEY = 'tranquil_pds_pending_verification'
7
8 let handle = $state('')
9 let email = $state('')
10 let password = $state('')
11 let confirmPassword = $state('')
12 let inviteCode = $state('')
13 let verificationChannel = $state<VerificationChannel>('email')
14 let discordId = $state('')
15 let telegramUsername = $state('')
16 let signalNumber = $state('')
17 let submitting = $state(false)
18 let error = $state<string | null>(null)
19 let serverInfo = $state<{
20 availableUserDomains: string[]
21 inviteCodeRequired: boolean
22 } | null>(null)
23 let loadingServerInfo = $state(true)
24 let serverInfoLoaded = false
25
26 const auth = getAuthState()
27
28 $effect(() => {
29 if (auth.session) {
30 navigate('/dashboard')
31 }
32 })
33
34 $effect(() => {
35 if (!serverInfoLoaded) {
36 serverInfoLoaded = true
37 loadServerInfo()
38 }
39 })
40
41 async function loadServerInfo() {
42 try {
43 serverInfo = await api.describeServer()
44 } catch (e) {
45 console.error('Failed to load server info:', e)
46 } finally {
47 loadingServerInfo = false
48 }
49 }
50
51 function validateForm(): string | null {
52 if (!handle.trim()) return 'Handle is required'
53 if (!password) return 'Password is required'
54 if (password.length < 8) return 'Password must be at least 8 characters'
55 if (password !== confirmPassword) return 'Passwords do not match'
56 if (serverInfo?.inviteCodeRequired && !inviteCode.trim()) {
57 return 'Invite code is required'
58 }
59 switch (verificationChannel) {
60 case 'email':
61 if (!email.trim()) return 'Email is required for email verification'
62 break
63 case 'discord':
64 if (!discordId.trim()) return 'Discord ID is required for Discord verification'
65 break
66 case 'telegram':
67 if (!telegramUsername.trim()) return 'Telegram username is required for Telegram verification'
68 break
69 case 'signal':
70 if (!signalNumber.trim()) return 'Phone number is required for Signal verification'
71 break
72 }
73 return null
74 }
75
76 async function handleSubmit(e: Event) {
77 e.preventDefault()
78 const validationError = validateForm()
79 if (validationError) {
80 error = validationError
81 return
82 }
83 submitting = true
84 error = null
85 try {
86 const result = await register({
87 handle: handle.trim(),
88 email: email.trim(),
89 password,
90 inviteCode: inviteCode.trim() || undefined,
91 verificationChannel,
92 discordId: discordId.trim() || undefined,
93 telegramUsername: telegramUsername.trim() || undefined,
94 signalNumber: signalNumber.trim() || undefined,
95 })
96 if (result.verificationRequired) {
97 localStorage.setItem(STORAGE_KEY, JSON.stringify({
98 did: result.did,
99 handle: result.handle,
100 channel: result.verificationChannel,
101 }))
102 navigate('/verify')
103 } else {
104 navigate('/dashboard')
105 }
106 } catch (err: any) {
107 if (err instanceof ApiError) {
108 error = err.message || 'Registration failed'
109 } else if (err instanceof Error) {
110 error = err.message || 'Registration failed'
111 } else {
112 error = 'Registration failed'
113 }
114 } finally {
115 submitting = false
116 }
117 }
118
119 let fullHandle = $derived(() => {
120 if (!handle.trim()) return ''
121 if (handle.includes('.')) return handle.trim()
122 const domain = serverInfo?.availableUserDomains?.[0]
123 if (domain) return `${handle.trim()}.${domain}`
124 return handle.trim()
125 })
126</script>
127<div class="register-container">
128 {#if error}
129 <div class="error">{error}</div>
130 {/if}
131 <h1>Create Account</h1>
132 <p class="subtitle">Create a new account on this PDS</p>
133 {#if loadingServerInfo}
134 <p class="loading">Loading...</p>
135 {:else}
136 <form onsubmit={(e) => { e.preventDefault(); handleSubmit(e); }}>
137 <div class="field">
138 <label for="handle">Handle</label>
139 <input
140 id="handle"
141 type="text"
142 bind:value={handle}
143 placeholder="yourname"
144 disabled={submitting}
145 required
146 />
147 {#if fullHandle()}
148 <p class="hint">Your full handle will be: @{fullHandle()}</p>
149 {/if}
150 </div>
151 <div class="field">
152 <label for="password">Password</label>
153 <input
154 id="password"
155 type="password"
156 bind:value={password}
157 placeholder="At least 8 characters"
158 disabled={submitting}
159 required
160 minlength="8"
161 />
162 </div>
163 <div class="field">
164 <label for="confirm-password">Confirm Password</label>
165 <input
166 id="confirm-password"
167 type="password"
168 bind:value={confirmPassword}
169 placeholder="Confirm your password"
170 disabled={submitting}
171 required
172 />
173 </div>
174 <fieldset class="verification-section">
175 <legend>Contact Method</legend>
176 <p class="section-hint">Choose how you'd like to verify your account and receive notifications. You only need one.</p>
177 <div class="field">
178 <label for="verification-channel">Verification Method</label>
179 <select
180 id="verification-channel"
181 bind:value={verificationChannel}
182 disabled={submitting}
183 >
184 <option value="email">Email</option>
185 <option value="discord">Discord</option>
186 <option value="telegram">Telegram</option>
187 <option value="signal">Signal</option>
188 </select>
189 </div>
190 {#if verificationChannel === 'email'}
191 <div class="field">
192 <label for="email">Email Address</label>
193 <input
194 id="email"
195 type="email"
196 bind:value={email}
197 placeholder="you@example.com"
198 disabled={submitting}
199 required
200 />
201 </div>
202 {:else if verificationChannel === 'discord'}
203 <div class="field">
204 <label for="discord-id">Discord User ID</label>
205 <input
206 id="discord-id"
207 type="text"
208 bind:value={discordId}
209 placeholder="Your Discord user ID"
210 disabled={submitting}
211 required
212 />
213 <p class="hint">Your numeric Discord user ID (enable Developer Mode to find it)</p>
214 </div>
215 {:else if verificationChannel === 'telegram'}
216 <div class="field">
217 <label for="telegram-username">Telegram Username</label>
218 <input
219 id="telegram-username"
220 type="text"
221 bind:value={telegramUsername}
222 placeholder="@yourusername"
223 disabled={submitting}
224 required
225 />
226 </div>
227 {:else if verificationChannel === 'signal'}
228 <div class="field">
229 <label for="signal-number">Signal Phone Number</label>
230 <input
231 id="signal-number"
232 type="tel"
233 bind:value={signalNumber}
234 placeholder="+1234567890"
235 disabled={submitting}
236 required
237 />
238 <p class="hint">Include country code (e.g., +1 for US)</p>
239 </div>
240 {/if}
241 </fieldset>
242 {#if serverInfo?.inviteCodeRequired}
243 <div class="field">
244 <label for="invite-code">Invite Code <span class="required">*</span></label>
245 <input
246 id="invite-code"
247 type="text"
248 bind:value={inviteCode}
249 placeholder="Enter your invite code"
250 disabled={submitting}
251 required
252 />
253 </div>
254 {/if}
255 <button type="submit" disabled={submitting}>
256 {submitting ? 'Creating account...' : 'Create Account'}
257 </button>
258 </form>
259 <p class="login-link">
260 Already have an account? <a href="#/login">Sign in</a>
261 </p>
262 {/if}
263</div>
264<style>
265 .register-container {
266 max-width: 400px;
267 margin: 4rem auto;
268 padding: 2rem;
269 }
270 h1 {
271 margin: 0 0 0.5rem 0;
272 }
273 .subtitle {
274 color: var(--text-secondary);
275 margin: 0 0 2rem 0;
276 }
277 .loading {
278 text-align: center;
279 color: var(--text-secondary);
280 }
281 form {
282 display: flex;
283 flex-direction: column;
284 gap: 1rem;
285 }
286 .field {
287 display: flex;
288 flex-direction: column;
289 gap: 0.25rem;
290 }
291 label {
292 font-size: 0.875rem;
293 font-weight: 500;
294 }
295 .required {
296 color: var(--error-text);
297 }
298 input, select {
299 padding: 0.75rem;
300 border: 1px solid var(--border-color-light);
301 border-radius: 4px;
302 font-size: 1rem;
303 background: var(--bg-input);
304 color: var(--text-primary);
305 }
306 input:focus, select:focus {
307 outline: none;
308 border-color: var(--accent);
309 }
310 .hint {
311 font-size: 0.75rem;
312 color: var(--text-secondary);
313 margin: 0.25rem 0 0 0;
314 }
315 .verification-section {
316 border: 1px solid var(--border-color-light);
317 border-radius: 6px;
318 padding: 1rem;
319 margin: 0.5rem 0;
320 }
321 .verification-section legend {
322 font-weight: 600;
323 padding: 0 0.5rem;
324 color: var(--text-primary);
325 }
326 .section-hint {
327 font-size: 0.8rem;
328 color: var(--text-secondary);
329 margin: 0 0 1rem 0;
330 }
331 button {
332 padding: 0.75rem;
333 background: var(--accent);
334 color: white;
335 border: none;
336 border-radius: 4px;
337 font-size: 1rem;
338 cursor: pointer;
339 margin-top: 0.5rem;
340 }
341 button:hover:not(:disabled) {
342 background: var(--accent-hover);
343 }
344 button:disabled {
345 opacity: 0.6;
346 cursor: not-allowed;
347 }
348 .error {
349 padding: 0.75rem;
350 background: var(--error-bg);
351 border: 1px solid var(--error-border);
352 border-radius: 4px;
353 color: var(--error-text);
354 }
355 .login-link {
356 text-align: center;
357 margin-top: 1.5rem;
358 color: var(--text-secondary);
359 }
360 .login-link a {
361 color: var(--accent);
362 }
363</style>