/** * Newsletter signup — posts the visitor's email to a configured serverless * endpoint. No credential is ever sent: the serverless function authenticates * with an API token it reads from platform env/secrets at deploy time. * * Pure-ish by design (no DOM, injectable fetch), so every behaviour is unit * testable in Node; the browser wiring at the bottom is guarded accordingly. */ import { NEWSLETTER_ENDPOINT } from "./newsletter-config.js"; /** User-safe message shown when the signup cannot be completed. */ export const NEWSLETTER_ERROR_MESSAGE = "Sorry, the newsletter signup isn't available right now. Please try again later."; /** Confirmation message shown after a successful signup. */ export const NEWSLETTER_SUCCESS_MESSAGE = "Thanks for subscribing!"; /** Simple email shape check; the HTML5 `type="email"` input is the primary gate. */ export function isValidEmail(value) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value).trim()); } /** * Read the trimmed email value from a form. * Works with a real DOM form and with the minimal fake form used in tests. * * @param {{querySelectorAll(selector: string): ArrayLike<{name: string, value: string}>}} form * @returns {string} */ export function readFormEmail(form) { for (const el of form.querySelectorAll("[name]")) { if (el.name === "email") return el.value.trim(); } return ""; } /** * POST the email to the serverless endpoint with no credential in the request * — no auth header, no token in the body or URL. The serverless function reads * its API token from platform env/secrets, never from the client. A clearly * invalid email fails fast with a user-safe error and no network call. Any * non-2xx response and any network failure map to the same generic message, so * the endpoint internals, any status code, and any raw response body are never * surfaced to the visitor. * * @param {string} email * @param {{endpoint?: string, fetchImpl?: typeof fetch}} [options] * @returns {Promise<{ok: boolean, message: string}>} */ export async function submitNewsletterSignup( email, { endpoint = NEWSLETTER_ENDPOINT, fetchImpl = fetch } = {}, ) { if (!isValidEmail(email)) { return { ok: false, message: NEWSLETTER_ERROR_MESSAGE }; } let response; try { response = await fetchImpl(endpoint, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ email }), }); } catch { return { ok: false, message: NEWSLETTER_ERROR_MESSAGE }; } if (!response.ok) { return { ok: false, message: NEWSLETTER_ERROR_MESSAGE }; } return { ok: true, message: NEWSLETTER_SUCCESS_MESSAGE }; } /** * Handle a newsletter form submit: validate the email, POST it, and surface * the result (confirmation or user-safe error) through `setStatus`. * * @param {HTMLFormElement} form * @param {{endpoint?: string, fetchImpl?: typeof fetch, setStatus?: (message: string, kind: "success"|"error") => void}} [options] * @returns {Promise<{ok: boolean, message: string}>} */ export async function handleNewsletterSubmit( form, { endpoint = NEWSLETTER_ENDPOINT, fetchImpl = fetch, setStatus = defaultSetStatus, } = {}, ) { const email = readFormEmail(form); if (!isValidEmail(email)) { setStatus(NEWSLETTER_ERROR_MESSAGE, "error"); return { ok: false, message: NEWSLETTER_ERROR_MESSAGE }; } const result = await submitNewsletterSignup(email, { endpoint, fetchImpl }); setStatus(result.message, result.ok ? "success" : "error"); return result; } /** Default status renderer — puts the message into `#newsletter-status`. */ function defaultSetStatus(message, kind) { const el = document.getElementById("newsletter-status"); if (el) { el.textContent = message; el.dataset.kind = kind; } } /** * Wire the submit handler onto a newsletter form. * @param {HTMLFormElement} form */ export function initNewsletterForm(form, options = {}) { form.addEventListener("submit", (event) => { event.preventDefault(); handleNewsletterSubmit(form, options); }); } // Browser-only wiring — guarded so this module stays importable in Node tests. if (typeof window !== "undefined" && typeof document !== "undefined") { const form = document.getElementById("newsletter-form"); if (form) initNewsletterForm(form); }