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 <div class="migrate-callout"> 136 <div class="migrate-icon"></div> 137 <div class="migrate-content"> 138 <strong>{$_('register.migrateTitle')}</strong> 139 <p>{$_('register.migrateDescription')}</p> 140 <a href="https://pdsmoover.com/moover" target="_blank" rel="noopener" class="migrate-link"> 141 {$_('register.migrateLink')}142 </a> 143 </div> 144 </div> 145 146 {#if error} 147 <div class="message error">{error}</div> 148 {/if} 149 150 <h1>{$_('register.title')}</h1> 151 <p class="subtitle">{$_('register.subtitle')}</p> 152 153 {#if loadingServerInfo} 154 <p class="loading">{$_('common.loading')}</p> 155 {:else} 156 <form onsubmit={(e) => { e.preventDefault(); handleSubmit(e); }}> 157 <div class="field"> 158 <label for="handle">{$_('register.handle')}</label> 159 <input 160 id="handle" 161 type="text" 162 bind:value={handle} 163 placeholder={$_('register.handlePlaceholder')} 164 disabled={submitting} 165 required 166 /> 167 {#if handleHasDot} 168 <p class="hint warning">{$_('register.handleDotWarning')}</p> 169 {:else if fullHandle()} 170 <p class="hint">{$_('register.handleHint', { values: { handle: fullHandle() } })}</p> 171 {/if} 172 </div> 173 174 <div class="field"> 175 <label for="password">{$_('register.password')}</label> 176 <input 177 id="password" 178 type="password" 179 bind:value={password} 180 placeholder={$_('register.passwordPlaceholder')} 181 disabled={submitting} 182 required 183 minlength="8" 184 /> 185 </div> 186 187 <div class="field"> 188 <label for="confirm-password">{$_('register.confirmPassword')}</label> 189 <input 190 id="confirm-password" 191 type="password" 192 bind:value={confirmPassword} 193 placeholder={$_('register.confirmPasswordPlaceholder')} 194 disabled={submitting} 195 required 196 /> 197 </div> 198 199 <fieldset class="section-fieldset"> 200 <legend>{$_('register.identityType')}</legend> 201 <p class="section-hint">{$_('register.identityHint')}</p> 202 203 <div class="radio-group"> 204 <label class="radio-label"> 205 <input type="radio" name="didType" value="plc" bind:group={didType} disabled={submitting} /> 206 <span class="radio-content"> 207 <strong>{$_('register.didPlc')}</strong> {$_('register.didPlcRecommended')} 208 <span class="radio-hint">{$_('register.didPlcHint')}</span> 209 </span> 210 </label> 211 212 <label class="radio-label"> 213 <input type="radio" name="didType" value="web" bind:group={didType} disabled={submitting} /> 214 <span class="radio-content"> 215 <strong>{$_('register.didWeb')}</strong> 216 <span class="radio-hint">{$_('register.didWebHint')}</span> 217 </span> 218 </label> 219 220 <label class="radio-label"> 221 <input type="radio" name="didType" value="web-external" bind:group={didType} disabled={submitting} /> 222 <span class="radio-content"> 223 <strong>{$_('register.didWebBYOD')}</strong> 224 <span class="radio-hint">{$_('register.didWebBYODHint')}</span> 225 </span> 226 </label> 227 </div> 228 229 {#if didType === 'web'} 230 <div class="warning-box"> 231 <strong>{$_('register.didWebWarningTitle')}</strong> 232 <ul> 233 <li><strong>{$_('register.didWebWarning1')}</strong> {$_('register.didWebWarning1Detail', { values: { did: `did:web:yourhandle.${serverInfo?.availableUserDomains?.[0] || 'this-pds.com'}` } })}</li> 234 <li><strong>{$_('register.didWebWarning2')}</strong> {$_('register.didWebWarning2Detail')}</li> 235 <li><strong>{$_('register.didWebWarning3')}</strong> {$_('register.didWebWarning3Detail')}</li> 236 <li><strong>{$_('register.didWebWarning4')}</strong> {$_('register.didWebWarning4Detail')}</li> 237 </ul> 238 </div> 239 {/if} 240 241 {#if didType === 'web-external'} 242 <div class="field"> 243 <label for="external-did">{$_('register.externalDid')}</label> 244 <input 245 id="external-did" 246 type="text" 247 bind:value={externalDid} 248 placeholder={$_('register.externalDidPlaceholder')} 249 disabled={submitting} 250 required 251 /> 252 <p class="hint">{$_('register.externalDidHint')}</p> 253 </div> 254 {/if} 255 </fieldset> 256 257 <fieldset class="section-fieldset"> 258 <legend>{$_('register.contactMethod')}</legend> 259 <p class="section-hint">{$_('register.contactMethodHint')}</p> 260 261 <div class="field"> 262 <label for="verification-channel">{$_('register.verificationMethod')}</label> 263 <select id="verification-channel" bind:value={verificationChannel} disabled={submitting}> 264 <option value="email">{$_('register.email')}</option> 265 <option value="discord">{$_('register.discord')}</option> 266 <option value="telegram">{$_('register.telegram')}</option> 267 <option value="signal">{$_('register.signal')}</option> 268 </select> 269 </div> 270 271 {#if verificationChannel === 'email'} 272 <div class="field"> 273 <label for="email">{$_('register.emailAddress')}</label> 274 <input 275 id="email" 276 type="email" 277 bind:value={email} 278 placeholder={$_('register.emailPlaceholder')} 279 disabled={submitting} 280 required 281 /> 282 </div> 283 {:else if verificationChannel === 'discord'} 284 <div class="field"> 285 <label for="discord-id">{$_('register.discordId')}</label> 286 <input 287 id="discord-id" 288 type="text" 289 bind:value={discordId} 290 placeholder={$_('register.discordIdPlaceholder')} 291 disabled={submitting} 292 required 293 /> 294 <p class="hint">{$_('register.discordIdHint')}</p> 295 </div> 296 {:else if verificationChannel === 'telegram'} 297 <div class="field"> 298 <label for="telegram-username">{$_('register.telegramUsername')}</label> 299 <input 300 id="telegram-username" 301 type="text" 302 bind:value={telegramUsername} 303 placeholder={$_('register.telegramUsernamePlaceholder')} 304 disabled={submitting} 305 required 306 /> 307 </div> 308 {:else if verificationChannel === 'signal'} 309 <div class="field"> 310 <label for="signal-number">{$_('register.signalNumber')}</label> 311 <input 312 id="signal-number" 313 type="tel" 314 bind:value={signalNumber} 315 placeholder={$_('register.signalNumberPlaceholder')} 316 disabled={submitting} 317 required 318 /> 319 <p class="hint">{$_('register.signalNumberHint')}</p> 320 </div> 321 {/if} 322 </fieldset> 323 324 {#if serverInfo?.inviteCodeRequired} 325 <div class="field"> 326 <label for="invite-code">{$_('register.inviteCode')} <span class="required">{$_('register.inviteCodeRequired')}</span></label> 327 <input 328 id="invite-code" 329 type="text" 330 bind:value={inviteCode} 331 placeholder={$_('register.inviteCodePlaceholder')} 332 disabled={submitting} 333 required 334 /> 335 </div> 336 {/if} 337 338 <button type="submit" disabled={submitting}> 339 {submitting ? $_('register.creating') : $_('register.createButton')} 340 </button> 341 </form> 342 343 <p class="link-text"> 344 {$_('register.alreadyHaveAccount')} <a href="#/login">{$_('register.signIn')}</a> 345 </p> 346 <p class="link-text"> 347 {$_('register.wantPasswordless')} <a href="#/register-passkey">{$_('register.createPasskeyAccount')}</a> 348 </p> 349 {/if} 350</div> 351 352<style> 353 .register-page { 354 max-width: var(--width-sm); 355 margin: var(--space-9) auto; 356 padding: var(--space-7); 357 } 358 359 .migrate-callout { 360 display: flex; 361 gap: var(--space-4); 362 padding: var(--space-5); 363 background: var(--accent-muted); 364 border: 1px solid var(--accent); 365 border-radius: var(--radius-xl); 366 margin-bottom: var(--space-6); 367 } 368 369 .migrate-icon { 370 font-size: var(--text-2xl); 371 line-height: 1; 372 color: var(--accent); 373 } 374 375 .migrate-content { 376 flex: 1; 377 } 378 379 .migrate-content strong { 380 display: block; 381 color: var(--text-primary); 382 margin-bottom: var(--space-2); 383 } 384 385 .migrate-content p { 386 margin: 0 0 var(--space-3) 0; 387 font-size: var(--text-sm); 388 color: var(--text-secondary); 389 line-height: var(--leading-relaxed); 390 } 391 392 .migrate-link { 393 font-size: var(--text-sm); 394 font-weight: var(--font-medium); 395 color: var(--accent); 396 text-decoration: none; 397 } 398 399 .migrate-link:hover { 400 text-decoration: underline; 401 } 402 403 h1 { 404 margin: 0 0 var(--space-3) 0; 405 } 406 407 .subtitle { 408 color: var(--text-secondary); 409 margin: 0 0 var(--space-7) 0; 410 } 411 412 .loading { 413 text-align: center; 414 color: var(--text-secondary); 415 } 416 417 form { 418 display: flex; 419 flex-direction: column; 420 gap: var(--space-5); 421 } 422 423 .required { 424 color: var(--error-text); 425 } 426 427 .section-fieldset { 428 border: 1px solid var(--border-color); 429 border-radius: var(--radius-lg); 430 padding: var(--space-5); 431 } 432 433 .section-fieldset legend { 434 font-weight: var(--font-semibold); 435 padding: 0 var(--space-3); 436 } 437 438 .section-hint { 439 font-size: var(--text-sm); 440 color: var(--text-secondary); 441 margin: 0 0 var(--space-5) 0; 442 } 443 444 .radio-group { 445 display: flex; 446 flex-direction: column; 447 gap: var(--space-4); 448 } 449 450 .radio-label { 451 display: flex; 452 align-items: flex-start; 453 gap: var(--space-3); 454 cursor: pointer; 455 font-size: var(--text-base); 456 font-weight: var(--font-normal); 457 margin-bottom: 0; 458 } 459 460 .radio-label input[type="radio"] { 461 margin-top: var(--space-1); 462 width: auto; 463 } 464 465 .radio-content { 466 display: flex; 467 flex-direction: column; 468 gap: var(--space-1); 469 } 470 471 .radio-hint { 472 font-size: var(--text-xs); 473 color: var(--text-secondary); 474 } 475 476 .warning-box { 477 margin-top: var(--space-5); 478 padding: var(--space-5); 479 background: var(--warning-bg); 480 border: 1px solid var(--warning-border); 481 border-radius: var(--radius-lg); 482 font-size: var(--text-sm); 483 } 484 485 .warning-box strong { 486 color: var(--warning-text); 487 } 488 489 .warning-box ul { 490 margin: var(--space-4) 0 0 0; 491 padding-left: var(--space-5); 492 } 493 494 .warning-box li { 495 margin-bottom: var(--space-3); 496 line-height: var(--leading-normal); 497 } 498 499 .warning-box li:last-child { 500 margin-bottom: 0; 501 } 502 503 button[type="submit"] { 504 margin-top: var(--space-3); 505 } 506 507 .link-text { 508 text-align: center; 509 margin-top: var(--space-6); 510 color: var(--text-secondary); 511 } 512 513 .link-text a { 514 color: var(--accent); 515 } 516</style>