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