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