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="#/notifications" class="nav-card">
159 <h3>Notification Preferences</h3>
160 <p>Discord, Telegram, Signal channels</p>
161 </a>
162 <a href="#/repo" class="nav-card">
163 <h3>Repository Explorer</h3>
164 <p>Browse and manage raw AT Protocol records</p>
165 </a>
166 {#if auth.session.isAdmin}
167 <a href="#/admin" class="nav-card admin-card">
168 <h3>Admin Panel</h3>
169 <p>Server stats and admin operations</p>
170 </a>
171 {/if}
172 </nav>
173 </div>
174{:else if auth.loading}
175 <div class="loading">Loading...</div>
176{/if}
177<style>
178 .dashboard {
179 max-width: 800px;
180 margin: 0 auto;
181 padding: 2rem;
182 }
183 header {
184 display: flex;
185 justify-content: space-between;
186 align-items: center;
187 margin-bottom: 2rem;
188 }
189 header h1 {
190 margin: 0;
191 }
192 .account-dropdown {
193 position: relative;
194 }
195 .account-trigger {
196 display: flex;
197 align-items: center;
198 gap: 0.5rem;
199 padding: 0.5rem 1rem;
200 background: transparent;
201 border: 1px solid var(--border-color-light);
202 border-radius: 4px;
203 cursor: pointer;
204 color: var(--text-primary);
205 }
206 .account-trigger:hover:not(:disabled) {
207 background: var(--bg-secondary);
208 }
209 .account-trigger:disabled {
210 opacity: 0.6;
211 cursor: not-allowed;
212 }
213 .account-trigger .account-handle {
214 font-weight: 500;
215 }
216 .dropdown-arrow {
217 font-size: 0.625rem;
218 color: var(--text-secondary);
219 }
220 .dropdown-menu {
221 position: absolute;
222 top: 100%;
223 right: 0;
224 margin-top: 0.25rem;
225 min-width: 200px;
226 background: var(--bg-card);
227 border: 1px solid var(--border-color);
228 border-radius: 8px;
229 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
230 z-index: 100;
231 overflow: hidden;
232 }
233 .dropdown-section {
234 padding: 0.5rem 0;
235 }
236 .dropdown-label {
237 display: block;
238 padding: 0.25rem 1rem;
239 font-size: 0.75rem;
240 color: var(--text-muted);
241 text-transform: uppercase;
242 letter-spacing: 0.05em;
243 }
244 .dropdown-item {
245 display: block;
246 width: 100%;
247 padding: 0.75rem 1rem;
248 background: transparent;
249 border: none;
250 text-align: left;
251 cursor: pointer;
252 color: var(--text-primary);
253 font-size: 0.875rem;
254 }
255 .dropdown-item:hover {
256 background: var(--bg-secondary);
257 }
258 .dropdown-item.logout-item {
259 color: var(--error-text);
260 }
261 .dropdown-divider {
262 height: 1px;
263 background: var(--border-color);
264 margin: 0;
265 }
266 section {
267 background: var(--bg-secondary);
268 padding: 1.5rem;
269 border-radius: 8px;
270 margin-bottom: 2rem;
271 }
272 section h2 {
273 margin: 0 0 1rem 0;
274 font-size: 1.25rem;
275 }
276 dl {
277 display: grid;
278 grid-template-columns: auto 1fr;
279 gap: 0.5rem 1rem;
280 margin: 0;
281 }
282 dt {
283 font-weight: 500;
284 color: var(--text-secondary);
285 }
286 dd {
287 margin: 0;
288 }
289 .mono {
290 font-family: monospace;
291 font-size: 0.875rem;
292 word-break: break-all;
293 }
294 .badge {
295 display: inline-block;
296 padding: 0.125rem 0.5rem;
297 border-radius: 4px;
298 font-size: 0.75rem;
299 margin-left: 0.5rem;
300 }
301 .badge.success {
302 background: var(--success-bg);
303 color: var(--success-text);
304 }
305 .badge.warning {
306 background: var(--warning-bg);
307 color: var(--warning-text);
308 }
309 .badge.admin {
310 background: var(--accent);
311 color: white;
312 }
313 .badge.deactivated {
314 background: var(--warning-bg);
315 color: var(--warning-text);
316 border: 1px solid #d4a03c;
317 }
318 .nav-grid {
319 display: grid;
320 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
321 gap: 1rem;
322 }
323 .nav-card {
324 display: block;
325 padding: 1.5rem;
326 background: var(--bg-card);
327 border: 1px solid var(--border-color);
328 border-radius: 8px;
329 text-decoration: none;
330 color: inherit;
331 transition: border-color 0.15s, box-shadow 0.15s;
332 }
333 .nav-card:hover {
334 border-color: var(--accent);
335 box-shadow: 0 2px 8px rgba(77, 166, 255, 0.15);
336 }
337 .nav-card h3 {
338 margin: 0 0 0.5rem 0;
339 color: var(--accent);
340 }
341 .nav-card p {
342 margin: 0;
343 color: var(--text-secondary);
344 font-size: 0.875rem;
345 }
346 .nav-card.admin-card {
347 border-color: var(--accent);
348 background: linear-gradient(135deg, var(--bg-card) 0%, rgba(77, 166, 255, 0.05) 100%);
349 }
350 .nav-card.admin-card:hover {
351 box-shadow: 0 2px 12px rgba(77, 166, 255, 0.25);
352 }
353 .loading {
354 text-align: center;
355 padding: 4rem;
356 color: var(--text-secondary);
357 }
358 .deactivated-banner {
359 background: var(--warning-bg);
360 border: 1px solid #d4a03c;
361 border-radius: 8px;
362 padding: 1rem 1.5rem;
363 margin-bottom: 2rem;
364 }
365 .deactivated-banner strong {
366 color: var(--warning-text);
367 font-size: 1rem;
368 }
369 .deactivated-banner p {
370 margin: 0.5rem 0 0 0;
371 color: var(--warning-text);
372 font-size: 0.875rem;
373 }
374</style>