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 === 'migrated'}
103 <div class="migrated-banner">
104 <strong>{$_('dashboard.migratedTitle')}</strong>
105 <p>{$_('dashboard.migratedMessage', { values: { pds: auth.session.migratedToPds || 'another PDS' } })}</p>
106 </div>
107 {:else if auth.session.status === 'deactivated' || auth.session.active === false}
108 <div class="deactivated-banner">
109 <strong>{$_('dashboard.deactivatedTitle')}</strong>
110 <p>{$_('dashboard.deactivatedMessage')}</p>
111 </div>
112 {/if}
113
114 <section class="account-overview">
115 <h2>{$_('dashboard.accountOverview')}</h2>
116 <dl>
117 <dt>{$_('dashboard.handle')}</dt>
118 <dd>
119 @{auth.session.handle}
120 {#if auth.session.isAdmin}
121 <span class="badge admin">{$_('dashboard.admin')}</span>
122 {/if}
123 {#if auth.session.status === 'migrated'}
124 <span class="badge migrated">{$_('dashboard.migrated')}</span>
125 {:else if auth.session.status === 'deactivated' || auth.session.active === false}
126 <span class="badge deactivated">{$_('dashboard.deactivated')}</span>
127 {/if}
128 </dd>
129 <dt>{$_('dashboard.did')}</dt>
130 <dd class="mono">{auth.session.did}</dd>
131 {#if auth.session.preferredChannel}
132 <dt>{$_('dashboard.primaryContact')}</dt>
133 <dd>
134 {#if auth.session.preferredChannel === 'email'}
135 {auth.session.email || $_('register.email')}
136 {:else if auth.session.preferredChannel === 'discord'}
137 {$_('register.discord')}
138 {:else if auth.session.preferredChannel === 'telegram'}
139 {$_('register.telegram')}
140 {:else if auth.session.preferredChannel === 'signal'}
141 {$_('register.signal')}
142 {:else}
143 {auth.session.preferredChannel}
144 {/if}
145 {#if auth.session.preferredChannelVerified}
146 <span class="badge success">{$_('dashboard.verified')}</span>
147 {:else}
148 <span class="badge warning">{$_('dashboard.unverified')}</span>
149 {/if}
150 </dd>
151 {:else if auth.session.email}
152 <dt>{$_('register.email')}</dt>
153 <dd>
154 {auth.session.email}
155 {#if auth.session.emailConfirmed}
156 <span class="badge success">{$_('dashboard.verified')}</span>
157 {:else}
158 <span class="badge warning">{$_('dashboard.unverified')}</span>
159 {/if}
160 </dd>
161 {/if}
162 </dl>
163 </section>
164
165 <nav class="nav-grid">
166 {#if auth.session.status === 'migrated'}
167 <a href="#/did-document" class="nav-card migrated-card">
168 <h3>{$_('dashboard.navDidDocument')}</h3>
169 <p>{$_('dashboard.navDidDocumentDesc')}</p>
170 </a>
171 <a href="#/sessions" class="nav-card">
172 <h3>{$_('dashboard.navSessions')}</h3>
173 <p>{$_('dashboard.navSessionsDesc')}</p>
174 </a>
175 <a href="#/security" class="nav-card">
176 <h3>{$_('dashboard.navSecurity')}</h3>
177 <p>{$_('dashboard.navSecurityDesc')}</p>
178 </a>
179 <a href="#/migrate" class="nav-card">
180 <h3>{$_('dashboard.navMigrateAgain')}</h3>
181 <p>{$_('dashboard.navMigrateAgainDesc')}</p>
182 </a>
183 {:else}
184 <a href="#/app-passwords" class="nav-card">
185 <h3>{$_('dashboard.navAppPasswords')}</h3>
186 <p>{$_('dashboard.navAppPasswordsDesc')}</p>
187 </a>
188 <a href="#/sessions" class="nav-card">
189 <h3>{$_('dashboard.navSessions')}</h3>
190 <p>{$_('dashboard.navSessionsDesc')}</p>
191 </a>
192 {#if inviteCodesEnabled && auth.session.isAdmin}
193 <a href="#/invite-codes" class="nav-card">
194 <h3>{$_('dashboard.navInviteCodes')}</h3>
195 <p>{$_('dashboard.navInviteCodesDesc')}</p>
196 </a>
197 {/if}
198 <a href="#/settings" class="nav-card">
199 <h3>{$_('dashboard.navSettings')}</h3>
200 <p>{$_('dashboard.navSettingsDesc')}</p>
201 </a>
202 <a href="#/security" class="nav-card">
203 <h3>{$_('dashboard.navSecurity')}</h3>
204 <p>{$_('dashboard.navSecurityDesc')}</p>
205 </a>
206 <a href="#/comms" class="nav-card">
207 <h3>{$_('dashboard.navComms')}</h3>
208 <p>{$_('dashboard.navCommsDesc')}</p>
209 </a>
210 <a href="#/repo" class="nav-card">
211 <h3>{$_('dashboard.navRepo')}</h3>
212 <p>{$_('dashboard.navRepoDesc')}</p>
213 </a>
214 <a href="#/controllers" class="nav-card">
215 <h3>{$_('dashboard.navDelegation')}</h3>
216 <p>{$_('dashboard.navDelegationDesc')}</p>
217 </a>
218 <a href="#/migrate" class="nav-card">
219 <h3>{$_('migration.navTitle')}</h3>
220 <p>{$_('migration.navDesc')}</p>
221 </a>
222 {#if auth.session.isAdmin}
223 <a href="#/admin" class="nav-card admin-card">
224 <h3>{$_('dashboard.navAdmin')}</h3>
225 <p>{$_('dashboard.navAdminDesc')}</p>
226 </a>
227 {/if}
228 {/if}
229 </nav>
230 </div>
231{:else if auth.loading}
232 <div class="loading">{$_('common.loading')}</div>
233{/if}
234
235<style>
236 .dashboard {
237 max-width: var(--width-xl);
238 margin: 0 auto;
239 padding: var(--space-7);
240 }
241
242 header {
243 display: flex;
244 justify-content: space-between;
245 align-items: center;
246 margin-bottom: var(--space-7);
247 }
248
249 header h1 {
250 margin: 0;
251 }
252
253 .account-dropdown {
254 position: relative;
255 }
256
257 .account-trigger {
258 display: flex;
259 align-items: center;
260 gap: var(--space-3);
261 padding: var(--space-3) var(--space-5);
262 background: transparent;
263 border: 1px solid var(--border-color);
264 border-radius: var(--radius-md);
265 cursor: pointer;
266 color: var(--text-primary);
267 }
268
269 .account-trigger:hover:not(:disabled) {
270 background: var(--bg-secondary);
271 }
272
273 .account-trigger:disabled {
274 opacity: 0.6;
275 cursor: not-allowed;
276 }
277
278 .account-trigger .account-handle {
279 font-weight: var(--font-medium);
280 }
281
282 .dropdown-arrow {
283 font-size: 0.625rem;
284 color: var(--text-secondary);
285 }
286
287 .dropdown-menu {
288 position: absolute;
289 top: 100%;
290 right: 0;
291 margin-top: var(--space-2);
292 min-width: 200px;
293 background: var(--bg-card);
294 border: 1px solid var(--border-color);
295 border-radius: var(--radius-xl);
296 box-shadow: var(--shadow-lg);
297 z-index: 100;
298 overflow: hidden;
299 }
300
301 .dropdown-section {
302 padding: var(--space-3) 0;
303 }
304
305 .dropdown-label {
306 display: block;
307 padding: var(--space-2) var(--space-5);
308 font-size: var(--text-xs);
309 color: var(--text-muted);
310 text-transform: uppercase;
311 letter-spacing: 0.05em;
312 }
313
314 .dropdown-item {
315 display: block;
316 width: 100%;
317 padding: var(--space-4) var(--space-5);
318 background: transparent;
319 border: none;
320 text-align: left;
321 cursor: pointer;
322 color: var(--text-primary);
323 font-size: var(--text-sm);
324 }
325
326 .dropdown-item:hover {
327 background: var(--bg-secondary);
328 }
329
330 .dropdown-item.logout-item {
331 color: var(--error-text);
332 }
333
334 .dropdown-divider {
335 height: 1px;
336 background: var(--border-color);
337 margin: 0;
338 }
339
340 section {
341 background: var(--bg-secondary);
342 padding: var(--space-6);
343 border-radius: var(--radius-xl);
344 margin-bottom: var(--space-7);
345 }
346
347 section h2 {
348 margin: 0 0 var(--space-4) 0;
349 font-size: var(--text-xl);
350 }
351
352 dl {
353 display: grid;
354 grid-template-columns: auto 1fr;
355 gap: var(--space-3) var(--space-5);
356 margin: 0;
357 }
358
359 dt {
360 font-weight: var(--font-medium);
361 color: var(--text-secondary);
362 }
363
364 dd {
365 margin: 0;
366 }
367
368 .mono {
369 font-family: ui-monospace, monospace;
370 font-size: var(--text-sm);
371 word-break: break-all;
372 }
373
374 .badge {
375 display: inline-block;
376 padding: var(--space-1) var(--space-3);
377 border-radius: var(--radius-md);
378 font-size: var(--text-xs);
379 margin-left: var(--space-3);
380 }
381
382 .badge.success {
383 background: var(--success-bg);
384 color: var(--success-text);
385 }
386
387 .badge.warning {
388 background: var(--warning-bg);
389 color: var(--warning-text);
390 }
391
392 .badge.admin {
393 background: var(--accent);
394 color: var(--text-inverse);
395 }
396
397 .badge.deactivated {
398 background: var(--warning-bg);
399 color: var(--warning-text);
400 border: 1px solid var(--warning-border);
401 }
402
403 .badge.migrated {
404 background: var(--info-bg, #e0f2fe);
405 color: var(--info-text, #0369a1);
406 border: 1px solid var(--info-border, #7dd3fc);
407 }
408
409 .nav-grid {
410 display: grid;
411 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
412 gap: var(--space-4);
413 }
414
415 .nav-card {
416 display: block;
417 padding: var(--space-6);
418 background: var(--bg-card);
419 border: 1px solid var(--border-color);
420 border-radius: var(--radius-xl);
421 text-decoration: none;
422 color: inherit;
423 transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
424 }
425
426 .nav-card:hover {
427 border-color: var(--accent);
428 box-shadow: 0 2px 8px var(--accent-muted);
429 }
430
431 .nav-card h3 {
432 margin: 0 0 var(--space-3) 0;
433 color: var(--accent);
434 }
435
436 .nav-card p {
437 margin: 0;
438 color: var(--text-secondary);
439 font-size: var(--text-sm);
440 }
441
442 .nav-card.admin-card {
443 border-color: var(--accent);
444 background: linear-gradient(135deg, var(--bg-card) 0%, var(--accent-muted) 100%);
445 }
446
447 .nav-card.admin-card:hover {
448 box-shadow: 0 2px 12px var(--accent-muted);
449 }
450
451 .loading {
452 text-align: center;
453 padding: var(--space-9);
454 color: var(--text-secondary);
455 }
456
457 .deactivated-banner {
458 background: var(--warning-bg);
459 border: 1px solid var(--warning-border);
460 border-radius: var(--radius-xl);
461 padding: var(--space-5) var(--space-6);
462 margin-bottom: var(--space-7);
463 }
464
465 .deactivated-banner strong {
466 color: var(--warning-text);
467 font-size: var(--text-base);
468 }
469
470 .deactivated-banner p {
471 margin: var(--space-3) 0 0 0;
472 color: var(--warning-text);
473 font-size: var(--text-sm);
474 }
475
476 .migrated-banner {
477 background: var(--info-bg, #e0f2fe);
478 border: 1px solid var(--info-border, #7dd3fc);
479 border-radius: var(--radius-xl);
480 padding: var(--space-5) var(--space-6);
481 margin-bottom: var(--space-7);
482 }
483
484 .migrated-banner strong {
485 color: var(--info-text, #0369a1);
486 font-size: var(--text-base);
487 }
488
489 .migrated-banner p {
490 margin: var(--space-3) 0 0 0;
491 color: var(--info-text, #0369a1);
492 font-size: var(--text-sm);
493 }
494
495 .nav-card.migrated-card {
496 border-color: var(--info-border, #7dd3fc);
497 background: linear-gradient(135deg, var(--bg-card) 0%, var(--info-bg, #e0f2fe) 100%);
498 }
499
500 .nav-card.migrated-card:hover {
501 box-shadow: 0 2px 12px var(--info-bg, #e0f2fe);
502 }
503
504 .nav-card.migrated-card h3 {
505 color: var(--info-text, #0369a1);
506 }
507</style>