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