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