|
7 | 7 | export let form: ActionData; |
8 | 8 |
|
9 | 9 | let step = 1; |
10 | | - let provider: 'none' | 'smtp' | 'mailgun' = 'none'; |
11 | 10 | let submitting = false; |
| 11 | +
|
| 12 | + // All fields are bound so their values survive any re-render (notably the |
| 13 | + // "Send test" round-trip, which previously wiped unbound inputs). |
| 14 | + let firstName = ''; |
| 15 | + let lastName = ''; |
| 16 | + let email = ''; |
| 17 | + let password = ''; |
| 18 | +
|
| 19 | + let provider: 'none' | 'smtp' | 'mailgun' = 'none'; |
| 20 | + let from = ''; |
| 21 | + let smtpHost = ''; |
| 22 | + let smtpPort = 587; |
| 23 | + let smtpSecure = false; |
| 24 | + let smtpUser = ''; |
| 25 | + let smtpPass = ''; |
| 26 | + let mgDomain = ''; |
| 27 | + let mgApiKey = ''; |
| 28 | + let mgApiUrl = ''; |
| 29 | +
|
| 30 | + // Test email is a pure client-side call — it never submits the form, so the |
| 31 | + // entered data is preserved. |
| 32 | + let testTo = ''; |
12 | 33 | let testing = false; |
| 34 | + let testError = ''; |
| 35 | + let testOk = false; |
| 36 | +
|
| 37 | + function emailPayload() { |
| 38 | + if (provider === 'smtp') { |
| 39 | + return { provider, from, host: smtpHost, port: smtpPort, secure: smtpSecure, user: smtpUser, pass: smtpPass }; |
| 40 | + } |
| 41 | + if (provider === 'mailgun') { |
| 42 | + return { provider, from, domain: mgDomain, apiKey: mgApiKey, apiUrl: mgApiUrl }; |
| 43 | + } |
| 44 | + return { provider: 'none' }; |
| 45 | + } |
| 46 | +
|
| 47 | + async function sendTest() { |
| 48 | + testing = true; |
| 49 | + testError = ''; |
| 50 | + testOk = false; |
| 51 | + try { |
| 52 | + const res = await fetch('/setup/test-email', { |
| 53 | + method: 'POST', |
| 54 | + headers: { 'Content-Type': 'application/json' }, |
| 55 | + body: JSON.stringify({ ...emailPayload(), to: testTo }), |
| 56 | + }); |
| 57 | + const data = await res.json(); |
| 58 | + if (!res.ok) throw new Error(data.error || 'Test failed'); |
| 59 | + testOk = true; |
| 60 | + } catch (err: any) { |
| 61 | + testError = err.message; |
| 62 | + } finally { |
| 63 | + testing = false; |
| 64 | + } |
| 65 | + } |
13 | 66 |
|
14 | 67 | // Shared input styling — matches the login/signup fields. |
15 | 68 | const inputClass = |
|
71 | 124 | <div class="grid grid-cols-2 gap-3"> |
72 | 125 | <div> |
73 | 126 | <label for="first_name" class="mb-1.5 block text-sm font-medium">First name</label> |
74 | | - <input id="first_name" name="first_name" autocomplete="given-name" class={inputClass} /> |
| 127 | + <input id="first_name" name="first_name" bind:value={firstName} autocomplete="given-name" class={inputClass} /> |
75 | 128 | </div> |
76 | 129 | <div> |
77 | 130 | <label for="last_name" class="mb-1.5 block text-sm font-medium">Last name</label> |
78 | | - <input id="last_name" name="last_name" autocomplete="family-name" class={inputClass} /> |
| 131 | + <input id="last_name" name="last_name" bind:value={lastName} autocomplete="family-name" class={inputClass} /> |
79 | 132 | </div> |
80 | 133 | </div> |
81 | 134 | <div> |
82 | 135 | <label for="email" class="mb-1.5 block text-sm font-medium">Email</label> |
83 | | - <input id="email" name="email" type="email" required autocomplete="email" class={inputClass} /> |
| 136 | + <input id="email" name="email" type="email" required bind:value={email} autocomplete="email" class={inputClass} /> |
84 | 137 | </div> |
85 | 138 | <div> |
86 | 139 | <label for="password" class="mb-1.5 block text-sm font-medium">Password</label> |
87 | | - <input id="password" name="password" type="password" required minlength="8" autocomplete="new-password" class={inputClass} /> |
| 140 | + <input id="password" name="password" type="password" required minlength="8" bind:value={password} autocomplete="new-password" class={inputClass} /> |
88 | 141 | <p class="mt-1.5 text-xs text-muted-foreground">At least 8 characters, with an uppercase letter and a number or symbol.</p> |
89 | 142 | </div> |
90 | 143 | <Button type="button" class="w-full" on:click={() => (step = 2)}>Continue</Button> |
|
104 | 157 | {#if provider !== 'none'} |
105 | 158 | <div> |
106 | 159 | <label for="from" class="mb-1.5 block text-sm font-medium">From address</label> |
107 | | - <input id="from" name="from" placeholder="Arc Launchpad <noreply@example.com>" class={inputClass} /> |
| 160 | + <input id="from" name="from" bind:value={from} placeholder="Arc Launchpad <noreply@example.com>" class={inputClass} /> |
108 | 161 | </div> |
109 | 162 | {/if} |
110 | 163 |
|
111 | 164 | {#if provider === 'smtp'} |
112 | 165 | <div class="grid grid-cols-3 gap-3"> |
113 | 166 | <div class="col-span-2"> |
114 | 167 | <label for="smtp_host" class="mb-1.5 block text-sm font-medium">Host</label> |
115 | | - <input id="smtp_host" name="smtp_host" placeholder="smtp.example.com" class={inputClass} /> |
| 168 | + <input id="smtp_host" name="smtp_host" bind:value={smtpHost} placeholder="smtp.example.com" class={inputClass} /> |
116 | 169 | </div> |
117 | 170 | <div> |
118 | 171 | <label for="smtp_port" class="mb-1.5 block text-sm font-medium">Port</label> |
119 | | - <input id="smtp_port" name="smtp_port" type="number" value="587" class={inputClass} /> |
| 172 | + <input id="smtp_port" name="smtp_port" type="number" bind:value={smtpPort} class={inputClass} /> |
120 | 173 | </div> |
121 | 174 | </div> |
122 | 175 | <div class="grid grid-cols-2 gap-3"> |
123 | 176 | <div> |
124 | 177 | <label for="smtp_user" class="mb-1.5 block text-sm font-medium">Username</label> |
125 | | - <input id="smtp_user" name="smtp_user" autocomplete="off" class={inputClass} /> |
| 178 | + <input id="smtp_user" name="smtp_user" bind:value={smtpUser} autocomplete="off" class={inputClass} /> |
126 | 179 | </div> |
127 | 180 | <div> |
128 | 181 | <label for="smtp_pass" class="mb-1.5 block text-sm font-medium">Password</label> |
129 | | - <input id="smtp_pass" name="smtp_pass" type="password" autocomplete="off" class={inputClass} /> |
| 182 | + <input id="smtp_pass" name="smtp_pass" type="password" bind:value={smtpPass} autocomplete="off" class={inputClass} /> |
130 | 183 | </div> |
131 | 184 | </div> |
132 | 185 | <label class="flex items-center gap-2 text-sm"> |
133 | | - <input type="checkbox" name="smtp_secure" class="h-4 w-4 rounded border-input accent-primary" /> |
| 186 | + <input type="checkbox" name="smtp_secure" bind:checked={smtpSecure} class="h-4 w-4 rounded border-input accent-primary" /> |
134 | 187 | <span class="text-muted-foreground">Use TLS (port 465)</span> |
135 | 188 | </label> |
136 | 189 | {/if} |
137 | 190 |
|
138 | 191 | {#if provider === 'mailgun'} |
139 | 192 | <div> |
140 | 193 | <label for="mg_domain" class="mb-1.5 block text-sm font-medium">Mailgun domain</label> |
141 | | - <input id="mg_domain" name="mg_domain" placeholder="mg.example.com" class={inputClass} /> |
| 194 | + <input id="mg_domain" name="mg_domain" bind:value={mgDomain} placeholder="mg.example.com" class={inputClass} /> |
142 | 195 | </div> |
143 | 196 | <div> |
144 | 197 | <label for="mg_api_key" class="mb-1.5 block text-sm font-medium">API key</label> |
145 | | - <input id="mg_api_key" name="mg_api_key" type="password" autocomplete="off" class={inputClass} /> |
| 198 | + <input id="mg_api_key" name="mg_api_key" type="password" bind:value={mgApiKey} autocomplete="off" class={inputClass} /> |
146 | 199 | </div> |
147 | 200 | <div> |
148 | 201 | <label for="mg_api_url" class="mb-1.5 block text-sm font-medium">API base URL</label> |
149 | | - <input id="mg_api_url" name="mg_api_url" placeholder="https://api.mailgun.net" class={inputClass} /> |
| 202 | + <input id="mg_api_url" name="mg_api_url" bind:value={mgApiUrl} placeholder="https://api.mailgun.net" class={inputClass} /> |
150 | 203 | </div> |
151 | 204 | {/if} |
152 | 205 |
|
153 | 206 | {#if provider !== 'none'} |
154 | | - {#if form?.emailError} |
155 | | - <div class="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{form.emailError}</div> |
| 207 | + {#if testError} |
| 208 | + <div class="rounded-md bg-destructive/10 p-3 text-sm text-destructive">{testError}</div> |
156 | 209 | {/if} |
157 | | - {#if form?.emailTested} |
| 210 | + {#if testOk} |
158 | 211 | <div class="flex items-center gap-1.5 rounded-md bg-green-500/10 p-3 text-sm text-green-600"> |
159 | 212 | <Check class="h-4 w-4" /> Test email sent — check the inbox. |
160 | 213 | </div> |
161 | 214 | {/if} |
162 | 215 | <div class="flex items-end gap-2"> |
163 | 216 | <div class="flex-1"> |
164 | 217 | <label for="test_to" class="mb-1.5 block text-sm font-medium">Send a test to</label> |
165 | | - <input id="test_to" name="test_to" type="email" placeholder="you@example.com" class={inputClass} /> |
| 218 | + <input id="test_to" type="email" bind:value={testTo} placeholder="you@example.com" class={inputClass} /> |
166 | 219 | </div> |
167 | | - <Button |
168 | | - type="submit" |
169 | | - variant="outline" |
170 | | - formaction="?/testEmail" |
171 | | - disabled={testing} |
172 | | - on:click={() => { |
173 | | - testing = true; |
174 | | - setTimeout(() => (testing = false), 4000); |
175 | | - }} |
176 | | - > |
| 220 | + <Button type="button" variant="outline" disabled={testing || !testTo} on:click={sendTest}> |
177 | 221 | {testing ? 'Sending…' : 'Send test'} |
178 | 222 | </Button> |
179 | 223 | </div> |
|
0 commit comments