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 === 'deactivated' || auth.session.active === false} 103 <div class="deactivated-banner"> 104 <strong>{$_('dashboard.deactivatedTitle')}</strong> 105 <p>{$_('dashboard.deactivatedMessage')}</p> 106 </div> 107 {/if} 108 109 <section class="account-overview"> 110 <h2>{$_('dashboard.accountOverview')}</h2> 111 <dl> 112 <dt>{$_('dashboard.handle')}</dt> 113 <dd> 114 @{auth.session.handle} 115 {#if auth.session.isAdmin} 116 <span class="badge admin">{$_('dashboard.admin')}</span> 117 {/if} 118 {#if auth.session.status === 'deactivated' || auth.session.active === false} 119 <span class="badge deactivated">{$_('dashboard.deactivated')}</span> 120 {/if} 121 </dd> 122 <dt>{$_('dashboard.did')}</dt> 123 <dd class="mono">{auth.session.did}</dd> 124 {#if auth.session.preferredChannel} 125 <dt>{$_('dashboard.primaryContact')}</dt> 126 <dd> 127 {#if auth.session.preferredChannel === 'email'} 128 {auth.session.email || $_('register.email')} 129 {:else if auth.session.preferredChannel === 'discord'} 130 {$_('register.discord')} 131 {:else if auth.session.preferredChannel === 'telegram'} 132 {$_('register.telegram')} 133 {:else if auth.session.preferredChannel === 'signal'} 134 {$_('register.signal')} 135 {:else} 136 {auth.session.preferredChannel} 137 {/if} 138 {#if auth.session.preferredChannelVerified} 139 <span class="badge success">{$_('dashboard.verified')}</span> 140 {:else} 141 <span class="badge warning">{$_('dashboard.unverified')}</span> 142 {/if} 143 </dd> 144 {:else if auth.session.email} 145 <dt>{$_('register.email')}</dt> 146 <dd> 147 {auth.session.email} 148 {#if auth.session.emailConfirmed} 149 <span class="badge success">{$_('dashboard.verified')}</span> 150 {:else} 151 <span class="badge warning">{$_('dashboard.unverified')}</span> 152 {/if} 153 </dd> 154 {/if} 155 </dl> 156 </section> 157 158 <nav class="nav-grid"> 159 <a href="#/app-passwords" class="nav-card"> 160 <h3>{$_('dashboard.navAppPasswords')}</h3> 161 <p>{$_('dashboard.navAppPasswordsDesc')}</p> 162 </a> 163 <a href="#/sessions" class="nav-card"> 164 <h3>{$_('dashboard.navSessions')}</h3> 165 <p>{$_('dashboard.navSessionsDesc')}</p> 166 </a> 167 {#if inviteCodesEnabled} 168 <a href="#/invite-codes" class="nav-card"> 169 <h3>{$_('dashboard.navInviteCodes')}</h3> 170 <p>{$_('dashboard.navInviteCodesDesc')}</p> 171 </a> 172 {/if} 173 <a href="#/settings" class="nav-card"> 174 <h3>{$_('dashboard.navSettings')}</h3> 175 <p>{$_('dashboard.navSettingsDesc')}</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="#/comms" class="nav-card"> 182 <h3>{$_('dashboard.navComms')}</h3> 183 <p>{$_('dashboard.navCommsDesc')}</p> 184 </a> 185 <a href="#/repo" class="nav-card"> 186 <h3>{$_('dashboard.navRepo')}</h3> 187 <p>{$_('dashboard.navRepoDesc')}</p> 188 </a> 189 <a href="#/controllers" class="nav-card"> 190 <h3>{$_('dashboard.navDelegation')}</h3> 191 <p>{$_('dashboard.navDelegationDesc')}</p> 192 </a> 193 <a href="#/migrate" class="nav-card"> 194 <h3>{$_('migration.navTitle')}</h3> 195 <p>{$_('migration.navDesc')}</p> 196 </a> 197 {#if auth.session.isAdmin} 198 <a href="#/admin" class="nav-card admin-card"> 199 <h3>{$_('dashboard.navAdmin')}</h3> 200 <p>{$_('dashboard.navAdminDesc')}</p> 201 </a> 202 {/if} 203 </nav> 204 </div> 205{:else if auth.loading} 206 <div class="loading">{$_('common.loading')}</div> 207{/if} 208 209<style> 210 .dashboard { 211 max-width: var(--width-xl); 212 margin: 0 auto; 213 padding: var(--space-7); 214 } 215 216 header { 217 display: flex; 218 justify-content: space-between; 219 align-items: center; 220 margin-bottom: var(--space-7); 221 } 222 223 header h1 { 224 margin: 0; 225 } 226 227 .account-dropdown { 228 position: relative; 229 } 230 231 .account-trigger { 232 display: flex; 233 align-items: center; 234 gap: var(--space-3); 235 padding: var(--space-3) var(--space-5); 236 background: transparent; 237 border: 1px solid var(--border-color); 238 border-radius: var(--radius-md); 239 cursor: pointer; 240 color: var(--text-primary); 241 } 242 243 .account-trigger:hover:not(:disabled) { 244 background: var(--bg-secondary); 245 } 246 247 .account-trigger:disabled { 248 opacity: 0.6; 249 cursor: not-allowed; 250 } 251 252 .account-trigger .account-handle { 253 font-weight: var(--font-medium); 254 } 255 256 .dropdown-arrow { 257 font-size: 0.625rem; 258 color: var(--text-secondary); 259 } 260 261 .dropdown-menu { 262 position: absolute; 263 top: 100%; 264 right: 0; 265 margin-top: var(--space-2); 266 min-width: 200px; 267 background: var(--bg-card); 268 border: 1px solid var(--border-color); 269 border-radius: var(--radius-xl); 270 box-shadow: var(--shadow-lg); 271 z-index: 100; 272 overflow: hidden; 273 } 274 275 .dropdown-section { 276 padding: var(--space-3) 0; 277 } 278 279 .dropdown-label { 280 display: block; 281 padding: var(--space-2) var(--space-5); 282 font-size: var(--text-xs); 283 color: var(--text-muted); 284 text-transform: uppercase; 285 letter-spacing: 0.05em; 286 } 287 288 .dropdown-item { 289 display: block; 290 width: 100%; 291 padding: var(--space-4) var(--space-5); 292 background: transparent; 293 border: none; 294 text-align: left; 295 cursor: pointer; 296 color: var(--text-primary); 297 font-size: var(--text-sm); 298 } 299 300 .dropdown-item:hover { 301 background: var(--bg-secondary); 302 } 303 304 .dropdown-item.logout-item { 305 color: var(--error-text); 306 } 307 308 .dropdown-divider { 309 height: 1px; 310 background: var(--border-color); 311 margin: 0; 312 } 313 314 section { 315 background: var(--bg-secondary); 316 padding: var(--space-6); 317 border-radius: var(--radius-xl); 318 margin-bottom: var(--space-7); 319 } 320 321 section h2 { 322 margin: 0 0 var(--space-4) 0; 323 font-size: var(--text-xl); 324 } 325 326 dl { 327 display: grid; 328 grid-template-columns: auto 1fr; 329 gap: var(--space-3) var(--space-5); 330 margin: 0; 331 } 332 333 dt { 334 font-weight: var(--font-medium); 335 color: var(--text-secondary); 336 } 337 338 dd { 339 margin: 0; 340 } 341 342 .mono { 343 font-family: ui-monospace, monospace; 344 font-size: var(--text-sm); 345 word-break: break-all; 346 } 347 348 .badge { 349 display: inline-block; 350 padding: var(--space-1) var(--space-3); 351 border-radius: var(--radius-md); 352 font-size: var(--text-xs); 353 margin-left: var(--space-3); 354 } 355 356 .badge.success { 357 background: var(--success-bg); 358 color: var(--success-text); 359 } 360 361 .badge.warning { 362 background: var(--warning-bg); 363 color: var(--warning-text); 364 } 365 366 .badge.admin { 367 background: var(--accent); 368 color: var(--text-inverse); 369 } 370 371 .badge.deactivated { 372 background: var(--warning-bg); 373 color: var(--warning-text); 374 border: 1px solid var(--warning-border); 375 } 376 377 .nav-grid { 378 display: grid; 379 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 380 gap: var(--space-4); 381 } 382 383 .nav-card { 384 display: block; 385 padding: var(--space-6); 386 background: var(--bg-card); 387 border: 1px solid var(--border-color); 388 border-radius: var(--radius-xl); 389 text-decoration: none; 390 color: inherit; 391 transition: border-color var(--transition-normal), box-shadow var(--transition-normal); 392 } 393 394 .nav-card:hover { 395 border-color: var(--accent); 396 box-shadow: 0 2px 8px var(--accent-muted); 397 } 398 399 .nav-card h3 { 400 margin: 0 0 var(--space-3) 0; 401 color: var(--accent); 402 } 403 404 .nav-card p { 405 margin: 0; 406 color: var(--text-secondary); 407 font-size: var(--text-sm); 408 } 409 410 .nav-card.admin-card { 411 border-color: var(--accent); 412 background: linear-gradient(135deg, var(--bg-card) 0%, var(--accent-muted) 100%); 413 } 414 415 .nav-card.admin-card:hover { 416 box-shadow: 0 2px 12px var(--accent-muted); 417 } 418 419 .loading { 420 text-align: center; 421 padding: var(--space-9); 422 color: var(--text-secondary); 423 } 424 425 .deactivated-banner { 426 background: var(--warning-bg); 427 border: 1px solid var(--warning-border); 428 border-radius: var(--radius-xl); 429 padding: var(--space-5) var(--space-6); 430 margin-bottom: var(--space-7); 431 } 432 433 .deactivated-banner strong { 434 color: var(--warning-text); 435 font-size: var(--text-base); 436 } 437 438 .deactivated-banner p { 439 margin: var(--space-3) 0 0 0; 440 color: var(--warning-text); 441 font-size: var(--text-sm); 442 } 443</style>