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