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