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>