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 const auth = getAuthState() 5 let dropdownOpen = $state(false) 6 let switching = $state(false) 7 $effect(() => { 8 if (!auth.loading && !auth.session) { 9 navigate('/login') 10 } 11 }) 12 async function handleLogout() { 13 await logout() 14 navigate('/login') 15 } 16 async function handleSwitchAccount(did: string) { 17 switching = true 18 dropdownOpen = false 19 try { 20 await switchAccount(did) 21 } catch { 22 navigate('/login') 23 } finally { 24 switching = false 25 } 26 } 27 function toggleDropdown() { 28 dropdownOpen = !dropdownOpen 29 } 30 function closeDropdown(e: MouseEvent) { 31 const target = e.target as HTMLElement 32 if (!target.closest('.account-dropdown')) { 33 dropdownOpen = false 34 } 35 } 36 $effect(() => { 37 if (dropdownOpen) { 38 document.addEventListener('click', closeDropdown) 39 return () => document.removeEventListener('click', closeDropdown) 40 } 41 }) 42 let otherAccounts = $derived( 43 auth.savedAccounts.filter(a => a.did !== auth.session?.did) 44 ) 45</script> 46{#if auth.session} 47 <div class="dashboard"> 48 <header> 49 <h1>Dashboard</h1> 50 <div class="account-dropdown"> 51 <button class="account-trigger" onclick={toggleDropdown} disabled={switching}> 52 <span class="account-handle">@{auth.session.handle}</span> 53 <span class="dropdown-arrow">{dropdownOpen ? '▲' : '▼'}</span> 54 </button> 55 {#if dropdownOpen} 56 <div class="dropdown-menu"> 57 {#if otherAccounts.length > 0} 58 <div class="dropdown-section"> 59 <span class="dropdown-label">Switch Account</span> 60 {#each otherAccounts as account} 61 <button 62 type="button" 63 class="dropdown-item" 64 onclick={() => handleSwitchAccount(account.did)} 65 > 66 @{account.handle} 67 </button> 68 {/each} 69 </div> 70 <div class="dropdown-divider"></div> 71 {/if} 72 <button 73 type="button" 74 class="dropdown-item" 75 onclick={() => { dropdownOpen = false; navigate('/login') }} 76 > 77 Add another account 78 </button> 79 <div class="dropdown-divider"></div> 80 <button type="button" class="dropdown-item logout-item" onclick={handleLogout}> 81 Sign out @{auth.session.handle} 82 </button> 83 </div> 84 {/if} 85 </div> 86 </header> 87 {#if auth.session.status === 'deactivated' || auth.session.active === false} 88 <div class="deactivated-banner"> 89 <strong>Account Deactivated</strong> 90 <p>Your account is currently deactivated. This typically happens during account migration. Some features may be limited until your account is reactivated.</p> 91 </div> 92 {/if} 93 <section class="account-overview"> 94 <h2>Account Overview</h2> 95 <dl> 96 <dt>Handle</dt> 97 <dd> 98 @{auth.session.handle} 99 {#if auth.session.isAdmin} 100 <span class="badge admin">Admin</span> 101 {/if} 102 {#if auth.session.status === 'deactivated' || auth.session.active === false} 103 <span class="badge deactivated">Deactivated</span> 104 {/if} 105 </dd> 106 <dt>DID</dt> 107 <dd class="mono">{auth.session.did}</dd> 108 {#if auth.session.preferredChannel} 109 <dt>Primary Contact</dt> 110 <dd> 111 {#if auth.session.preferredChannel === 'email'} 112 {auth.session.email || 'Email'} 113 {:else if auth.session.preferredChannel === 'discord'} 114 Discord 115 {:else if auth.session.preferredChannel === 'telegram'} 116 Telegram 117 {:else if auth.session.preferredChannel === 'signal'} 118 Signal 119 {:else} 120 {auth.session.preferredChannel} 121 {/if} 122 {#if auth.session.preferredChannelVerified} 123 <span class="badge success">Verified</span> 124 {:else} 125 <span class="badge warning">Unverified</span> 126 {/if} 127 </dd> 128 {:else if auth.session.email} 129 <dt>Email</dt> 130 <dd> 131 {auth.session.email} 132 {#if auth.session.emailConfirmed} 133 <span class="badge success">Verified</span> 134 {:else} 135 <span class="badge warning">Unverified</span> 136 {/if} 137 </dd> 138 {/if} 139 </dl> 140 </section> 141 <nav class="nav-grid"> 142 <a href="#/app-passwords" class="nav-card"> 143 <h3>App Passwords</h3> 144 <p>Manage passwords for third-party apps</p> 145 </a> 146 <a href="#/sessions" class="nav-card"> 147 <h3>Active Sessions</h3> 148 <p>View and manage your login sessions</p> 149 </a> 150 <a href="#/invite-codes" class="nav-card"> 151 <h3>Invite Codes</h3> 152 <p>View and create invite codes</p> 153 </a> 154 <a href="#/settings" class="nav-card"> 155 <h3>Account Settings</h3> 156 <p>Email, password, handle, and more</p> 157 </a> 158 <a href="#/security" class="nav-card"> 159 <h3>Security</h3> 160 <p>Two-factor authentication</p> 161 </a> 162 <a href="#/notifications" class="nav-card"> 163 <h3>Notification Preferences</h3> 164 <p>Discord, Telegram, Signal channels</p> 165 </a> 166 <a href="#/repo" class="nav-card"> 167 <h3>Repository Explorer</h3> 168 <p>Browse and manage raw AT Protocol records</p> 169 </a> 170 {#if auth.session.isAdmin} 171 <a href="#/admin" class="nav-card admin-card"> 172 <h3>Admin Panel</h3> 173 <p>Server stats and admin operations</p> 174 </a> 175 {/if} 176 </nav> 177 </div> 178{:else if auth.loading} 179 <div class="loading">Loading...</div> 180{/if} 181<style> 182 .dashboard { 183 max-width: 800px; 184 margin: 0 auto; 185 padding: 2rem; 186 } 187 header { 188 display: flex; 189 justify-content: space-between; 190 align-items: center; 191 margin-bottom: 2rem; 192 } 193 header h1 { 194 margin: 0; 195 } 196 .account-dropdown { 197 position: relative; 198 } 199 .account-trigger { 200 display: flex; 201 align-items: center; 202 gap: 0.5rem; 203 padding: 0.5rem 1rem; 204 background: transparent; 205 border: 1px solid var(--border-color-light); 206 border-radius: 4px; 207 cursor: pointer; 208 color: var(--text-primary); 209 } 210 .account-trigger:hover:not(:disabled) { 211 background: var(--bg-secondary); 212 } 213 .account-trigger:disabled { 214 opacity: 0.6; 215 cursor: not-allowed; 216 } 217 .account-trigger .account-handle { 218 font-weight: 500; 219 } 220 .dropdown-arrow { 221 font-size: 0.625rem; 222 color: var(--text-secondary); 223 } 224 .dropdown-menu { 225 position: absolute; 226 top: 100%; 227 right: 0; 228 margin-top: 0.25rem; 229 min-width: 200px; 230 background: var(--bg-card); 231 border: 1px solid var(--border-color); 232 border-radius: 8px; 233 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 234 z-index: 100; 235 overflow: hidden; 236 } 237 .dropdown-section { 238 padding: 0.5rem 0; 239 } 240 .dropdown-label { 241 display: block; 242 padding: 0.25rem 1rem; 243 font-size: 0.75rem; 244 color: var(--text-muted); 245 text-transform: uppercase; 246 letter-spacing: 0.05em; 247 } 248 .dropdown-item { 249 display: block; 250 width: 100%; 251 padding: 0.75rem 1rem; 252 background: transparent; 253 border: none; 254 text-align: left; 255 cursor: pointer; 256 color: var(--text-primary); 257 font-size: 0.875rem; 258 } 259 .dropdown-item:hover { 260 background: var(--bg-secondary); 261 } 262 .dropdown-item.logout-item { 263 color: var(--error-text); 264 } 265 .dropdown-divider { 266 height: 1px; 267 background: var(--border-color); 268 margin: 0; 269 } 270 section { 271 background: var(--bg-secondary); 272 padding: 1.5rem; 273 border-radius: 8px; 274 margin-bottom: 2rem; 275 } 276 section h2 { 277 margin: 0 0 1rem 0; 278 font-size: 1.25rem; 279 } 280 dl { 281 display: grid; 282 grid-template-columns: auto 1fr; 283 gap: 0.5rem 1rem; 284 margin: 0; 285 } 286 dt { 287 font-weight: 500; 288 color: var(--text-secondary); 289 } 290 dd { 291 margin: 0; 292 } 293 .mono { 294 font-family: monospace; 295 font-size: 0.875rem; 296 word-break: break-all; 297 } 298 .badge { 299 display: inline-block; 300 padding: 0.125rem 0.5rem; 301 border-radius: 4px; 302 font-size: 0.75rem; 303 margin-left: 0.5rem; 304 } 305 .badge.success { 306 background: var(--success-bg); 307 color: var(--success-text); 308 } 309 .badge.warning { 310 background: var(--warning-bg); 311 color: var(--warning-text); 312 } 313 .badge.admin { 314 background: var(--accent); 315 color: white; 316 } 317 .badge.deactivated { 318 background: var(--warning-bg); 319 color: var(--warning-text); 320 border: 1px solid #d4a03c; 321 } 322 .nav-grid { 323 display: grid; 324 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 325 gap: 1rem; 326 } 327 .nav-card { 328 display: block; 329 padding: 1.5rem; 330 background: var(--bg-card); 331 border: 1px solid var(--border-color); 332 border-radius: 8px; 333 text-decoration: none; 334 color: inherit; 335 transition: border-color 0.15s, box-shadow 0.15s; 336 } 337 .nav-card:hover { 338 border-color: var(--accent); 339 box-shadow: 0 2px 8px rgba(77, 166, 255, 0.15); 340 } 341 .nav-card h3 { 342 margin: 0 0 0.5rem 0; 343 color: var(--accent); 344 } 345 .nav-card p { 346 margin: 0; 347 color: var(--text-secondary); 348 font-size: 0.875rem; 349 } 350 .nav-card.admin-card { 351 border-color: var(--accent); 352 background: linear-gradient(135deg, var(--bg-card) 0%, rgba(77, 166, 255, 0.05) 100%); 353 } 354 .nav-card.admin-card:hover { 355 box-shadow: 0 2px 12px rgba(77, 166, 255, 0.25); 356 } 357 .loading { 358 text-align: center; 359 padding: 4rem; 360 color: var(--text-secondary); 361 } 362 .deactivated-banner { 363 background: var(--warning-bg); 364 border: 1px solid #d4a03c; 365 border-radius: 8px; 366 padding: 1rem 1.5rem; 367 margin-bottom: 2rem; 368 } 369 .deactivated-banner strong { 370 color: var(--warning-text); 371 font-size: 1rem; 372 } 373 .deactivated-banner p { 374 margin: 0.5rem 0 0 0; 375 color: var(--warning-text); 376 font-size: 0.875rem; 377 } 378</style>