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 {#if auth.session.isAdmin} 194 <a href="#/admin" class="nav-card admin-card"> 195 <h3>{$_('dashboard.navAdmin')}</h3> 196 <p>{$_('dashboard.navAdminDesc')}</p> 197 </a> 198 {/if} 199 </nav> 200 </div> 201{:else if auth.loading} 202 <div class="loading">{$_('common.loading')}</div> 203{/if} 204 205<style> 206 .dashboard { 207 max-width: var(--width-xl); 208 margin: 0 auto; 209 padding: var(--space-7); 210 } 211 212 header { 213 display: flex; 214 justify-content: space-between; 215 align-items: center; 216 margin-bottom: var(--space-7); 217 } 218 219 header h1 { 220 margin: 0; 221 } 222 223 .account-dropdown { 224 position: relative; 225 } 226 227 .account-trigger { 228 display: flex; 229 align-items: center; 230 gap: var(--space-3); 231 padding: var(--space-3) var(--space-5); 232 background: transparent; 233 border: 1px solid var(--border-color); 234 border-radius: var(--radius-md); 235 cursor: pointer; 236 color: var(--text-primary); 237 } 238 239 .account-trigger:hover:not(:disabled) { 240 background: var(--bg-secondary); 241 } 242 243 .account-trigger:disabled { 244 opacity: 0.6; 245 cursor: not-allowed; 246 } 247 248 .account-trigger .account-handle { 249 font-weight: var(--font-medium); 250 } 251 252 .dropdown-arrow { 253 font-size: 0.625rem; 254 color: var(--text-secondary); 255 } 256 257 .dropdown-menu { 258 position: absolute; 259 top: 100%; 260 right: 0; 261 margin-top: var(--space-2); 262 min-width: 200px; 263 background: var(--bg-card); 264 border: 1px solid var(--border-color); 265 border-radius: var(--radius-xl); 266 box-shadow: var(--shadow-lg); 267 z-index: 100; 268 overflow: hidden; 269 } 270 271 .dropdown-section { 272 padding: var(--space-3) 0; 273 } 274 275 .dropdown-label { 276 display: block; 277 padding: var(--space-2) var(--space-5); 278 font-size: var(--text-xs); 279 color: var(--text-muted); 280 text-transform: uppercase; 281 letter-spacing: 0.05em; 282 } 283 284 .dropdown-item { 285 display: block; 286 width: 100%; 287 padding: var(--space-4) var(--space-5); 288 background: transparent; 289 border: none; 290 text-align: left; 291 cursor: pointer; 292 color: var(--text-primary); 293 font-size: var(--text-sm); 294 } 295 296 .dropdown-item:hover { 297 background: var(--bg-secondary); 298 } 299 300 .dropdown-item.logout-item { 301 color: var(--error-text); 302 } 303 304 .dropdown-divider { 305 height: 1px; 306 background: var(--border-color); 307 margin: 0; 308 } 309 310 section { 311 background: var(--bg-secondary); 312 padding: var(--space-6); 313 border-radius: var(--radius-xl); 314 margin-bottom: var(--space-7); 315 } 316 317 section h2 { 318 margin: 0 0 var(--space-4) 0; 319 font-size: var(--text-xl); 320 } 321 322 dl { 323 display: grid; 324 grid-template-columns: auto 1fr; 325 gap: var(--space-3) var(--space-5); 326 margin: 0; 327 } 328 329 dt { 330 font-weight: var(--font-medium); 331 color: var(--text-secondary); 332 } 333 334 dd { 335 margin: 0; 336 } 337 338 .mono { 339 font-family: ui-monospace, monospace; 340 font-size: var(--text-sm); 341 word-break: break-all; 342 } 343 344 .badge { 345 display: inline-block; 346 padding: var(--space-1) var(--space-3); 347 border-radius: var(--radius-md); 348 font-size: var(--text-xs); 349 margin-left: var(--space-3); 350 } 351 352 .badge.success { 353 background: var(--success-bg); 354 color: var(--success-text); 355 } 356 357 .badge.warning { 358 background: var(--warning-bg); 359 color: var(--warning-text); 360 } 361 362 .badge.admin { 363 background: var(--accent); 364 color: var(--text-inverse); 365 } 366 367 .badge.deactivated { 368 background: var(--warning-bg); 369 color: var(--warning-text); 370 border: 1px solid var(--warning-border); 371 } 372 373 .nav-grid { 374 display: grid; 375 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 376 gap: var(--space-4); 377 } 378 379 .nav-card { 380 display: block; 381 padding: var(--space-6); 382 background: var(--bg-card); 383 border: 1px solid var(--border-color); 384 border-radius: var(--radius-xl); 385 text-decoration: none; 386 color: inherit; 387 transition: border-color var(--transition-normal), box-shadow var(--transition-normal); 388 } 389 390 .nav-card:hover { 391 border-color: var(--accent); 392 box-shadow: 0 2px 8px var(--accent-muted); 393 } 394 395 .nav-card h3 { 396 margin: 0 0 var(--space-3) 0; 397 color: var(--accent); 398 } 399 400 .nav-card p { 401 margin: 0; 402 color: var(--text-secondary); 403 font-size: var(--text-sm); 404 } 405 406 .nav-card.admin-card { 407 border-color: var(--accent); 408 background: linear-gradient(135deg, var(--bg-card) 0%, var(--accent-muted) 100%); 409 } 410 411 .nav-card.admin-card:hover { 412 box-shadow: 0 2px 12px var(--accent-muted); 413 } 414 415 .loading { 416 text-align: center; 417 padding: var(--space-9); 418 color: var(--text-secondary); 419 } 420 421 .deactivated-banner { 422 background: var(--warning-bg); 423 border: 1px solid var(--warning-border); 424 border-radius: var(--radius-xl); 425 padding: var(--space-5) var(--space-6); 426 margin-bottom: var(--space-7); 427 } 428 429 .deactivated-banner strong { 430 color: var(--warning-text); 431 font-size: var(--text-base); 432 } 433 434 .deactivated-banner p { 435 margin: var(--space-3) 0 0 0; 436 color: var(--warning-text); 437 font-size: var(--text-sm); 438 } 439</style>