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>