this repo has no description
1<script lang="ts"> 2 import { getAuthState, logout } from '../lib/auth.svelte' 3 import { navigate } from '../lib/router.svelte' 4 import { api, ApiError } from '../lib/api' 5 6 const auth = getAuthState() 7 8 let message = $state<{ type: 'success' | 'error'; text: string } | null>(null) 9 10 let emailLoading = $state(false) 11 let newEmail = $state('') 12 let emailToken = $state('') 13 let emailTokenRequired = $state(false) 14 15 let handleLoading = $state(false) 16 let newHandle = $state('') 17 18 let deleteLoading = $state(false) 19 let deletePassword = $state('') 20 let deleteToken = $state('') 21 let deleteTokenSent = $state(false) 22 23 $effect(() => { 24 if (!auth.loading && !auth.session) { 25 navigate('/login') 26 } 27 }) 28 29 function showMessage(type: 'success' | 'error', text: string) { 30 message = { type, text } 31 setTimeout(() => { 32 if (message?.text === text) message = null 33 }, 5000) 34 } 35 36 async function handleRequestEmailUpdate(e: Event) { 37 e.preventDefault() 38 if (!auth.session || !newEmail) return 39 40 emailLoading = true 41 message = null 42 43 try { 44 const result = await api.requestEmailUpdate(auth.session.accessJwt) 45 emailTokenRequired = result.tokenRequired 46 if (emailTokenRequired) { 47 showMessage('success', 'Verification code sent to your current email') 48 } else { 49 await api.updateEmail(auth.session.accessJwt, newEmail) 50 showMessage('success', 'Email updated successfully') 51 newEmail = '' 52 } 53 } catch (e) { 54 showMessage('error', e instanceof ApiError ? e.message : 'Failed to update email') 55 } finally { 56 emailLoading = false 57 } 58 } 59 60 async function handleConfirmEmailUpdate(e: Event) { 61 e.preventDefault() 62 if (!auth.session || !newEmail || !emailToken) return 63 64 emailLoading = true 65 message = null 66 67 try { 68 await api.updateEmail(auth.session.accessJwt, newEmail, emailToken) 69 showMessage('success', 'Email updated successfully') 70 newEmail = '' 71 emailToken = '' 72 emailTokenRequired = false 73 } catch (e) { 74 showMessage('error', e instanceof ApiError ? e.message : 'Failed to update email') 75 } finally { 76 emailLoading = false 77 } 78 } 79 80 async function handleUpdateHandle(e: Event) { 81 e.preventDefault() 82 if (!auth.session || !newHandle) return 83 84 handleLoading = true 85 message = null 86 87 try { 88 await api.updateHandle(auth.session.accessJwt, newHandle) 89 showMessage('success', 'Handle updated successfully') 90 newHandle = '' 91 } catch (e) { 92 showMessage('error', e instanceof ApiError ? e.message : 'Failed to update handle') 93 } finally { 94 handleLoading = false 95 } 96 } 97 98 async function handleRequestDelete() { 99 if (!auth.session) return 100 101 deleteLoading = true 102 message = null 103 104 try { 105 await api.requestAccountDelete(auth.session.accessJwt) 106 deleteTokenSent = true 107 showMessage('success', 'Deletion confirmation sent to your email') 108 } catch (e) { 109 showMessage('error', e instanceof ApiError ? e.message : 'Failed to request deletion') 110 } finally { 111 deleteLoading = false 112 } 113 } 114 115 async function handleConfirmDelete(e: Event) { 116 e.preventDefault() 117 if (!auth.session || !deletePassword || !deleteToken) return 118 119 if (!confirm('Are you absolutely sure you want to delete your account? This cannot be undone.')) { 120 return 121 } 122 123 deleteLoading = true 124 message = null 125 126 try { 127 await api.deleteAccount(auth.session.did, deletePassword, deleteToken) 128 await logout() 129 navigate('/login') 130 } catch (e) { 131 showMessage('error', e instanceof ApiError ? e.message : 'Failed to delete account') 132 } finally { 133 deleteLoading = false 134 } 135 } 136</script> 137 138<div class="page"> 139 <header> 140 <a href="#/dashboard" class="back">&larr; Dashboard</a> 141 <h1>Account Settings</h1> 142 </header> 143 144 {#if message} 145 <div class="message {message.type}">{message.text}</div> 146 {/if} 147 148 <section> 149 <h2>Change Email</h2> 150 {#if auth.session?.email} 151 <p class="current">Current: {auth.session.email}</p> 152 {/if} 153 154 {#if emailTokenRequired} 155 <form onsubmit={handleConfirmEmailUpdate}> 156 <div class="field"> 157 <label for="email-token">Verification Code</label> 158 <input 159 id="email-token" 160 type="text" 161 bind:value={emailToken} 162 placeholder="Enter code from email" 163 disabled={emailLoading} 164 required 165 /> 166 </div> 167 <div class="actions"> 168 <button type="submit" disabled={emailLoading || !emailToken}> 169 {emailLoading ? 'Updating...' : 'Confirm Email Change'} 170 </button> 171 <button type="button" class="secondary" onclick={() => { emailTokenRequired = false; emailToken = '' }}> 172 Cancel 173 </button> 174 </div> 175 </form> 176 {:else} 177 <form onsubmit={handleRequestEmailUpdate}> 178 <div class="field"> 179 <label for="new-email">New Email</label> 180 <input 181 id="new-email" 182 type="email" 183 bind:value={newEmail} 184 placeholder="new@example.com" 185 disabled={emailLoading} 186 required 187 /> 188 </div> 189 <button type="submit" disabled={emailLoading || !newEmail}> 190 {emailLoading ? 'Requesting...' : 'Change Email'} 191 </button> 192 </form> 193 {/if} 194 </section> 195 196 <section> 197 <h2>Change Handle</h2> 198 {#if auth.session} 199 <p class="current">Current: @{auth.session.handle}</p> 200 {/if} 201 202 <form onsubmit={handleUpdateHandle}> 203 <div class="field"> 204 <label for="new-handle">New Handle</label> 205 <input 206 id="new-handle" 207 type="text" 208 bind:value={newHandle} 209 placeholder="newhandle.bsky.social" 210 disabled={handleLoading} 211 required 212 /> 213 </div> 214 <button type="submit" disabled={handleLoading || !newHandle}> 215 {handleLoading ? 'Updating...' : 'Change Handle'} 216 </button> 217 </form> 218 </section> 219 220 <section class="danger-zone"> 221 <h2>Delete Account</h2> 222 <p class="warning">This action is irreversible. All your data will be permanently deleted.</p> 223 224 {#if deleteTokenSent} 225 <form onsubmit={handleConfirmDelete}> 226 <div class="field"> 227 <label for="delete-token">Confirmation Code (from email)</label> 228 <input 229 id="delete-token" 230 type="text" 231 bind:value={deleteToken} 232 placeholder="Enter confirmation code" 233 disabled={deleteLoading} 234 required 235 /> 236 </div> 237 <div class="field"> 238 <label for="delete-password">Your Password</label> 239 <input 240 id="delete-password" 241 type="password" 242 bind:value={deletePassword} 243 placeholder="Enter your password" 244 disabled={deleteLoading} 245 required 246 /> 247 </div> 248 <div class="actions"> 249 <button type="submit" class="danger" disabled={deleteLoading || !deleteToken || !deletePassword}> 250 {deleteLoading ? 'Deleting...' : 'Permanently Delete Account'} 251 </button> 252 <button type="button" class="secondary" onclick={() => { deleteTokenSent = false; deleteToken = ''; deletePassword = '' }}> 253 Cancel 254 </button> 255 </div> 256 </form> 257 {:else} 258 <button class="danger" onclick={handleRequestDelete} disabled={deleteLoading}> 259 {deleteLoading ? 'Requesting...' : 'Request Account Deletion'} 260 </button> 261 {/if} 262 </section> 263</div> 264 265<style> 266 .page { 267 max-width: 600px; 268 margin: 0 auto; 269 padding: 2rem; 270 } 271 272 header { 273 margin-bottom: 2rem; 274 } 275 276 .back { 277 color: var(--text-secondary); 278 text-decoration: none; 279 font-size: 0.875rem; 280 } 281 282 .back:hover { 283 color: var(--accent); 284 } 285 286 h1 { 287 margin: 0.5rem 0 0 0; 288 } 289 290 .message { 291 padding: 0.75rem; 292 border-radius: 4px; 293 margin-bottom: 1rem; 294 } 295 296 .message.success { 297 background: var(--success-bg); 298 border: 1px solid var(--success-border); 299 color: var(--success-text); 300 } 301 302 .message.error { 303 background: var(--error-bg); 304 border: 1px solid var(--error-border); 305 color: var(--error-text); 306 } 307 308 section { 309 padding: 1.5rem; 310 background: var(--bg-secondary); 311 border-radius: 8px; 312 margin-bottom: 1.5rem; 313 } 314 315 section h2 { 316 margin: 0 0 0.5rem 0; 317 font-size: 1.125rem; 318 } 319 320 .current { 321 color: var(--text-secondary); 322 font-size: 0.875rem; 323 margin-bottom: 1rem; 324 } 325 326 .field { 327 margin-bottom: 1rem; 328 } 329 330 label { 331 display: block; 332 font-size: 0.875rem; 333 font-weight: 500; 334 margin-bottom: 0.25rem; 335 } 336 337 input { 338 width: 100%; 339 padding: 0.75rem; 340 border: 1px solid var(--border-color-light); 341 border-radius: 4px; 342 font-size: 1rem; 343 box-sizing: border-box; 344 background: var(--bg-input); 345 color: var(--text-primary); 346 } 347 348 input:focus { 349 outline: none; 350 border-color: var(--accent); 351 } 352 353 button { 354 padding: 0.75rem 1.5rem; 355 background: var(--accent); 356 color: white; 357 border: none; 358 border-radius: 4px; 359 cursor: pointer; 360 font-size: 1rem; 361 } 362 363 button:hover:not(:disabled) { 364 background: var(--accent-hover); 365 } 366 367 button:disabled { 368 opacity: 0.6; 369 cursor: not-allowed; 370 } 371 372 button.secondary { 373 background: transparent; 374 color: var(--text-secondary); 375 border: 1px solid var(--border-color-light); 376 } 377 378 button.secondary:hover:not(:disabled) { 379 background: var(--bg-secondary); 380 } 381 382 button.danger { 383 background: var(--error-text); 384 } 385 386 button.danger:hover:not(:disabled) { 387 background: #900; 388 } 389 390 .actions { 391 display: flex; 392 gap: 0.5rem; 393 } 394 395 .danger-zone { 396 background: var(--error-bg); 397 border: 1px solid var(--error-border); 398 } 399 400 .danger-zone h2 { 401 color: var(--error-text); 402 } 403 404 .warning { 405 color: var(--error-text); 406 font-size: 0.875rem; 407 margin-bottom: 1rem; 408 } 409</style>