this repo has no description
1<script lang="ts">
2 import { loginWithOAuth, confirmSignup, resendVerification, getAuthState, switchAccount, forgetAccount } from '../lib/auth.svelte'
3 import { navigate } from '../lib/router.svelte'
4 import { _ } from '../lib/i18n'
5
6 let submitting = $state(false)
7 let pendingVerification = $state<{ did: string } | null>(null)
8 let verificationCode = $state('')
9 let resendingCode = $state(false)
10 let resendMessage = $state<string | null>(null)
11 let autoRedirectAttempted = $state(false)
12 const auth = getAuthState()
13
14 $effect(() => {
15 if (!auth.loading && !auth.error && auth.savedAccounts.length === 0 && !pendingVerification && !autoRedirectAttempted) {
16 autoRedirectAttempted = true
17 loginWithOAuth()
18 }
19 })
20
21 async function handleSwitchAccount(did: string) {
22 submitting = true
23 try {
24 await switchAccount(did)
25 navigate('/dashboard')
26 } catch {
27 submitting = false
28 }
29 }
30
31 function handleForgetAccount(did: string, e: Event) {
32 e.stopPropagation()
33 forgetAccount(did)
34 }
35
36 async function handleOAuthLogin() {
37 submitting = true
38 try {
39 await loginWithOAuth()
40 } catch {
41 submitting = false
42 }
43 }
44
45 async function handleVerification(e: Event) {
46 e.preventDefault()
47 if (!pendingVerification || !verificationCode.trim()) return
48 submitting = true
49 try {
50 await confirmSignup(pendingVerification.did, verificationCode.trim())
51 navigate('/dashboard')
52 } catch {
53 submitting = false
54 }
55 }
56
57 async function handleResendCode() {
58 if (!pendingVerification || resendingCode) return
59 resendingCode = true
60 resendMessage = null
61 try {
62 await resendVerification(pendingVerification.did)
63 resendMessage = $_('verification.resent')
64 } catch {
65 resendMessage = null
66 } finally {
67 resendingCode = false
68 }
69 }
70
71 function backToLogin() {
72 pendingVerification = null
73 verificationCode = ''
74 resendMessage = null
75 }
76</script>
77
78<div class="login-page">
79 {#if auth.error}
80 <div class="message error">{auth.error}</div>
81 {/if}
82
83 {#if pendingVerification}
84 <header class="page-header">
85 <h1>{$_('verification.title')}</h1>
86 <p class="subtitle">{$_('verification.subtitle')}</p>
87 </header>
88
89 {#if resendMessage}
90 <div class="message success">{resendMessage}</div>
91 {/if}
92
93 <form onsubmit={(e) => { e.preventDefault(); handleVerification(e); }}>
94 <div class="field">
95 <label for="verification-code">{$_('verification.codeLabel')}</label>
96 <input
97 id="verification-code"
98 type="text"
99 bind:value={verificationCode}
100 placeholder={$_('verification.codePlaceholder')}
101 disabled={submitting}
102 required
103 maxlength="6"
104 pattern="[0-9]{6}"
105 autocomplete="one-time-code"
106 />
107 </div>
108 <div class="actions">
109 <button type="submit" disabled={submitting || !verificationCode.trim()}>
110 {submitting ? $_('verification.verifying') : $_('verification.verifyButton')}
111 </button>
112 <button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode}>
113 {resendingCode ? $_('verification.resending') : $_('verification.resendButton')}
114 </button>
115 <button type="button" class="tertiary" onclick={backToLogin}>
116 {$_('verification.backToLogin')}
117 </button>
118 </div>
119 </form>
120
121 {:else}
122 <header class="page-header">
123 <h1>{$_('login.title')}</h1>
124 <p class="subtitle">{auth.savedAccounts.length > 0 ? $_('login.chooseAccount') : $_('login.subtitle')}</p>
125 </header>
126
127 <div class="split-layout sidebar-right">
128 <div class="main-section">
129 {#if auth.savedAccounts.length > 0}
130 <div class="saved-accounts">
131 {#each auth.savedAccounts as account}
132 <div
133 class="account-item"
134 class:disabled={submitting}
135 role="button"
136 tabindex="0"
137 onclick={() => !submitting && handleSwitchAccount(account.did)}
138 onkeydown={(e) => e.key === 'Enter' && !submitting && handleSwitchAccount(account.did)}
139 >
140 <div class="account-info">
141 <span class="account-handle">@{account.handle}</span>
142 <span class="account-did">{account.did}</span>
143 </div>
144 <button
145 type="button"
146 class="forget-btn"
147 onclick={(e) => handleForgetAccount(account.did, e)}
148 title={$_('login.removeAccount')}
149 >
150 ×
151 </button>
152 </div>
153 {/each}
154 </div>
155
156 <p class="or-divider">{$_('login.signInToAnother')}</p>
157 {/if}
158
159 <button type="button" class="oauth-btn" onclick={handleOAuthLogin} disabled={submitting || auth.loading}>
160 {submitting ? $_('login.redirecting') : $_('login.button')}
161 </button>
162
163 <p class="forgot-links">
164 <a href="#/reset-password">{$_('login.forgotPassword')}</a>
165 <span class="separator">·</span>
166 <a href="#/request-passkey-recovery">{$_('login.lostPasskey')}</a>
167 </p>
168
169 <p class="link-text">
170 {$_('login.noAccount')} <a href="#/register">{$_('login.createAccount')}</a>
171 </p>
172 </div>
173
174 <aside class="info-panel">
175 {#if auth.savedAccounts.length > 0}
176 <h3>{$_('login.infoSavedAccountsTitle')}</h3>
177 <p>{$_('login.infoSavedAccountsDesc')}</p>
178
179 <h3>{$_('login.infoNewAccountTitle')}</h3>
180 <p>{$_('login.infoNewAccountDesc')}</p>
181 {:else}
182 <h3>{$_('login.infoSecureSignInTitle')}</h3>
183 <p>{$_('login.infoSecureSignInDesc')}</p>
184
185 <h3>{$_('login.infoStaySignedInTitle')}</h3>
186 <p>{$_('login.infoStaySignedInDesc')}</p>
187 {/if}
188
189 <h3>{$_('login.infoRecoveryTitle')}</h3>
190 <p>{$_('login.infoRecoveryDesc')}</p>
191 </aside>
192 </div>
193 {/if}
194</div>
195
196<style>
197 .login-page {
198 max-width: var(--width-lg);
199 margin: var(--space-9) auto;
200 padding: var(--space-7);
201 }
202
203 .page-header {
204 margin-bottom: var(--space-6);
205 }
206
207 h1 {
208 margin: 0 0 var(--space-3) 0;
209 }
210
211 .subtitle {
212 color: var(--text-secondary);
213 margin: 0;
214 }
215
216 .main-section {
217 min-width: 0;
218 }
219
220 form {
221 display: flex;
222 flex-direction: column;
223 gap: var(--space-4);
224 max-width: var(--width-sm);
225 }
226
227 .actions {
228 display: flex;
229 flex-direction: column;
230 gap: var(--space-3);
231 margin-top: var(--space-3);
232 }
233
234 @media (min-width: 600px) {
235 .actions {
236 flex-direction: row;
237 }
238
239 .actions button {
240 flex: 1;
241 }
242 }
243
244 .oauth-btn {
245 width: 100%;
246 padding: var(--space-5);
247 font-size: var(--text-lg);
248 }
249
250 .forgot-links {
251 margin-top: var(--space-4);
252 font-size: var(--text-sm);
253 color: var(--text-secondary);
254 }
255
256 .forgot-links a {
257 color: var(--accent);
258 }
259
260 .separator {
261 margin: 0 var(--space-2);
262 }
263
264 .link-text {
265 margin-top: var(--space-6);
266 font-size: var(--text-sm);
267 color: var(--text-secondary);
268 }
269
270 .link-text a {
271 color: var(--accent);
272 }
273
274 .saved-accounts {
275 display: flex;
276 flex-direction: column;
277 gap: var(--space-3);
278 margin-bottom: var(--space-5);
279 }
280
281 .account-item {
282 display: flex;
283 align-items: center;
284 justify-content: space-between;
285 padding: var(--space-5);
286 background: var(--bg-card);
287 border: 1px solid var(--border-color);
288 border-radius: var(--radius-xl);
289 cursor: pointer;
290 transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
291 }
292
293 .account-item:hover:not(.disabled) {
294 border-color: var(--accent);
295 box-shadow: var(--shadow-md);
296 }
297
298 .account-item.disabled {
299 opacity: 0.6;
300 cursor: not-allowed;
301 }
302
303 .account-info {
304 display: flex;
305 flex-direction: column;
306 gap: var(--space-1);
307 }
308
309 .account-handle {
310 font-weight: var(--font-medium);
311 color: var(--text-primary);
312 }
313
314 .account-did {
315 font-size: var(--text-xs);
316 color: var(--text-muted);
317 font-family: ui-monospace, monospace;
318 overflow: hidden;
319 text-overflow: ellipsis;
320 max-width: 250px;
321 }
322
323 .forget-btn {
324 padding: var(--space-2) var(--space-3);
325 background: transparent;
326 border: none;
327 color: var(--text-muted);
328 cursor: pointer;
329 font-size: var(--text-xl);
330 line-height: 1;
331 border-radius: var(--radius-md);
332 }
333
334 .forget-btn:hover {
335 background: var(--error-bg);
336 color: var(--error-text);
337 }
338
339 .or-divider {
340 text-align: center;
341 color: var(--text-muted);
342 font-size: var(--text-sm);
343 margin: var(--space-5) 0;
344 }
345</style>