feat: validate email via Emailable, remove auto-download, fix keyboard nav in inputs
All checks were successful
Deploy Production / Deploy to Production (push) Successful in 32s
All checks were successful
Deploy Production / Deploy to Production (push) Successful in 32s
- Plug Emailable API into checkEmailDeliverability (fail open if no key) - Show inline red error + shake animation on email field when address is undeliverable - Remove window.open auto-download after form submission; success now checks result.success - Disable arrow-key slide navigation when an input or textarea is focused Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4ea4cd91f5
commit
d087012ea8
4 changed files with 117 additions and 13 deletions
|
|
@ -49,12 +49,45 @@ function parseContact(array $body): array
|
|||
return compact('firstName', 'lastName', 'email', 'company', 'role');
|
||||
}
|
||||
|
||||
function checkEmailDeliverability(string $email): bool
|
||||
function checkEmailDeliverability(string $email, string $apiKey): bool
|
||||
{
|
||||
// Plug in an external email validation API here (e.g. Hunter, ZeroBounce, AbstractAPI)
|
||||
if ($apiKey === '') {
|
||||
error_log('[WP] Emailable: no api_key configured, skipping email check');
|
||||
return true;
|
||||
}
|
||||
|
||||
$url = 'https://api.emailable.com/v1/verify?' . http_build_query([
|
||||
'email' => $email,
|
||||
'api_key' => $apiKey,
|
||||
]);
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPGET => true,
|
||||
CURLOPT_TIMEOUT => 8,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false || $httpCode !== 200) {
|
||||
error_log('[WP] Emailable check failed (HTTP ' . $httpCode . '): ' . $curlError);
|
||||
return true; // fail open
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
$state = $data['state'] ?? 'unknown';
|
||||
|
||||
error_log('[WP] Emailable response for ' . $email . ': state=' . $state . ' | raw=' . $response);
|
||||
|
||||
return $state !== 'undeliverable';
|
||||
}
|
||||
|
||||
function upsertKirbyContact(\Kirby\Cms\Page $page, array $contact): void
|
||||
{
|
||||
$entries = page('livres-blancs')->contactDatabase()->toStructure()->toArray();
|
||||
|
|
@ -179,7 +212,8 @@ return [
|
|||
|
||||
$contact = parseContact($body);
|
||||
|
||||
if (!checkEmailDeliverability($contact['email'])) {
|
||||
$emailableApiKey = kirby()->option('emailable_whitepaper', [])['api_key'] ?? '';
|
||||
if (!checkEmailDeliverability($contact['email'], $emailableApiKey)) {
|
||||
wpReject(422, 'Undeliverable email');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -196,6 +196,8 @@
|
|||
|
||||
const handleKeydown = (e) => {
|
||||
if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return
|
||||
const tag = document.activeElement?.tagName
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || document.activeElement?.isContentEditable) return
|
||||
|
||||
// Si on est sur une sous-page (ex: /livres-blancs/slug), ne pas changer de slide
|
||||
const activePath = slides.active?.path ?? ''
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
let honeypot = $state('')
|
||||
let submitting = $state(false)
|
||||
let status = $state(null) // null | 'success' | 'error'
|
||||
let emailInvalid = $state(false)
|
||||
|
||||
let isEmailValid = $derived.by(() => {
|
||||
const re = /^[\w\-\.]+@([\w-]+\.)+[\w-]{2,}$/gm
|
||||
|
|
@ -66,6 +67,7 @@
|
|||
if (!consent || !uri) return
|
||||
submitting = true
|
||||
status = null
|
||||
emailInvalid = false
|
||||
try {
|
||||
const prefix = locale.current === 'en' ? '/en' : ''
|
||||
const res = await fetch(`${prefix}/${uri}/download`, {
|
||||
|
|
@ -74,8 +76,11 @@
|
|||
body: JSON.stringify({ firstName, lastName, company, role, email, _hp: honeypot })
|
||||
})
|
||||
const result = await res.json()
|
||||
if (result.fileUrl) {
|
||||
window.open(result.fileUrl, '_blank')
|
||||
if (res.status === 422 && result.error === 'Undeliverable email') {
|
||||
emailInvalid = true
|
||||
return
|
||||
}
|
||||
if (result.success) {
|
||||
status = 'success'
|
||||
} else {
|
||||
status = 'error'
|
||||
|
|
@ -188,13 +193,19 @@
|
|||
<input
|
||||
id="wp-email"
|
||||
class="wp-dialog__input"
|
||||
class:wp-dialog__input--invalid={emailInvalid}
|
||||
type="email"
|
||||
placeholder={t('wp_email')}
|
||||
autocomplete="email"
|
||||
required
|
||||
aria-required="true"
|
||||
aria-describedby={emailInvalid ? 'wp-email-error' : undefined}
|
||||
bind:value={email}
|
||||
oninput={() => emailInvalid = false}
|
||||
/>
|
||||
{#if emailInvalid}
|
||||
<p class="wp-dialog__email-error" id="wp-email-error" role="alert">Adresse email invalide</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Honeypot anti-spam -->
|
||||
|
|
@ -344,6 +355,26 @@
|
|||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.wp-dialog__input--invalid {
|
||||
border-color: #ff6b6b;
|
||||
animation: shake 0.4s ease;
|
||||
}
|
||||
|
||||
.wp-dialog__email-error {
|
||||
font-family: "Danzza", sans-serif;
|
||||
font-size: var(--font-size-paragraph-small);
|
||||
color: #ff6b6b;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-6px); }
|
||||
40% { transform: translateX(6px); }
|
||||
60% { transform: translateX(-4px); }
|
||||
80% { transform: translateX(3px); }
|
||||
}
|
||||
|
||||
.wp-dialog__hp {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
let consent = $state(false)
|
||||
let submitting = $state(false)
|
||||
let status = $state(null) // null | 'success' | 'error'
|
||||
let emailInvalid = $state(false)
|
||||
let showForm = $state(false)
|
||||
let honeypot = $state('')
|
||||
|
||||
|
|
@ -29,6 +30,7 @@
|
|||
if (!consent) return
|
||||
submitting = true
|
||||
status = null
|
||||
emailInvalid = false
|
||||
try {
|
||||
const prefix = locale.current === 'en' ? '/en' : ''
|
||||
const res = await fetch(`${prefix}/${data.uri}/download`, {
|
||||
|
|
@ -37,8 +39,11 @@
|
|||
body: JSON.stringify({ firstName, lastName, company, role, email, _hp: honeypot })
|
||||
})
|
||||
const result = await res.json()
|
||||
if (result.fileUrl) {
|
||||
window.open(result.fileUrl, '_blank')
|
||||
if (res.status === 422 && result.error === 'Undeliverable email') {
|
||||
emailInvalid = true
|
||||
return
|
||||
}
|
||||
if (result.success) {
|
||||
status = 'success'
|
||||
} else {
|
||||
status = 'error'
|
||||
|
|
@ -96,7 +101,19 @@
|
|||
</div>
|
||||
<input class="input" type="text" placeholder={t('wp_company')} bind:value={company} required />
|
||||
<input class="input" type="text" placeholder={t('wp_role')} bind:value={role} required />
|
||||
<input class="input" type="email" placeholder={t('wp_email')} bind:value={email} required />
|
||||
<input
|
||||
class="input"
|
||||
class:input--invalid={emailInvalid}
|
||||
type="email"
|
||||
placeholder={t('wp_email')}
|
||||
bind:value={email}
|
||||
required
|
||||
aria-describedby={emailInvalid ? 'wp-email-error' : undefined}
|
||||
oninput={() => emailInvalid = false}
|
||||
/>
|
||||
{#if emailInvalid}
|
||||
<p class="email-error" id="wp-email-error" role="alert">Adresse email invalide</p>
|
||||
{/if}
|
||||
|
||||
<div class="hp" aria-hidden="true">
|
||||
<label for="website">Website</label>
|
||||
|
|
@ -246,6 +263,26 @@
|
|||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.input--invalid {
|
||||
border-color: #ff6b6b;
|
||||
animation: shake 0.4s ease;
|
||||
}
|
||||
|
||||
.email-error {
|
||||
font-family: "Danzza", sans-serif;
|
||||
font-size: var(--font-size-paragraph-small);
|
||||
color: #ff6b6b;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-6px); }
|
||||
40% { transform: translateX(6px); }
|
||||
60% { transform: translateX(-4px); }
|
||||
80% { transform: translateX(3px); }
|
||||
}
|
||||
|
||||
.hp {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue