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