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, type DidType } from '../lib/api' 5 import { _ } from '../lib/i18n' 6 7 const STORAGE_KEY = 'tranquil_pds_pending_verification' 8 9 let handle = $state('') 10 let email = $state('') 11 let password = $state('') 12 let confirmPassword = $state('') 13 let inviteCode = $state('') 14 let verificationChannel = $state<VerificationChannel>('email') 15 let discordId = $state('') 16 let telegramUsername = $state('') 17 let signalNumber = $state('') 18 let didType = $state<DidType>('plc') 19 let externalDid = $state('') 20 let submitting = $state(false) 21 let error = $state<string | null>(null) 22 let serverInfo = $state<{ 23 availableUserDomains: string[] 24 inviteCodeRequired: boolean 25 } | null>(null) 26 let loadingServerInfo = $state(true) 27 let serverInfoLoaded = false 28 29 const auth = getAuthState() 30 31 $effect(() => { 32 if (!serverInfoLoaded) { 33 serverInfoLoaded = true 34 loadServerInfo() 35 } 36 }) 37 38 async function loadServerInfo() { 39 try { 40 serverInfo = await api.describeServer() 41 } catch (e) { 42 console.error('Failed to load server info:', e) 43 } finally { 44 loadingServerInfo = false 45 } 46 } 47 48 let handleHasDot = $derived(handle.includes('.')) 49 50 function validateForm(): string | null { 51 if (!handle.trim()) return $_('register.validation.handleRequired') 52 if (handle.includes('.')) return $_('register.validation.handleNoDots') 53 if (!password) return $_('register.validation.passwordRequired') 54 if (password.length < 8) return $_('register.validation.passwordLength') 55 if (password !== confirmPassword) return $_('register.validation.passwordsMismatch') 56 if (serverInfo?.inviteCodeRequired && !inviteCode.trim()) { 57 return $_('register.validation.inviteCodeRequired') 58 } 59 if (didType === 'web-external') { 60 if (!externalDid.trim()) return $_('register.validation.externalDidRequired') 61 if (!externalDid.trim().startsWith('did:web:')) return $_('register.validation.externalDidFormat') 62 } 63 switch (verificationChannel) { 64 case 'email': 65 if (!email.trim()) return $_('register.validation.emailRequired') 66 break 67 case 'discord': 68 if (!discordId.trim()) return $_('register.validation.discordIdRequired') 69 break 70 case 'telegram': 71 if (!telegramUsername.trim()) return $_('register.validation.telegramRequired') 72 break 73 case 'signal': 74 if (!signalNumber.trim()) return $_('register.validation.signalRequired') 75 break 76 } 77 return null 78 } 79 80 async function handleSubmit(e: Event) { 81 e.preventDefault() 82 const validationError = validateForm() 83 if (validationError) { 84 error = validationError 85 return 86 } 87 submitting = true 88 error = null 89 try { 90 const result = await register({ 91 handle: handle.trim(), 92 email: email.trim(), 93 password, 94 inviteCode: inviteCode.trim() || undefined, 95 didType, 96 did: didType === 'web-external' ? externalDid.trim() : undefined, 97 verificationChannel, 98 discordId: discordId.trim() || undefined, 99 telegramUsername: telegramUsername.trim() || undefined, 100 signalNumber: signalNumber.trim() || undefined, 101 }) 102 if (result.verificationRequired) { 103 localStorage.setItem(STORAGE_KEY, JSON.stringify({ 104 did: result.did, 105 handle: result.handle, 106 channel: result.verificationChannel, 107 })) 108 navigate('/verify') 109 } else { 110 navigate('/dashboard') 111 } 112 } catch (err: any) { 113 if (err instanceof ApiError) { 114 error = err.message || 'Registration failed' 115 } else if (err instanceof Error) { 116 error = err.message || 'Registration failed' 117 } else { 118 error = 'Registration failed' 119 } 120 } finally { 121 submitting = false 122 } 123 } 124 125 let fullHandle = $derived(() => { 126 if (!handle.trim()) return '' 127 if (handle.includes('.')) return handle.trim() 128 const domain = serverInfo?.availableUserDomains?.[0] 129 if (domain) return `${handle.trim()}.${domain}` 130 return handle.trim() 131 }) 132</script> 133 134<div class="register-page"> 135 {#if error} 136 <div class="message error">{error}</div> 137 {/if} 138 139 <h1>{$_('register.title')}</h1> 140 <p class="subtitle">{$_('register.subtitle')}</p> 141 142 {#if loadingServerInfo} 143 <p class="loading">{$_('common.loading')}</p> 144 {:else} 145 <form onsubmit={(e) => { e.preventDefault(); handleSubmit(e); }}> 146 <div class="field"> 147 <label for="handle">{$_('register.handle')}</label> 148 <input 149 id="handle" 150 type="text" 151 bind:value={handle} 152 placeholder={$_('register.handlePlaceholder')} 153 disabled={submitting} 154 required 155 /> 156 {#if handleHasDot} 157 <p class="hint warning">{$_('register.handleDotWarning')}</p> 158 {:else if fullHandle()} 159 <p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p> 160 {/if} 161 </div> 162 163 <div class="field"> 164 <label for="password">{$_('register.password')}</label> 165 <input 166 id="password" 167 type="password" 168 bind:value={password} 169 placeholder={$_('register.passwordPlaceholder')} 170 disabled={submitting} 171 required 172 minlength="8" 173 /> 174 </div> 175 176 <div class="field"> 177 <label for="confirm-password">{$_('register.confirmPassword')}</label> 178 <input 179 id="confirm-password" 180 type="password" 181 bind:value={confirmPassword} 182 placeholder={$_('register.confirmPasswordPlaceholder')} 183 disabled={submitting} 184 required 185 /> 186 </div> 187 188 <fieldset class="section-fieldset"> 189 <legend>{$_('register.identityType')}</legend> 190 <p class="section-hint">{$_('register.identityHint')}</p> 191 192 <div class="radio-group"> 193 <label class="radio-label"> 194 <input type="radio" name="didType" value="plc" bind:group={didType} disabled={submitting} /> 195 <span class="radio-content"> 196 <strong>{$_('register.didPlc')}</strong> {$_('register.didPlcRecommended')} 197 <span class="radio-hint">{$_('register.didPlcHint')}</span> 198 </span> 199 </label> 200 201 <label class="radio-label"> 202 <input type="radio" name="didType" value="web" bind:group={didType} disabled={submitting} /> 203 <span class="radio-content"> 204 <strong>{$_('register.didWeb')}</strong> 205 <span class="radio-hint">{$_('register.didWebHint')}</span> 206 </span> 207 </label> 208 209 <label class="radio-label"> 210 <input type="radio" name="didType" value="web-external" bind:group={didType} disabled={submitting} /> 211 <span class="radio-content"> 212 <strong>{$_('register.didWebBYOD')}</strong> 213 <span class="radio-hint">{$_('register.didWebBYODHint')}</span> 214 </span> 215 </label> 216 </div> 217 218 {#if didType === 'web'} 219 <div class="warning-box"> 220 <strong>{$_('register.didWebWarningTitle')}</strong> 221 <ul> 222 <li><strong>{$_('register.didWebWarning1')}</strong> {$_('register.didWebWarning1Detail', { values: { did: `did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}` } })}</li> 223 <li><strong>{$_('register.didWebWarning2')}</strong> {$_('register.didWebWarning2Detail')}</li> 224 <li><strong>{$_('register.didWebWarning3')}</strong> {$_('register.didWebWarning3Detail')}</li> 225 <li><strong>{$_('register.didWebWarning4')}</strong> {$_('register.didWebWarning4Detail')}</li> 226 </ul> 227 </div> 228 {/if} 229 230 {#if didType === 'web-external'} 231 <div class="field"> 232 <label for="external-did">{$_('register.externalDid')}</label> 233 <input 234 id="external-did" 235 type="text" 236 bind:value={externalDid} 237 placeholder={$_('register.externalDidPlaceholder')} 238 disabled={submitting} 239 required 240 /> 241 <p class="hint">{$_('register.externalDidHint')}</p> 242 </div> 243 {/if} 244 </fieldset> 245 246 <fieldset class="section-fieldset"> 247 <legend>{$_('register.contactMethod')}</legend> 248 <p class="section-hint">{$_('register.contactMethodHint')}</p> 249 250 <div class="field"> 251 <label for="verification-channel">{$_('register.verificationMethod')}</label> 252 <select id="verification-channel" bind:value={verificationChannel} disabled={submitting}> 253 <option value="email">{$_('register.email')}</option> 254 <option value="discord">{$_('register.discord')}</option> 255 <option value="telegram">{$_('register.telegram')}</option> 256 <option value="signal">{$_('register.signal')}</option> 257 </select> 258 </div> 259 260 {#if verificationChannel === 'email'} 261 <div class="field"> 262 <label for="email">{$_('register.emailAddress')}</label> 263 <input 264 id="email" 265 type="email" 266 bind:value={email} 267 placeholder={$_('register.emailPlaceholder')} 268 disabled={submitting} 269 required 270 /> 271 </div> 272 {:else if verificationChannel === 'discord'} 273 <div class="field"> 274 <label for="discord-id">{$_('register.discordId')}</label> 275 <input 276 id="discord-id" 277 type="text" 278 bind:value={discordId} 279 placeholder={$_('register.discordIdPlaceholder')} 280 disabled={submitting} 281 required 282 /> 283 <p class="hint">{$_('register.discordIdHint')}</p> 284 </div> 285 {:else if verificationChannel === 'telegram'} 286 <div class="field"> 287 <label for="telegram-username">{$_('register.telegramUsername')}</label> 288 <input 289 id="telegram-username" 290 type="text" 291 bind:value={telegramUsername} 292 placeholder={$_('register.telegramUsernamePlaceholder')} 293 disabled={submitting} 294 required 295 /> 296 </div> 297 {:else if verificationChannel === 'signal'} 298 <div class="field"> 299 <label for="signal-number">{$_('register.signalNumber')}</label> 300 <input 301 id="signal-number" 302 type="tel" 303 bind:value={signalNumber} 304 placeholder={$_('register.signalNumberPlaceholder')} 305 disabled={submitting} 306 required 307 /> 308 <p class="hint">{$_('register.signalNumberHint')}</p> 309 </div> 310 {/if} 311 </fieldset> 312 313 {#if serverInfo?.inviteCodeRequired} 314 <div class="field"> 315 <label for="invite-code">{$_('register.inviteCode')} <span class="required">{$_('register.inviteCodeRequired')}</span></label> 316 <input 317 id="invite-code" 318 type="text" 319 bind:value={inviteCode} 320 placeholder={$_('register.inviteCodePlaceholder')} 321 disabled={submitting} 322 required 323 /> 324 </div> 325 {/if} 326 327 <button type="submit" disabled={submitting}> 328 {submitting ? $_('register.creating') : $_('register.createButton')} 329 </button> 330 </form> 331 332 <p class="link-text"> 333 {$_('register.alreadyHaveAccount')} <a href="#/login">{$_('register.signIn')}</a> 334 </p> 335 <p class="link-text"> 336 {$_('register.wantPasswordless')} <a href="#/register-passkey">{$_('register.createPasskeyAccount')}</a> 337 </p> 338 {/if} 339</div> 340 341<style> 342 .register-page { 343 max-width: var(--width-sm); 344 margin: var(--space-9) auto; 345 padding: var(--space-7); 346 } 347 348 h1 { 349 margin: 0 0 var(--space-3) 0; 350 } 351 352 .subtitle { 353 color: var(--text-secondary); 354 margin: 0 0 var(--space-7) 0; 355 } 356 357 .loading { 358 text-align: center; 359 color: var(--text-secondary); 360 } 361 362 form { 363 display: flex; 364 flex-direction: column; 365 gap: var(--space-5); 366 } 367 368 .required { 369 color: var(--error-text); 370 } 371 372 .section-fieldset { 373 border: 1px solid var(--border-color); 374 border-radius: var(--radius-lg); 375 padding: var(--space-5); 376 } 377 378 .section-fieldset legend { 379 font-weight: var(--font-semibold); 380 padding: 0 var(--space-3); 381 } 382 383 .section-hint { 384 font-size: var(--text-sm); 385 color: var(--text-secondary); 386 margin: 0 0 var(--space-5) 0; 387 } 388 389 .radio-group { 390 display: flex; 391 flex-direction: column; 392 gap: var(--space-4); 393 } 394 395 .radio-label { 396 display: flex; 397 align-items: flex-start; 398 gap: var(--space-3); 399 cursor: pointer; 400 font-size: var(--text-base); 401 font-weight: var(--font-normal); 402 margin-bottom: 0; 403 } 404 405 .radio-label input[type="radio"] { 406 margin-top: var(--space-1); 407 width: auto; 408 } 409 410 .radio-content { 411 display: flex; 412 flex-direction: column; 413 gap: var(--space-1); 414 } 415 416 .radio-hint { 417 font-size: var(--text-xs); 418 color: var(--text-secondary); 419 } 420 421 .warning-box { 422 margin-top: var(--space-5); 423 padding: var(--space-5); 424 background: var(--warning-bg); 425 border: 1px solid var(--warning-border); 426 border-radius: var(--radius-lg); 427 font-size: var(--text-sm); 428 } 429 430 .warning-box strong { 431 color: var(--warning-text); 432 } 433 434 .warning-box ul { 435 margin: var(--space-4) 0 0 0; 436 padding-left: var(--space-5); 437 } 438 439 .warning-box li { 440 margin-bottom: var(--space-3); 441 line-height: var(--leading-normal); 442 } 443 444 .warning-box li:last-child { 445 margin-bottom: 0; 446 } 447 448 button[type="submit"] { 449 margin-top: var(--space-3); 450 } 451 452 .link-text { 453 text-align: center; 454 margin-top: var(--space-6); 455 color: var(--text-secondary); 456 } 457 458 .link-text a { 459 color: var(--accent); 460 } 461</style>