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="#/notifications" class="nav-card"> 159 <h3>Notification Preferences</h3> 160 <p>Discord, Telegram, Signal channels</p> 161 </a> 162 <a href="#/repo" class="nav-card"> 163 <h3>Repository Explorer</h3> 164 <p>Browse and manage raw AT Protocol records</p> 165 </a> 166 {#if auth.session.isAdmin} 167 <a href="#/admin" class="nav-card admin-card"> 168 <h3>Admin Panel</h3> 169 <p>Server stats and admin operations</p> 170 </a> 171 {/if} 172 </nav> 173 </div> 174{:else if auth.loading} 175 <div class="loading">Loading...</div> 176{/if} 177<style> 178 .dashboard { 179 max-width: 800px; 180 margin: 0 auto; 181 padding: 2rem; 182 } 183 header { 184 display: flex; 185 justify-content: space-between; 186 align-items: center; 187 margin-bottom: 2rem; 188 } 189 header h1 { 190 margin: 0; 191 } 192 .account-dropdown { 193 position: relative; 194 } 195 .account-trigger { 196 display: flex; 197 align-items: center; 198 gap: 0.5rem; 199 padding: 0.5rem 1rem; 200 background: transparent; 201 border: 1px solid var(--border-color-light); 202 border-radius: 4px; 203 cursor: pointer; 204 color: var(--text-primary); 205 } 206 .account-trigger:hover:not(:disabled) { 207 background: var(--bg-secondary); 208 } 209 .account-trigger:disabled { 210 opacity: 0.6; 211 cursor: not-allowed; 212 } 213 .account-trigger .account-handle { 214 font-weight: 500; 215 } 216 .dropdown-arrow { 217 font-size: 0.625rem; 218 color: var(--text-secondary); 219 } 220 .dropdown-menu { 221 position: absolute; 222 top: 100%; 223 right: 0; 224 margin-top: 0.25rem; 225 min-width: 200px; 226 background: var(--bg-card); 227 border: 1px solid var(--border-color); 228 border-radius: 8px; 229 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 230 z-index: 100; 231 overflow: hidden; 232 } 233 .dropdown-section { 234 padding: 0.5rem 0; 235 } 236 .dropdown-label { 237 display: block; 238 padding: 0.25rem 1rem; 239 font-size: 0.75rem; 240 color: var(--text-muted); 241 text-transform: uppercase; 242 letter-spacing: 0.05em; 243 } 244 .dropdown-item { 245 display: block; 246 width: 100%; 247 padding: 0.75rem 1rem; 248 background: transparent; 249 border: none; 250 text-align: left; 251 cursor: pointer; 252 color: var(--text-primary); 253 font-size: 0.875rem; 254 } 255 .dropdown-item:hover { 256 background: var(--bg-secondary); 257 } 258 .dropdown-item.logout-item { 259 color: var(--error-text); 260 } 261 .dropdown-divider { 262 height: 1px; 263 background: var(--border-color); 264 margin: 0; 265 } 266 section { 267 background: var(--bg-secondary); 268 padding: 1.5rem; 269 border-radius: 8px; 270 margin-bottom: 2rem; 271 } 272 section h2 { 273 margin: 0 0 1rem 0; 274 font-size: 1.25rem; 275 } 276 dl { 277 display: grid; 278 grid-template-columns: auto 1fr; 279 gap: 0.5rem 1rem; 280 margin: 0; 281 } 282 dt { 283 font-weight: 500; 284 color: var(--text-secondary); 285 } 286 dd { 287 margin: 0; 288 } 289 .mono { 290 font-family: monospace; 291 font-size: 0.875rem; 292 word-break: break-all; 293 } 294 .badge { 295 display: inline-block; 296 padding: 0.125rem 0.5rem; 297 border-radius: 4px; 298 font-size: 0.75rem; 299 margin-left: 0.5rem; 300 } 301 .badge.success { 302 background: var(--success-bg); 303 color: var(--success-text); 304 } 305 .badge.warning { 306 background: var(--warning-bg); 307 color: var(--warning-text); 308 } 309 .badge.admin { 310 background: var(--accent); 311 color: white; 312 } 313 .badge.deactivated { 314 background: var(--warning-bg); 315 color: var(--warning-text); 316 border: 1px solid #d4a03c; 317 } 318 .nav-grid { 319 display: grid; 320 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 321 gap: 1rem; 322 } 323 .nav-card { 324 display: block; 325 padding: 1.5rem; 326 background: var(--bg-card); 327 border: 1px solid var(--border-color); 328 border-radius: 8px; 329 text-decoration: none; 330 color: inherit; 331 transition: border-color 0.15s, box-shadow 0.15s; 332 } 333 .nav-card:hover { 334 border-color: var(--accent); 335 box-shadow: 0 2px 8px rgba(77, 166, 255, 0.15); 336 } 337 .nav-card h3 { 338 margin: 0 0 0.5rem 0; 339 color: var(--accent); 340 } 341 .nav-card p { 342 margin: 0; 343 color: var(--text-secondary); 344 font-size: 0.875rem; 345 } 346 .nav-card.admin-card { 347 border-color: var(--accent); 348 background: linear-gradient(135deg, var(--bg-card) 0%, rgba(77, 166, 255, 0.05) 100%); 349 } 350 .nav-card.admin-card:hover { 351 box-shadow: 0 2px 12px rgba(77, 166, 255, 0.25); 352 } 353 .loading { 354 text-align: center; 355 padding: 4rem; 356 color: var(--text-secondary); 357 } 358 .deactivated-banner { 359 background: var(--warning-bg); 360 border: 1px solid #d4a03c; 361 border-radius: 8px; 362 padding: 1rem 1.5rem; 363 margin-bottom: 2rem; 364 } 365 .deactivated-banner strong { 366 color: var(--warning-text); 367 font-size: 1rem; 368 } 369 .deactivated-banner p { 370 margin: 0.5rem 0 0 0; 371 color: var(--warning-text); 372 font-size: 0.875rem; 373 } 374</style>