refactor(frontend): migrate email fields to EmailInput component

Replace raw <Input type="email"> with <EmailInput> in login (register + login), account (gift card recipient), BookingFlow (guest email), and GiftCardsManagement (admin gift card email).

Removes 30-line validateEmail() function from login page (now handled by EmailInput internally).

Removes validateEmailFormat() from BookingFlow (EmailInput handles format validation; debounced backend email check preserved via onvaluechange/onblur).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-12 10:51:07 +01:00
co-authored by Sisyphus
parent 1cadbbe968
commit 18eaa00479
4 changed files with 48 additions and 100 deletions
@@ -4,6 +4,7 @@
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { EmailInput } from '$lib/components/ui/email-input';
import * as Modal from '$lib/components/ui/dialog'; import * as Modal from '$lib/components/ui/dialog';
import { Skeleton } from '$lib/components/ui/skeleton'; import { Skeleton } from '$lib/components/ui/skeleton';
@@ -1765,11 +1766,10 @@
Recipient Email Address (Optional) Recipient Email Address (Optional)
{/if} {/if}
</label> </label>
<Input <EmailInput
id="generate-email" id="generate-email"
type="email"
placeholder="e.g. customer@example.com"
bind:value={generateEmail} bind:value={generateEmail}
placeholder="e.g. customer@example.com"
/> />
{#if selectedCustomer?.email && !generateEmail.trim()} {#if selectedCustomer?.email && !generateEmail.trim()}
<p class="text-xs text-gray-500 italic">Defaults to: {selectedCustomer.email}</p> <p class="text-xs text-gray-500 italic">Defaults to: {selectedCustomer.email}</p>
@@ -2,6 +2,7 @@
import { Button } from '$lib/components/ui/button/index.js'; import { Button } from '$lib/components/ui/button/index.js';
import * as Card from '$lib/components/ui/card/index.js'; import * as Card from '$lib/components/ui/card/index.js';
import { Input } from '$lib/components/ui/input/index.js'; import { Input } from '$lib/components/ui/input/index.js';
import { EmailInput } from '$lib/components/ui/email-input/index.js';
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import { Textarea } from '$lib/components/ui/textarea/index.js'; import { Textarea } from '$lib/components/ui/textarea/index.js';
import CharCounter from '$lib/components/ui/CharCounter.svelte'; import CharCounter from '$lib/components/ui/CharCounter.svelte';
@@ -106,18 +107,6 @@
isValidUKPhone(customerInfo.phone) isValidUKPhone(customerInfo.phone)
); );
function validateEmailFormat(email: string) {
if (!email) {
emailError = '';
return;
}
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email)) {
emailError = 'Please enter a valid email address';
} else {
emailError = '';
}
}
async function checkEmailExists(email: string) { async function checkEmailExists(email: string) {
if (!email || !/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email)) { if (!email || !/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email)) {
emailSuggestion = null; emailSuggestion = null;
@@ -1713,18 +1702,17 @@
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Label for="email">Email *</Label> <Label for="email">Email *</Label>
<Input <EmailInput
id="email" id="email"
type="email"
bind:value={customerInfo.email} bind:value={customerInfo.email}
placeholder="Enter your email" placeholder="Enter your email"
onblur={() => { required
validateEmailFormat(customerInfo.email); onvaluechange={() => {
emailSuggestion = null;
debouncedEmailCheck(); debouncedEmailCheck();
}} }}
oninput={() => { onerrorchange={(err) => { emailError = err; }}
emailSuggestion = null; onblur={() => {
emailError = '';
debouncedEmailCheck(); debouncedEmailCheck();
}} }}
/> />
+12 -12
View File
@@ -28,6 +28,7 @@
import { Checkbox } from '$lib/components/ui/checkbox'; import { Checkbox } from '$lib/components/ui/checkbox';
import * as Card from '$lib/components/ui/card'; import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { EmailInput } from '$lib/components/ui/email-input/index.js';
import { PhoneInput } from '$lib/components/ui/phone-input/index.js'; import { PhoneInput } from '$lib/components/ui/phone-input/index.js';
import { Separator } from '$lib/components/ui/separator'; import { Separator } from '$lib/components/ui/separator';
import * as AlertDialog from '$lib/components/ui/alert-dialog'; import * as AlertDialog from '$lib/components/ui/alert-dialog';
@@ -2095,18 +2096,17 @@
</div> </div>
{#if buyRecipientType === 'friend'} {#if buyRecipientType === 'friend'}
<div class="space-y-2"> <div class="space-y-2">
<label for="recipient-email" class="text-sm font-medium text-gray-700" <label for="recipient-email" class="text-sm font-medium text-gray-700"
>Friend's Email (Optional)</label >Friend's Email (Optional)</label
> >
<Input <EmailInput
id="recipient-email" id="recipient-email"
type="email" bind:value={buyRecipientEmail}
placeholder="friend@example.com (blank to send to yourself)" placeholder="friend@example.com (blank to send to yourself)"
bind:value={buyRecipientEmail} class="mt-1"
class="mt-1" />
/> </div>
</div>
{/if} {/if}
<div class="space-y-3 border-t pt-2"> <div class="space-y-3 border-t pt-2">
+26 -66
View File
@@ -2,6 +2,7 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { Button } from '$lib/components/ui/button/index.js'; import { Button } from '$lib/components/ui/button/index.js';
import { Input } from '$lib/components/ui/input/index.js'; import { Input } from '$lib/components/ui/input/index.js';
import { EmailInput } from '$lib/components/ui/email-input/index.js';
import { PhoneInput } from '$lib/components/ui/phone-input/index.js'; import { PhoneInput } from '$lib/components/ui/phone-input/index.js';
import { Label } from '$lib/components/ui/label/index.js'; import { Label } from '$lib/components/ui/label/index.js';
import { Separator } from '$lib/components/ui/separator/index.js'; import { Separator } from '$lib/components/ui/separator/index.js';
@@ -67,37 +68,6 @@
}; };
} }
// Email validation
function validateEmail(email: string): boolean {
// ASCII-only email regex (safe with most providers)
const asciiRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
// Unicode-friendly regex (valid per RFC 6531)
const unicodeRegex = /^[\p{L}\p{M}0-9._%+-]+@[\p{L}\p{M}0-9.-]+\.[\p{L}\p{M}]{2,}$/u;
if (!email) {
validationErrors.email = '';
return true;
}
if (asciiRegex.test(email)) {
// OK: standard ASCII address
validationErrors.email = '';
return true;
} else if (unicodeRegex.test(email)) {
// Looks like a valid internationalised address, but unsupported
validationErrors.email =
'This looks like a valid international email address, ' +
'but our providers only supports standard (A-Z, 0-9) email addresses. ' +
'We are sorry. Please use an ASCII-compatible email.';
return false;
} else {
// Not even valid under the Unicode spec
validationErrors.email = 'Invalid email format';
return false;
}
}
function validatePhone(phone: string): boolean { function validatePhone(phone: string): boolean {
const valid = isValidUKPhone(phone); const valid = isValidUKPhone(phone);
validationErrors.phone = valid ? '' : 'Please enter a valid UK phone number'; validationErrors.phone = valid ? '' : 'Please enter a valid UK phone number';
@@ -217,7 +187,7 @@
} }
// Validate before submitting // Validate before submitting
const isEmailValid = validateEmail(formData.email); const isEmailValid = !validationErrors.email && formData.email.trim().length > 0;
const isPhoneValid = validatePhone(formData.phone); const isPhoneValid = validatePhone(formData.phone);
const isAgeValid = validateAge(formData.dateOfBirth); const isAgeValid = validateAge(formData.dateOfBirth);
@@ -380,23 +350,18 @@
{#if !isLogin} {#if !isLogin}
<!-- SECTION 1: LOGIN DETAILS --> <!-- SECTION 1: LOGIN DETAILS -->
<div class="space-y-4"> <div class="space-y-4">
<h3 class="text-lg font-semibold">What we need to log you in</h3> <h3 class="text-lg font-semibold">What we need to log you in</h3>
<div class="space-y-2"> <div class="space-y-2">
<RequiredLabel forId="email" text="Email" /> <RequiredLabel forId="email" text="Email" />
<Input <EmailInput
id="email" id="email"
type="email" bind:value={formData.email}
placeholder="john@example.com" bind:error={validationErrors.email}
maxlength={255} placeholder="john@example.com"
bind:value={formData.email} required
onblur={() => validateEmail(formData.email)} />
required </div>
/>
{#if validationErrors.email}
<p class="text-sm text-red-500">{validationErrors.email}</p>
{/if}
</div>
<div class="space-y-2"> <div class="space-y-2">
<RequiredLabel forId="password" text="Password" /> <RequiredLabel forId="password" text="Password" />
@@ -540,25 +505,20 @@
</div> </div>
{:else} {:else}
<!-- Login Fields (Simple inline layout) --> <!-- Login Fields (Simple inline layout) -->
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<RequiredLabel forId="email" text="Email" /> <RequiredLabel forId="email" text="Email" />
<Input <EmailInput
id="email" id="email"
type="email" bind:value={formData.email}
placeholder="john@example.com" bind:error={validationErrors.email}
maxlength={255} placeholder="john@example.com"
bind:value={formData.email} required
onblur={() => validateEmail(formData.email)} />
required </div>
/>
{#if validationErrors.email}
<p class="text-sm text-red-500">{validationErrors.email}</p>
{/if}
</div>
<div class="space-y-2"> <div class="space-y-2">
<RequiredLabel forId="password" text="Password" /> <RequiredLabel forId="password" text="Password" />
<Input <Input
id="password" id="password"
type="password" type="password"