this repo has no description
1<script lang="ts"> 2 import { getAuthState, logout, switchAccount } from '../lib/auth.svelte' 3 import { navigate } from '../lib/router.svelte' 4 import { _ } from '../lib/i18n' 5 import { api } from '../lib/api' 6 import { onMount } from 'svelte' 7 8 const auth = getAuthState() 9 let dropdownOpen = $state(false) 10 let switching = $state(false) 11 let inviteCodesEnabled = $state(false) 12 13 const isDidWeb = $derived(auth.session?.did?.startsWith('did:web:') ?? false) 14 15 onMount(async () => { 16 try { 17 const serverInfo = await api.describeServer() 18 inviteCodesEnabled = serverInfo.inviteCodeRequired 19 } catch { 20 inviteCodesEnabled = false 21 } 22 }) 23 24 $effect(() => { 25 if (!auth.loading && !auth.session) { 26 navigate('/login') 27 } 28 }) 29 30 async function handleLogout() { 31 await logout() 32 navigate('/login') 33 } 34 35 async function handleSwitchAccount(did: string) { 36 switching = true 37 dropdownOpen = false 38 try { 39 await switchAccount(did) 40 } catch { 41 navigate('/login') 42 } finally { 43 switching = false 44 } 45 } 46 47 function toggleDropdown() { 48 dropdownOpen = !dropdownOpen 49 } 50 51 function closeDropdown(e: MouseEvent) { 52 const target = e.target as HTMLElement 53 if (!target.closest('.account-dropdown')) { 54 dropdownOpen = false 55 } 56 } 57 58 $effect(() => { 59 if (dropdownOpen) { 60 document.addEventListener('click', closeDropdown) 61 return () => document.removeEventListener('click', closeDropdown) 62 } 63 }) 64 65 let otherAccounts = $derived( 66 auth.savedAccounts.filter(a => a.did !== auth.session?.did) 67 ) 68</script> 69 70{#if auth.session} 71 <div class="dashboard"> 72 <header> 73 <h1>{$_('dashboard.title')}</h1> 74 <div class="account-dropdown"> 75 <button class="account-trigger" onclick={toggleDropdown} disabled={switching}> 76 <span class="account-handle">@{auth.session.handle}</span> 77 <span class="dropdown-arrow">{dropdownOpen ? '▲' : '▼'}</span> 78 </button> 79 {#if dropdownOpen} 80 <div class="dropdown-menu"> 81 {#if otherAccounts.length > 0} 82 <div class="dropdown-section"> 83 <span class="dropdown-label">{$_('dashboard.switchAccount')}</span> 84 {#each otherAccounts as account} 85 <button type="button" class="dropdown-item" onclick={() => handleSwitchAccount(account.did)}> 86 @{account.handle} 87 </button> 88 {/each} 89 </div> 90 <div class="dropdown-divider"></div> 91 {/if} 92 <button type="button" class="dropdown-item" onclick={() => { dropdownOpen = false; navigate('/login') }}> 93 {$_('dashboard.addAnotherAccount')} 94 </button> 95 <div class="dropdown-divider"></div> 96 <button type="button" class="dropdown-item logout-item" onclick={handleLogout}> 97 {$_('dashboard.signOut', { values: { handle: auth.session.handle } })} 98 </button> 99 </div> 100 {/if} 101 </div> 102 </header> 103 104 {#if auth.session.status === 'migrated'} 105 <div class="migrated-banner"> 106 <strong>{$_('dashboard.migratedTitle')}</strong> 107 <p>{$_('dashboard.migratedMessage', { values: { pds: auth.session.migratedToPds || 'another PDS' } })}</p> 108 </div> 109 {:else if auth.session.status === 'deactivated' || auth.session.active === false} 110 <div class="deactivated-banner"> 111 <strong>{$_('dashboard.deactivatedTitle')}</strong> 112 <p>{$_('dashboard.deactivatedMessage')}</p> 113 </div> 114 {/if} 115 116 <section class="account-overview"> 117 <h2>{$_('dashboard.accountOverview')}</h2> 118 <dl> 119 <dt>{$_('dashboard.handle')}</dt> 120 <dd> 121 @{auth.session.handle} 122 {#if auth.session.isAdmin} 123 <span class="badge admin">{$_('dashboard.admin')}</span> 124 {/if} 125 {#if auth.session.status === 'migrated'} 126 <span class="badge migrated">{$_('dashboard.migrated')}</span> 127 {:else if auth.session.status === 'deactivated' || auth.session.active === false} 128 <span class="badge deactivated">{$_('dashboard.deactivated')}</span> 129 {/if} 130 </dd> 131 <dt>{$_('dashboard.did')}</dt> 132 <dd class="mono">{auth.session.did}</dd> 133 {#if auth.session.preferredChannel} 134 <dt>{$_('dashboard.primaryContact')}</dt> 135 <dd> 136 {#if auth.session.preferredChannel === 'email'} 137 {auth.session.email || $_('register.email')} 138 {:else if auth.session.preferredChannel === 'discord'} 139 {$_('register.discord')} 140 {:else if auth.session.preferredChannel === 'telegram'} 141 {$_('register.telegram')} 142 {:else if auth.session.preferredChannel === 'signal'} 143 {$_('register.signal')} 144 {:else} 145 {auth.session.preferredChannel} 146 {/if} 147 {#if auth.session.preferredChannelVerified} 148 <span class="badge success">{$_('dashboard.verified')}</span> 149 {:else} 150 <span class="badge warning">{$_('dashboard.unverified')}</span> 151 {/if} 152 </dd> 153 {:else if auth.session.email} 154 <dt>{$_('register.email')}</dt> 155 <dd> 156 {auth.session.email} 157 {#if auth.session.emailConfirmed} 158 <span class="badge success">{$_('dashboard.verified')}</span> 159 {:else} 160 <span class="badge warning">{$_('dashboard.unverified')}</span> 161 {/if} 162 </dd> 163 {/if} 164 </dl> 165 </section> 166 167 <nav class="nav-grid"> 168 {#if auth.session.status === 'migrated'} 169 <a href="#/did-document" class="nav-card migrated-card"> 170 <h3>{$_('dashboard.navDidDocument')}</h3> 171 <p>{$_('dashboard.navDidDocumentDesc')}</p> 172 </a> 173 <a href="#/sessions" class="nav-card"> 174 <h3>{$_('dashboard.navSessions')}</h3> 175 <p>{$_('dashboard.navSessionsDesc')}</p> 176 </a> 177 <a href="#/security" class="nav-card"> 178 <h3>{$_('dashboard.navSecurity')}</h3> 179 <p>{$_('dashboard.navSecurityDesc')}</p> 180 </a> 181 <a href="#/settings" class="nav-card"> 182 <h3>{$_('dashboard.navSettings')}</h3> 183 <p>{$_('dashboard.navSettingsDesc')}</p> 184 </a> 185 <a href="#/migrate" class="nav-card"> 186 <h3>{$_('dashboard.navMigrateAgain')}</h3> 187 <p>{$_('dashboard.navMigrateAgainDesc')}</p> 188 </a> 189 {:else} 190 <a href="#/app-passwords" class="nav-card"> 191 <h3>{$_('dashboard.navAppPasswords')}</h3> 192 <p>{$_('dashboard.navAppPasswordsDesc')}</p> 193 </a> 194 <a href="#/sessions" class="nav-card"> 195 <h3>{$_('dashboard.navSessions')}</h3> 196 <p>{$_('dashboard.navSessionsDesc')}</p> 197 </a> 198 {#if inviteCodesEnabled && auth.session.isAdmin} 199 <a href="#/invite-codes" class="nav-card"> 200 <h3>{$_('dashboard.navInviteCodes')}</h3> 201 <p>{$_('dashboard.navInviteCodesDesc')}</p> 202 </a> 203 {/if} 204 <a href="#/settings" class="nav-card"> 205 <h3>{$_('dashboard.navSettings')}</h3> 206 <p>{$_('dashboard.navSettingsDesc')}</p> 207 </a> 208 <a href="#/security" class="nav-card"> 209 <h3>{$_('dashboard.navSecurity')}</h3> 210 <p>{$_('dashboard.navSecurityDesc')}</p> 211 </a> 212 <a href="#/comms" class="nav-card"> 213 <h3>{$_('dashboard.navComms')}</h3> 214 <p>{$_('dashboard.navCommsDesc')}</p> 215 </a> 216 <a href="#/repo" class="nav-card"> 217 <h3>{$_('dashboard.navRepo')}</h3> 218 <p>{$_('dashboard.navRepoDesc')}</p> 219 </a> 220 <a href="#/controllers" class="nav-card"> 221 <h3>{$_('dashboard.navDelegation')}</h3> 222 <p>{$_('dashboard.navDelegationDesc')}</p> 223 </a> 224 {#if isDidWeb} 225 <a href="#/did-document" class="nav-card did-web-card"> 226 <h3>{$_('dashboard.navDidDocument')}</h3> 227 <p>{$_('dashboard.navDidDocumentDescActive')}</p> 228 </a> 229 {/if} 230 <a href="#/migrate" class="nav-card"> 231 <h3>{$_('migration.navTitle')}</h3> 232 <p>{$_('migration.navDesc')}</p> 233 </a> 234 {#if auth.session.isAdmin} 235 <a href="#/admin" class="nav-card admin-card"> 236 <h3>{$_('dashboard.navAdmin')}</h3> 237 <p>{$_('dashboard.navAdminDesc')}</p> 238 </a> 239 {/if} 240 {/if} 241 </nav> 242 </div> 243{:else if auth.loading} 244 <div class="loading">{$_('common.loading')}</div> 245{/if} 246 247<style> 248 .dashboard { 249 max-width: var(--width-xl); 250 margin: 0 auto; 251 padding: var(--space-7); 252 } 253 254 header { 255 display: flex; 256 justify-content: space-between; 257 align-items: center; 258 margin-bottom: var(--space-7); 259 } 260 261 header h1 { 262 margin: 0; 263 } 264 265 .account-dropdown { 266 position: relative; 267 } 268 269 .account-trigger { 270 display: flex; 271 align-items: center; 272 gap: var(--space-3); 273 padding: var(--space-3) var(--space-5); 274 background: transparent; 275 border: 1px solid var(--border-color); 276 border-radius: var(--radius-md); 277 cursor: pointer; 278 color: var(--text-primary); 279 } 280 281 .account-trigger:hover:not(:disabled) { 282 background: var(--bg-secondary); 283 } 284 285 .account-trigger:disabled { 286 opacity: 0.6; 287 cursor: not-allowed; 288 } 289 290 .account-trigger .account-handle { 291 font-weight: var(--font-medium); 292 } 293 294 .dropdown-arrow { 295 font-size: 0.625rem; 296 color: var(--text-secondary); 297 } 298 299 .dropdown-menu { 300 position: absolute; 301 top: 100%; 302 right: 0; 303 margin-top: var(--space-2); 304 min-width: 200px; 305 background: var(--bg-card); 306 border: 1px solid var(--border-color); 307 border-radius: var(--radius-xl); 308 box-shadow: var(--shadow-lg); 309 z-index: 100; 310 overflow: hidden; 311 } 312 313 .dropdown-section { 314 padding: var(--space-3) 0; 315 } 316 317 .dropdown-label { 318 display: block; 319 padding: var(--space-2) var(--space-5); 320 font-size: var(--text-xs); 321 color: var(--text-muted); 322 text-transform: uppercase; 323 letter-spacing: 0.05em; 324 } 325 326 .dropdown-item { 327 display: block; 328 width: 100%; 329 padding: var(--space-4) var(--space-5); 330 background: transparent; 331 border: none; 332 text-align: left; 333 cursor: pointer; 334 color: var(--text-primary); 335 font-size: var(--text-sm); 336 } 337 338 .dropdown-item:hover { 339 background: var(--bg-secondary); 340 } 341 342 .dropdown-item.logout-item { 343 color: var(--error-text); 344 } 345 346 .dropdown-divider { 347 height: 1px; 348 background: var(--border-color); 349 margin: 0; 350 } 351 352 section { 353 background: var(--bg-secondary); 354 padding: var(--space-6); 355 border-radius: var(--radius-xl); 356 margin-bottom: var(--space-7); 357 } 358 359 section h2 { 360 margin: 0 0 var(--space-4) 0; 361 font-size: var(--text-xl); 362 } 363 364 dl { 365 display: grid; 366 grid-template-columns: auto 1fr; 367 gap: var(--space-3) var(--space-5); 368 margin: 0; 369 } 370 371 dt { 372 font-weight: var(--font-medium); 373 color: var(--text-secondary); 374 } 375 376 dd { 377 margin: 0; 378 } 379 380 .mono { 381 font-family: ui-monospace, monospace; 382 font-size: var(--text-sm); 383 word-break: break-all; 384 } 385 386 .badge { 387 display: inline-block; 388 padding: var(--space-1) var(--space-3); 389 border-radius: var(--radius-md); 390 font-size: var(--text-xs); 391 margin-left: var(--space-3); 392 } 393 394 .badge.success { 395 background: var(--success-bg); 396 color: var(--success-text); 397 } 398 399 .badge.warning { 400 background: var(--warning-bg); 401 color: var(--warning-text); 402 } 403 404 .badge.admin { 405 background: var(--accent); 406 color: var(--text-inverse); 407 } 408 409 .badge.deactivated { 410 background: var(--warning-bg); 411 color: var(--warning-text); 412 border: 1px solid var(--warning-border); 413 } 414 415 .badge.migrated { 416 background: var(--info-bg, #e0f2fe); 417 color: var(--info-text, #0369a1); 418 border: 1px solid var(--info-border, #7dd3fc); 419 } 420 421 .nav-grid { 422 display: grid; 423 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 424 gap: var(--space-4); 425 } 426 427 .nav-card { 428 display: block; 429 padding: var(--space-6); 430 background: var(--bg-card); 431 border: 1px solid var(--border-color); 432 border-radius: var(--radius-xl); 433 text-decoration: none; 434 color: inherit; 435 transition: border-color var(--transition-normal), box-shadow var(--transition-normal); 436 } 437 438 .nav-card:hover { 439 border-color: var(--accent); 440 box-shadow: 0 2px 8px var(--accent-muted); 441 } 442 443 .nav-card h3 { 444 margin: 0 0 var(--space-3) 0; 445 color: var(--accent); 446 } 447 448 .nav-card p { 449 margin: 0; 450 color: var(--text-secondary); 451 font-size: var(--text-sm); 452 } 453 454 .nav-card.admin-card { 455 border-color: var(--accent); 456 background: linear-gradient(135deg, var(--bg-card) 0%, var(--accent-muted) 100%); 457 } 458 459 .nav-card.admin-card:hover { 460 box-shadow: 0 2px 12px var(--accent-muted); 461 } 462 463 .loading { 464 text-align: center; 465 padding: var(--space-9); 466 color: var(--text-secondary); 467 } 468 469 .deactivated-banner { 470 background: var(--warning-bg); 471 border: 1px solid var(--warning-border); 472 border-radius: var(--radius-xl); 473 padding: var(--space-5) var(--space-6); 474 margin-bottom: var(--space-7); 475 } 476 477 .deactivated-banner strong { 478 color: var(--warning-text); 479 font-size: var(--text-base); 480 } 481 482 .deactivated-banner p { 483 margin: var(--space-3) 0 0 0; 484 color: var(--warning-text); 485 font-size: var(--text-sm); 486 } 487 488 .migrated-banner { 489 background: var(--info-bg, #e0f2fe); 490 border: 1px solid var(--info-border, #7dd3fc); 491 border-radius: var(--radius-xl); 492 padding: var(--space-5) var(--space-6); 493 margin-bottom: var(--space-7); 494 } 495 496 .migrated-banner strong { 497 color: var(--info-text, #0369a1); 498 font-size: var(--text-base); 499 } 500 501 .migrated-banner p { 502 margin: var(--space-3) 0 0 0; 503 color: var(--info-text, #0369a1); 504 font-size: var(--text-sm); 505 } 506 507 .nav-card.migrated-card { 508 border-color: var(--info-border, #7dd3fc); 509 background: linear-gradient(135deg, var(--bg-card) 0%, var(--info-bg, #e0f2fe) 100%); 510 } 511 512 .nav-card.migrated-card:hover { 513 box-shadow: 0 2px 12px var(--info-bg, #e0f2fe); 514 } 515 516 .nav-card.migrated-card h3 { 517 color: var(--info-text, #0369a1); 518 } 519 520 .nav-card.did-web-card { 521 border-color: var(--accent); 522 background: linear-gradient(135deg, var(--bg-card) 0%, var(--accent-muted) 100%); 523 } 524 525 .nav-card.did-web-card:hover { 526 box-shadow: 0 2px 12px var(--accent-muted); 527 } 528</style>