From 9c9a2fd40aa0a3d3b9e75f3cc6f8bbc601b907ee Mon Sep 17 00:00:00 2001 From: isUnknown Date: Fri, 27 Feb 2026 15:12:35 +0100 Subject: [PATCH] feat: newsletter form fonctionnel via Brevo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Route Kirby `api/newsletter` (proxy vers l'API Brevo) dans site/config/routes/newsletter.php - JS de soumission du formulaire dans assets/js/newsletter-brevo.js - Chargement du script dans le template newsletter.php - Clé API dans config.index.ngo.php (gitignored) Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 5 ++ assets/js/newsletter-brevo.js | 89 +++++++++++++++++++++++ site/config/config.php | 8 ++- site/config/routes/newsletter.php | 116 ++++++++++++++++++++++++++++++ site/templates/newsletter.php | 1 + 5 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 assets/js/newsletter-brevo.js create mode 100644 site/config/routes/newsletter.php diff --git a/.gitignore b/.gitignore index 510df57..a0f7b25 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,11 @@ Icon /site/config/.license +# Host-specific config (credentials) +# --------------- + +/site/config/config.index.ngo.php + # Content # --------------- diff --git a/assets/js/newsletter-brevo.js b/assets/js/newsletter-brevo.js new file mode 100644 index 0000000..67b1b91 --- /dev/null +++ b/assets/js/newsletter-brevo.js @@ -0,0 +1,89 @@ +(function () { + 'use strict'; + + const PROXY_URL = '/api/newsletter'; + + async function subscribeToNewsletter(email, attributes = {}) { + const response = await fetch(PROXY_URL, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, attributes }), + }); + + const data = await response.json(); + + if (!response.ok) { + const error = new Error( + data.user_message || data.message || 'Subscription error' + ); + error.code = data.error; + error.data = data; + throw error; + } + + return data; + } + + function showMessage(form, text, isError = false) { + const oldMessages = form.parentNode.querySelectorAll('.newsletter-message'); + oldMessages.forEach((msg) => msg.remove()); + + const message = document.createElement('p'); + message.className = 'newsletter-message'; + message.textContent = text; + message.style.marginTop = '0.5rem'; + message.style.fontSize = '0.9rem'; + message.style.color = isError + ? 'var(--color-error, #ef4444)' + : 'var(--color-success, #22c55e)'; + + form.parentNode.insertBefore(message, form.nextSibling); + + if (!isError) { + setTimeout(() => message.remove(), 5000); + } + } + + async function handleFormSubmit(event) { + event.preventDefault(); + + const form = event.target; + const emailInput = form.querySelector('input[type="email"]'); + const submitButton = form.querySelector('button[type="submit"]'); + + if (!emailInput || !emailInput.value) { + const message = document.documentElement.lang.startsWith('en') + ? 'Please enter a valid email address.' + : 'Veuillez entrer une adresse email valide.'; + showMessage(form, message, true); + return; + } + + const email = emailInput.value.trim(); + submitButton.disabled = true; + + try { + await subscribeToNewsletter(email); + const message = document.documentElement.lang.startsWith('en') + ? 'Thank you! Your subscription has been confirmed.' + : 'Merci, votre inscription est confirmée !'; + showMessage(form, message, false); + form.reset(); + } catch (error) { + const isAlreadySubscribed = error.code === 'email_already_exists'; + showMessage(form, error.message, !isAlreadySubscribed); + } finally { + submitButton.disabled = false; + } + } + + function initNewsletterForms() { + const forms = document.querySelectorAll('.form__newsletter'); + forms.forEach((form) => form.addEventListener('submit', handleFormSubmit)); + } + + document.addEventListener('DOMContentLoaded', initNewsletterForms); +})(); diff --git a/site/config/config.php b/site/config/config.php index b55f626..766c527 100644 --- a/site/config/config.php +++ b/site/config/config.php @@ -16,7 +16,7 @@ return [ 'default' => [ 'width' => 1024, 'format' => 'webp' ], - 'full' => 2048, + 'full' => 2048, 'format' => 'webp' ], 'srcsets' => [ @@ -76,6 +76,10 @@ return [ ], 'tobimori.seo.canonicalBase' => 'https://www.index.ngo', + 'routes' => [ + require(__DIR__ . '/routes/newsletter.php'), + ], + 'hooks' => [ 'page.update:after' => function ($newPage) { if ($newPage->intendedTemplate()->name() !== 'investigation') { @@ -112,4 +116,4 @@ return [ } } ] -]; \ No newline at end of file +]; diff --git a/site/config/routes/newsletter.php b/site/config/routes/newsletter.php new file mode 100644 index 0000000..577ed2d --- /dev/null +++ b/site/config/routes/newsletter.php @@ -0,0 +1,116 @@ + 'api/newsletter', + 'method' => 'POST|OPTIONS', + 'action' => function () { + header('Content-Type: application/json'); + + if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + http_response_code(200); + die(); + } + + $config = kirby()->option('brevo'); + $apiKey = $config['api_key'] ?? ''; + $listId = (int)($config['list_id'] ?? 2); + $apiUrl = $config['api_url'] ?? 'https://api.brevo.com/v3/contacts'; + + if (empty($apiKey)) { + http_response_code(500); + die(json_encode(['error' => 'Server configuration error', 'message' => 'Brevo API key not configured'])); + } + + $input = file_get_contents('php://input'); + $data = json_decode($input, true); + + if (!isset($data['email']) || empty($data['email'])) { + http_response_code(400); + die(json_encode(['error' => 'Email required'])); + } + + $email = filter_var($data['email'], FILTER_VALIDATE_EMAIL); + if ($email === false) { + http_response_code(400); + die(json_encode(['error' => 'Invalid email'])); + } + + $brevoData = [ + 'email' => $email, + 'listIds' => [$listId], + 'updateEnabled' => true, + ]; + + if (isset($data['attributes']) && is_array($data['attributes']) && !empty($data['attributes'])) { + $brevoData['attributes'] = $data['attributes']; + } + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $apiUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($brevoData), + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'api-key: ' . $apiKey, + 'User-Agent: Index-NGO-Newsletter', + ], + CURLOPT_TIMEOUT => 10, + 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) { + http_response_code(500); + die(json_encode(['error' => 'Connection error', 'details' => $curlError])); + } + + $responseData = json_decode($response, true); + + switch ($httpCode) { + case 201: + case 204: + http_response_code(200); + die(json_encode(['success' => true, 'message' => 'Successfully subscribed', 'email' => $email])); + + case 400: + $isDuplicate = isset($responseData['code']) && $responseData['code'] === 'duplicate_parameter'; + http_response_code(400); + die(json_encode([ + 'error' => $isDuplicate ? 'email_already_exists' : 'invalid_data', + 'message' => $isDuplicate ? 'You are already subscribed!' : 'Invalid email address.', + 'user_message' => $isDuplicate ? 'Vous êtes déjà inscrit·e !' : 'Veuillez vérifier votre adresse email.', + ])); + + case 401: + http_response_code(500); + die(json_encode([ + 'error' => 'invalid_api_key', + 'message' => 'Invalid API key', + 'user_message' => 'Une erreur technique est survenue. Veuillez réessayer plus tard.', + ])); + + case 404: + http_response_code(500); + die(json_encode([ + 'error' => 'list_not_found', + 'message' => 'Contact list not found', + 'user_message' => 'Une erreur technique est survenue. Veuillez réessayer plus tard.', + ])); + + default: + http_response_code($httpCode); + die(json_encode([ + 'error' => 'api_error', + 'message' => 'Error communicating with subscription service', + 'user_message' => 'Une erreur est survenue. Veuillez réessayer.', + 'http_code' => $httpCode, + ])); + } + }, +]; diff --git a/site/templates/newsletter.php b/site/templates/newsletter.php index 0e0ab55..e9c1ec8 100644 --- a/site/templates/newsletter.php +++ b/site/templates/newsletter.php @@ -1,4 +1,5 @@ +