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 const auth = getAuthState()
5 let dropdownOpen = $state(false)
6 let switching = $state(false)
7 $effect(() => {
8 if (!auth.loading && !auth.session) {
9 navigate('/login')
10 }
11 })
12 async function handleLogout() {
13 await logout()
14 navigate('/login')
15 }
16 async function handleSwitchAccount(did: string) {
17 switching = true
18 dropdownOpen = false
19 try {
20 await switchAccount(did)
21 } catch {
22 navigate('/login')
23 } finally {
24 switching = false
25 }
26 }
27 function toggleDropdown() {
28 dropdownOpen = !dropdownOpen
29 }
30 function closeDropdown(e: MouseEvent) {
31 const target = e.target as HTMLElement
32 if (!target.closest('.account-dropdown')) {
33 dropdownOpen = false
34 }
35 }
36 $effect(() => {
37 if (dropdownOpen) {
38 document.addEventListener('click', closeDropdown)
39 return () => document.removeEventListener('click', closeDropdown)
40 }
41 })
42 let otherAccounts = $derived(
43 auth.savedAccounts.filter(a => a.did !== auth.session?.did)
44 )
45</script>
46{#if auth.session}
47 <div class="dashboard">
48 <header>
49 <h1>Dashboard</h1>
50 <div class="account-dropdown">
51 <button class="account-trigger" onclick={toggleDropdown} disabled={switching}>
52 <span class="account-handle">@{auth.session.handle}</span>
53 <span class="dropdown-arrow">{dropdownOpen ? '▲' : '▼'}</span>
54 </button>
55 {#if dropdownOpen}
56 <div class="dropdown-menu">
57 {#if otherAccounts.length > 0}
58 <div class="dropdown-section">
59 <span class="dropdown-label">Switch Account</span>
60 {#each otherAccounts as account}
61 <button
62 type="button"
63 class="dropdown-item"
64 onclick={() => handleSwitchAccount(account.did)}
65 >
66 @{account.handle}
67 </button>
68 {/each}
69 </div>
70 <div class="dropdown-divider"></div>
71 {/if}
72 <button
73 type="button"
74 class="dropdown-item"
75 onclick={() => { dropdownOpen = false; navigate('/login') }}
76 >
77 Add another account
78 </button>
79 <div class="dropdown-divider"></div>
80 <button type="button" class="dropdown-item logout-item" onclick={handleLogout}>
81 Sign out @{auth.session.handle}
82 </button>
83 </div>
84 {/if}
85 </div>
86 </header>
87 {#if auth.session.status === 'deactivated' || auth.session.active === false}
88 <div class="deactivated-banner">
89 <strong>Account Deactivated</strong>
90 <p>Your account is currently deactivated. This typically happens during account migration. Some features may be limited until your account is reactivated.</p>
91 </div>
92 {/if}
93 <section class="account-overview">
94 <h2>Account Overview</h2>
95 <dl>
96 <dt>Handle</dt>
97 <dd>
98 @{auth.session.handle}
99 {#if auth.session.isAdmin}
100 <span class="badge admin">Admin</span>
101 {/if}
102 {#if auth.session.status === 'deactivated' || auth.session.active === false}
103 <span class="badge deactivated">Deactivated</span>
104 {/if}
105 </dd>
106 <dt>DID</dt>
107 <dd class="mono">{auth.session.did}</dd>
108 {#if auth.session.preferredChannel}
109 <dt>Primary Contact</dt>
110 <dd>
111 {#if auth.session.preferredChannel === 'email'}
112 {auth.session.email || 'Email'}
113 {:else if auth.session.preferredChannel === 'discord'}
114 Discord
115 {:else if auth.session.preferredChannel === 'telegram'}
116 Telegram
117 {:else if auth.session.preferredChannel === 'signal'}
118 Signal
119 {:else}
120 {auth.session.preferredChannel}
121 {/if}
122 {#if auth.session.preferredChannelVerified}
123 <span class="badge success">Verified</span>
124 {:else}
125 <span class="badge warning">Unverified</span>
126 {/if}
127 </dd>
128 {:else if auth.session.email}
129 <dt>Email</dt>
130 <dd>
131 {auth.session.email}
132 {#if auth.session.emailConfirmed}
133 <span class="badge success">Verified</span>
134 {:else}
135 <span class="badge warning">Unverified</span>
136 {/if}
137 </dd>
138 {/if}
139 </dl>
140 </section>
141 <nav class="nav-grid">
142 <a href="#/app-passwords" class="nav-card">
143 <h3>App Passwords</h3>
144 <p>Manage passwords for third-party apps</p>
145 </a>
146 <a href="#/sessions" class="nav-card">
147 <h3>Active Sessions</h3>
148 <p>View and manage your login sessions</p>
149 </a>
150 <a href="#/invite-codes" class="nav-card">
151 <h3>Invite Codes</h3>
152 <p>View and create invite codes</p>
153 </a>
154 <a href="#/settings" class="nav-card">
155 <h3>Account Settings</h3>
156 <p>Email, password, handle, and more</p>
157 </a>
158 <a href="#/security" class="nav-card">
159 <h3>Security</h3>
160 <p>Two-factor authentication</p>
161 </a>
162 <a href="#/notifications" class="nav-card">
163 <h3>Notification Preferences</h3>
164 <p>Discord, Telegram, Signal channels</p>
165 </a>
166 <a href="#/repo" class="nav-card">
167 <h3>Repository Explorer</h3>
168 <p>Browse and manage raw AT Protocol records</p>
169 </a>
170 {#if auth.session.isAdmin}
171 <a href="#/admin" class="nav-card admin-card">
172 <h3>Admin Panel</h3>
173 <p>Server stats and admin operations</p>
174 </a>
175 {/if}
176 </nav>
177 </div>
178{:else if auth.loading}
179 <div class="loading">Loading...</div>
180{/if}
181<style>
182 .dashboard {
183 max-width: 800px;
184 margin: 0 auto;
185 padding: 2rem;
186 }
187 header {
188 display: flex;
189 justify-content: space-between;
190 align-items: center;
191 margin-bottom: 2rem;
192 }
193 header h1 {
194 margin: 0;
195 }
196 .account-dropdown {
197 position: relative;
198 }
199 .account-trigger {
200 display: flex;
201 align-items: center;
202 gap: 0.5rem;
203 padding: 0.5rem 1rem;
204 background: transparent;
205 border: 1px solid var(--border-color-light);
206 border-radius: 4px;
207 cursor: pointer;
208 color: var(--text-primary);
209 }
210 .account-trigger:hover:not(:disabled) {
211 background: var(--bg-secondary);
212 }
213 .account-trigger:disabled {
214 opacity: 0.6;
215 cursor: not-allowed;
216 }
217 .account-trigger .account-handle {
218 font-weight: 500;
219 }
220 .dropdown-arrow {
221 font-size: 0.625rem;
222 color: var(--text-secondary);
223 }
224 .dropdown-menu {
225 position: absolute;
226 top: 100%;
227 right: 0;
228 margin-top: 0.25rem;
229 min-width: 200px;
230 background: var(--bg-card);
231 border: 1px solid var(--border-color);
232 border-radius: 8px;
233 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
234 z-index: 100;
235 overflow: hidden;
236 }
237 .dropdown-section {
238 padding: 0.5rem 0;
239 }
240 .dropdown-label {
241 display: block;
242 padding: 0.25rem 1rem;
243 font-size: 0.75rem;
244 color: var(--text-muted);
245 text-transform: uppercase;
246 letter-spacing: 0.05em;
247 }
248 .dropdown-item {
249 display: block;
250 width: 100%;
251 padding: 0.75rem 1rem;
252 background: transparent;
253 border: none;
254 text-align: left;
255 cursor: pointer;
256 color: var(--text-primary);
257 font-size: 0.875rem;
258 }
259 .dropdown-item:hover {
260 background: var(--bg-secondary);
261 }
262 .dropdown-item.logout-item {
263 color: var(--error-text);
264 }
265 .dropdown-divider {
266 height: 1px;
267 background: var(--border-color);
268 margin: 0;
269 }
270 section {
271 background: var(--bg-secondary);
272 padding: 1.5rem;
273 border-radius: 8px;
274 margin-bottom: 2rem;
275 }
276 section h2 {
277 margin: 0 0 1rem 0;
278 font-size: 1.25rem;
279 }
280 dl {
281 display: grid;
282 grid-template-columns: auto 1fr;
283 gap: 0.5rem 1rem;
284 margin: 0;
285 }
286 dt {
287 font-weight: 500;
288 color: var(--text-secondary);
289 }
290 dd {
291 margin: 0;
292 }
293 .mono {
294 font-family: monospace;
295 font-size: 0.875rem;
296 word-break: break-all;
297 }
298 .badge {
299 display: inline-block;
300 padding: 0.125rem 0.5rem;
301 border-radius: 4px;
302 font-size: 0.75rem;
303 margin-left: 0.5rem;
304 }
305 .badge.success {
306 background: var(--success-bg);
307 color: var(--success-text);
308 }
309 .badge.warning {
310 background: var(--warning-bg);
311 color: var(--warning-text);
312 }
313 .badge.admin {
314 background: var(--accent);
315 color: white;
316 }
317 .badge.deactivated {
318 background: var(--warning-bg);
319 color: var(--warning-text);
320 border: 1px solid #d4a03c;
321 }
322 .nav-grid {
323 display: grid;
324 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
325 gap: 1rem;
326 }
327 .nav-card {
328 display: block;
329 padding: 1.5rem;
330 background: var(--bg-card);
331 border: 1px solid var(--border-color);
332 border-radius: 8px;
333 text-decoration: none;
334 color: inherit;
335 transition: border-color 0.15s, box-shadow 0.15s;
336 }
337 .nav-card:hover {
338 border-color: var(--accent);
339 box-shadow: 0 2px 8px rgba(77, 166, 255, 0.15);
340 }
341 .nav-card h3 {
342 margin: 0 0 0.5rem 0;
343 color: var(--accent);
344 }
345 .nav-card p {
346 margin: 0;
347 color: var(--text-secondary);
348 font-size: 0.875rem;
349 }
350 .nav-card.admin-card {
351 border-color: var(--accent);
352 background: linear-gradient(135deg, var(--bg-card) 0%, rgba(77, 166, 255, 0.05) 100%);
353 }
354 .nav-card.admin-card:hover {
355 box-shadow: 0 2px 12px rgba(77, 166, 255, 0.25);
356 }
357 .loading {
358 text-align: center;
359 padding: 4rem;
360 color: var(--text-secondary);
361 }
362 .deactivated-banner {
363 background: var(--warning-bg);
364 border: 1px solid #d4a03c;
365 border-radius: 8px;
366 padding: 1rem 1.5rem;
367 margin-bottom: 2rem;
368 }
369 .deactivated-banner strong {
370 color: var(--warning-text);
371 font-size: 1rem;
372 }
373 .deactivated-banner p {
374 margin: 0.5rem 0 0 0;
375 color: var(--warning-text);
376 font-size: 0.875rem;
377 }
378</style>