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