this repo has no description
1<script lang="ts"> 2 import { navigate } from '../lib/router.svelte' 3 import { api, ApiError } from '../lib/api' 4 import { getAuthState } from '../lib/auth.svelte' 5 const auth = getAuthState() 6 let email = $state('') 7 let token = $state('') 8 let newPassword = $state('') 9 let confirmPassword = $state('') 10 let submitting = $state(false) 11 let error = $state<string | null>(null) 12 let success = $state<string | null>(null) 13 let tokenSent = $state(false) 14 $effect(() => { 15 if (auth.session) { 16 navigate('/dashboard') 17 } 18 }) 19 async function handleRequestReset(e: Event) { 20 e.preventDefault() 21 if (!email) return 22 submitting = true 23 error = null 24 success = null 25 try { 26 await api.requestPasswordReset(email) 27 tokenSent = true 28 success = 'Password reset code sent! Check your preferred notification channel.' 29 } catch (e) { 30 error = e instanceof ApiError ? e.message : 'Failed to send reset code' 31 } finally { 32 submitting = false 33 } 34 } 35 async function handleReset(e: Event) { 36 e.preventDefault() 37 if (!token || !newPassword || !confirmPassword) return 38 if (newPassword !== confirmPassword) { 39 error = 'Passwords do not match' 40 return 41 } 42 if (newPassword.length < 8) { 43 error = 'Password must be at least 8 characters' 44 return 45 } 46 submitting = true 47 error = null 48 success = null 49 try { 50 await api.resetPassword(token, newPassword) 51 success = 'Password reset successfully!' 52 setTimeout(() => navigate('/login'), 2000) 53 } catch (e) { 54 error = e instanceof ApiError ? e.message : 'Failed to reset password' 55 } finally { 56 submitting = false 57 } 58 } 59</script> 60<div class="reset-container"> 61 {#if error} 62 <div class="message error">{error}</div> 63 {/if} 64 {#if success} 65 <div class="message success">{success}</div> 66 {/if} 67 {#if tokenSent} 68 <h1>Reset Password</h1> 69 <p class="subtitle">Enter the code you received and choose a new password.</p> 70 <form onsubmit={handleReset}> 71 <div class="field"> 72 <label for="token">Reset Code</label> 73 <input 74 id="token" 75 type="text" 76 bind:value={token} 77 placeholder="Enter reset code" 78 disabled={submitting} 79 required 80 /> 81 </div> 82 <div class="field"> 83 <label for="new-password">New Password</label> 84 <input 85 id="new-password" 86 type="password" 87 bind:value={newPassword} 88 placeholder="At least 8 characters" 89 disabled={submitting} 90 required 91 minlength="8" 92 /> 93 </div> 94 <div class="field"> 95 <label for="confirm-password">Confirm Password</label> 96 <input 97 id="confirm-password" 98 type="password" 99 bind:value={confirmPassword} 100 placeholder="Confirm new password" 101 disabled={submitting} 102 required 103 /> 104 </div> 105 <button type="submit" disabled={submitting || !token || !newPassword || !confirmPassword}> 106 {submitting ? 'Resetting...' : 'Reset Password'} 107 </button> 108 <button type="button" class="secondary" onclick={() => { tokenSent = false; token = ''; newPassword = ''; confirmPassword = '' }}> 109 Request New Code 110 </button> 111 </form> 112 {:else} 113 <h1>Forgot Password</h1> 114 <p class="subtitle">Enter your handle or email and we'll send you a code to reset your password.</p> 115 <form onsubmit={handleRequestReset}> 116 <div class="field"> 117 <label for="email">Handle or Email</label> 118 <input 119 id="email" 120 type="text" 121 bind:value={email} 122 placeholder="handle or you@example.com" 123 disabled={submitting} 124 required 125 /> 126 </div> 127 <button type="submit" disabled={submitting || !email}> 128 {submitting ? 'Sending...' : 'Send Reset Code'} 129 </button> 130 </form> 131 {/if} 132 <p class="back-link"> 133 <a href="#/login">Back to Sign In</a> 134 </p> 135</div> 136<style> 137 .reset-container { 138 max-width: 400px; 139 margin: 4rem auto; 140 padding: 2rem; 141 } 142 h1 { 143 margin: 0 0 0.5rem 0; 144 } 145 .subtitle { 146 color: var(--text-secondary); 147 margin: 0 0 2rem 0; 148 } 149 form { 150 display: flex; 151 flex-direction: column; 152 gap: 1rem; 153 } 154 .field { 155 display: flex; 156 flex-direction: column; 157 gap: 0.25rem; 158 } 159 label { 160 font-size: 0.875rem; 161 font-weight: 500; 162 } 163 input { 164 padding: 0.75rem; 165 border: 1px solid var(--border-color-light); 166 border-radius: 4px; 167 font-size: 1rem; 168 background: var(--bg-input); 169 color: var(--text-primary); 170 } 171 input:focus { 172 outline: none; 173 border-color: var(--accent); 174 } 175 button { 176 padding: 0.75rem; 177 background: var(--accent); 178 color: white; 179 border: none; 180 border-radius: 4px; 181 font-size: 1rem; 182 cursor: pointer; 183 margin-top: 0.5rem; 184 } 185 button:hover:not(:disabled) { 186 background: var(--accent-hover); 187 } 188 button:disabled { 189 opacity: 0.6; 190 cursor: not-allowed; 191 } 192 button.secondary { 193 background: transparent; 194 color: var(--text-secondary); 195 border: 1px solid var(--border-color-light); 196 } 197 button.secondary:hover:not(:disabled) { 198 background: var(--bg-secondary); 199 } 200 .message { 201 padding: 0.75rem; 202 border-radius: 4px; 203 margin-bottom: 1rem; 204 } 205 .message.success { 206 background: var(--success-bg); 207 border: 1px solid var(--success-border); 208 color: var(--success-text); 209 } 210 .message.error { 211 background: var(--error-bg); 212 border: 1px solid var(--error-border); 213 color: var(--error-text); 214 } 215 .back-link { 216 text-align: center; 217 margin-top: 1.5rem; 218 color: var(--text-secondary); 219 } 220 .back-link a { 221 color: var(--accent); 222 } 223</style>