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
6 const auth = getAuthState()
7 let dropdownOpen = $state(false)
8 let switching = $state(false)
9
10 $effect(() => {
11 if (!auth.loading && !auth.session) {
12 navigate('/login')
13 }
14 })
15
16 async function handleLogout() {
17 await logout()
18 navigate('/login')
19 }
20
21 async function handleSwitchAccount(did: string) {
22 switching = true
23 dropdownOpen = false
24 try {
25 await switchAccount(did)
26 } catch {
27 navigate('/login')
28 } finally {
29 switching = false
30 }
31 }
32
33 function toggleDropdown() {
34 dropdownOpen = !dropdownOpen
35 }
36
37 function closeDropdown(e: MouseEvent) {
38 const target = e.target as HTMLElement
39 if (!target.closest('.account-dropdown')) {
40 dropdownOpen = false
41 }
42 }
43
44 $effect(() => {
45 if (dropdownOpen) {
46 document.addEventListener('click', closeDropdown)
47 return () => document.removeEventListener('click', closeDropdown)
48 }
49 })
50
51 let otherAccounts = $derived(
52 auth.savedAccounts.filter(a => a.did !== auth.session?.did)
53 )
54</script>
55
56{#if auth.session}
57 <div class="dashboard">
58 <header>
59 <h1>{$_('dashboard.title')}</h1>
60 <div class="account-dropdown">
61 <button class="account-trigger" onclick={toggleDropdown} disabled={switching}>
62 <span class="account-handle">@{auth.session.handle}</span>
63 <span class="dropdown-arrow">{dropdownOpen ? '▲' : '▼'}</span>
64 </button>
65 {#if dropdownOpen}
66 <div class="dropdown-menu">
67 {#if otherAccounts.length > 0}
68 <div class="dropdown-section">
69 <span class="dropdown-label">{$_('dashboard.switchAccount')}</span>
70 {#each otherAccounts as account}
71 <button type="button" class="dropdown-item" onclick={() => handleSwitchAccount(account.did)}>
72 @{account.handle}
73 </button>
74 {/each}
75 </div>
76 <div class="dropdown-divider"></div>
77 {/if}
78 <button type="button" class="dropdown-item" onclick={() => { dropdownOpen = false; navigate('/login') }}>
79 {$_('dashboard.addAnotherAccount')}
80 </button>
81 <div class="dropdown-divider"></div>
82 <button type="button" class="dropdown-item logout-item" onclick={handleLogout}>
83 {$_('dashboard.signOut', { values: { handle: auth.session.handle } })}
84 </button>
85 </div>
86 {/if}
87 </div>
88 </header>
89
90 {#if auth.session.status === 'deactivated' || auth.session.active === false}
91 <div class="deactivated-banner">
92 <strong>{$_('dashboard.deactivatedTitle')}</strong>
93 <p>{$_('dashboard.deactivatedMessage')}</p>
94 </div>
95 {/if}
96
97 <section class="account-overview">
98 <h2>{$_('dashboard.accountOverview')}</h2>
99 <dl>
100 <dt>{$_('dashboard.handle')}</dt>
101 <dd>
102 @{auth.session.handle}
103 {#if auth.session.isAdmin}
104 <span class="badge admin">{$_('dashboard.admin')}</span>
105 {/if}
106 {#if auth.session.status === 'deactivated' || auth.session.active === false}
107 <span class="badge deactivated">{$_('dashboard.deactivated')}</span>
108 {/if}
109 </dd>
110 <dt>{$_('dashboard.did')}</dt>
111 <dd class="mono">{auth.session.did}</dd>
112 {#if auth.session.preferredChannel}
113 <dt>{$_('dashboard.primaryContact')}</dt>
114 <dd>
115 {#if auth.session.preferredChannel === 'email'}
116 {auth.session.email || $_('register.email')}
117 {:else if auth.session.preferredChannel === 'discord'}
118 {$_('register.discord')}
119 {:else if auth.session.preferredChannel === 'telegram'}
120 {$_('register.telegram')}
121 {:else if auth.session.preferredChannel === 'signal'}
122 {$_('register.signal')}
123 {:else}
124 {auth.session.preferredChannel}
125 {/if}
126 {#if auth.session.preferredChannelVerified}
127 <span class="badge success">{$_('dashboard.verified')}</span>
128 {:else}
129 <span class="badge warning">{$_('dashboard.unverified')}</span>
130 {/if}
131 </dd>
132 {:else if auth.session.email}
133 <dt>{$_('register.email')}</dt>
134 <dd>
135 {auth.session.email}
136 {#if auth.session.emailConfirmed}
137 <span class="badge success">{$_('dashboard.verified')}</span>
138 {:else}
139 <span class="badge warning">{$_('dashboard.unverified')}</span>
140 {/if}
141 </dd>
142 {/if}
143 </dl>
144 </section>
145
146 <nav class="nav-grid">
147 <a href="#/app-passwords" class="nav-card">
148 <h3>{$_('dashboard.navAppPasswords')}</h3>
149 <p>{$_('dashboard.navAppPasswordsDesc')}</p>
150 </a>
151 <a href="#/sessions" class="nav-card">
152 <h3>{$_('dashboard.navSessions')}</h3>
153 <p>{$_('dashboard.navSessionsDesc')}</p>
154 </a>
155 <a href="#/invite-codes" class="nav-card">
156 <h3>{$_('dashboard.navInviteCodes')}</h3>
157 <p>{$_('dashboard.navInviteCodesDesc')}</p>
158 </a>
159 <a href="#/settings" class="nav-card">
160 <h3>{$_('dashboard.navSettings')}</h3>
161 <p>{$_('dashboard.navSettingsDesc')}</p>
162 </a>
163 <a href="#/security" class="nav-card">
164 <h3>{$_('dashboard.navSecurity')}</h3>
165 <p>{$_('dashboard.navSecurityDesc')}</p>
166 </a>
167 <a href="#/comms" class="nav-card">
168 <h3>{$_('dashboard.navComms')}</h3>
169 <p>{$_('dashboard.navCommsDesc')}</p>
170 </a>
171 <a href="#/repo" class="nav-card">
172 <h3>{$_('dashboard.navRepo')}</h3>
173 <p>{$_('dashboard.navRepoDesc')}</p>
174 </a>
175 {#if auth.session.isAdmin}
176 <a href="#/admin" class="nav-card admin-card">
177 <h3>{$_('dashboard.navAdmin')}</h3>
178 <p>{$_('dashboard.navAdminDesc')}</p>
179 </a>
180 {/if}
181 </nav>
182 </div>
183{:else if auth.loading}
184 <div class="loading">{$_('common.loading')}</div>
185{/if}
186
187<style>
188 .dashboard {
189 max-width: var(--width-lg);
190 margin: 0 auto;
191 padding: var(--space-7);
192 }
193
194 header {
195 display: flex;
196 justify-content: space-between;
197 align-items: center;
198 margin-bottom: var(--space-7);
199 }
200
201 header h1 {
202 margin: 0;
203 }
204
205 .account-dropdown {
206 position: relative;
207 }
208
209 .account-trigger {
210 display: flex;
211 align-items: center;
212 gap: var(--space-3);
213 padding: var(--space-3) var(--space-5);
214 background: transparent;
215 border: 1px solid var(--border-color);
216 border-radius: var(--radius-md);
217 cursor: pointer;
218 color: var(--text-primary);
219 }
220
221 .account-trigger:hover:not(:disabled) {
222 background: var(--bg-secondary);
223 }
224
225 .account-trigger:disabled {
226 opacity: 0.6;
227 cursor: not-allowed;
228 }
229
230 .account-trigger .account-handle {
231 font-weight: var(--font-medium);
232 }
233
234 .dropdown-arrow {
235 font-size: 0.625rem;
236 color: var(--text-secondary);
237 }
238
239 .dropdown-menu {
240 position: absolute;
241 top: 100%;
242 right: 0;
243 margin-top: var(--space-2);
244 min-width: 200px;
245 background: var(--bg-card);
246 border: 1px solid var(--border-color);
247 border-radius: var(--radius-xl);
248 box-shadow: var(--shadow-lg);
249 z-index: 100;
250 overflow: hidden;
251 }
252
253 .dropdown-section {
254 padding: var(--space-3) 0;
255 }
256
257 .dropdown-label {
258 display: block;
259 padding: var(--space-2) var(--space-5);
260 font-size: var(--text-xs);
261 color: var(--text-muted);
262 text-transform: uppercase;
263 letter-spacing: 0.05em;
264 }
265
266 .dropdown-item {
267 display: block;
268 width: 100%;
269 padding: var(--space-4) var(--space-5);
270 background: transparent;
271 border: none;
272 text-align: left;
273 cursor: pointer;
274 color: var(--text-primary);
275 font-size: var(--text-sm);
276 }
277
278 .dropdown-item:hover {
279 background: var(--bg-secondary);
280 }
281
282 .dropdown-item.logout-item {
283 color: var(--error-text);
284 }
285
286 .dropdown-divider {
287 height: 1px;
288 background: var(--border-color);
289 margin: 0;
290 }
291
292 section {
293 background: var(--bg-secondary);
294 padding: var(--space-6);
295 border-radius: var(--radius-xl);
296 margin-bottom: var(--space-7);
297 }
298
299 section h2 {
300 margin: 0 0 var(--space-4) 0;
301 font-size: var(--text-xl);
302 }
303
304 dl {
305 display: grid;
306 grid-template-columns: auto 1fr;
307 gap: var(--space-3) var(--space-5);
308 margin: 0;
309 }
310
311 dt {
312 font-weight: var(--font-medium);
313 color: var(--text-secondary);
314 }
315
316 dd {
317 margin: 0;
318 }
319
320 .mono {
321 font-family: ui-monospace, monospace;
322 font-size: var(--text-sm);
323 word-break: break-all;
324 }
325
326 .badge {
327 display: inline-block;
328 padding: var(--space-1) var(--space-3);
329 border-radius: var(--radius-md);
330 font-size: var(--text-xs);
331 margin-left: var(--space-3);
332 }
333
334 .badge.success {
335 background: var(--success-bg);
336 color: var(--success-text);
337 }
338
339 .badge.warning {
340 background: var(--warning-bg);
341 color: var(--warning-text);
342 }
343
344 .badge.admin {
345 background: var(--accent);
346 color: var(--text-inverse);
347 }
348
349 .badge.deactivated {
350 background: var(--warning-bg);
351 color: var(--warning-text);
352 border: 1px solid var(--warning-border);
353 }
354
355 .nav-grid {
356 display: grid;
357 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
358 gap: var(--space-4);
359 }
360
361 .nav-card {
362 display: block;
363 padding: var(--space-6);
364 background: var(--bg-card);
365 border: 1px solid var(--border-color);
366 border-radius: var(--radius-xl);
367 text-decoration: none;
368 color: inherit;
369 transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
370 }
371
372 .nav-card:hover {
373 border-color: var(--accent);
374 box-shadow: 0 2px 8px var(--accent-muted);
375 }
376
377 .nav-card h3 {
378 margin: 0 0 var(--space-3) 0;
379 color: var(--accent);
380 }
381
382 .nav-card p {
383 margin: 0;
384 color: var(--text-secondary);
385 font-size: var(--text-sm);
386 }
387
388 .nav-card.admin-card {
389 border-color: var(--accent);
390 background: linear-gradient(135deg, var(--bg-card) 0%, var(--accent-muted) 100%);
391 }
392
393 .nav-card.admin-card:hover {
394 box-shadow: 0 2px 12px var(--accent-muted);
395 }
396
397 .loading {
398 text-align: center;
399 padding: var(--space-9);
400 color: var(--text-secondary);
401 }
402
403 .deactivated-banner {
404 background: var(--warning-bg);
405 border: 1px solid var(--warning-border);
406 border-radius: var(--radius-xl);
407 padding: var(--space-5) var(--space-6);
408 margin-bottom: var(--space-7);
409 }
410
411 .deactivated-banner strong {
412 color: var(--warning-text);
413 font-size: var(--text-base);
414 }
415
416 .deactivated-banner p {
417 margin: var(--space-3) 0 0 0;
418 color: var(--warning-text);
419 font-size: var(--text-sm);
420 }
421</style>