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 &times; 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">&middot;</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>