seahaven-site/assets/js/form.js

89 lines
3.1 KiB
JavaScript
Raw Normal View History

// Basin AJAX submission with lazily-loaded reCAPTCHA v3 for the contact form
// and job apply forms. The reCAPTCHA script is not loaded until the visitor
// first interacts with a form (or submits), keeping it off the initial page load.
Migrate to thin Eleventy build + a11y/SEO/perf/CI hardening (#20) * chore(build): add Eleventy scaffold Thin Eleventy build (v3.1.6, pinned) — passthrough-copies assets/, robots.txt, sitemap.xml; outputs flat HTML to _site/. _data/site.json holds site-wide constants; _data/images.json maps image keys to src+width+height for the {% image %} shortcode (CLS fix). Output stays flat HTML served from the same S3 bucket + CloudFront. * refactor(templates): base layout, partials, shared JS, CSS extraction - _includes/base.njk + nav/mobile-menu/footer partials reproduce the shared chrome once (was hand-duplicated across 13 pages). Adds a skip-link and <main> landmark (WCAG 2.4.1), aria-expanded/role=dialog hooks on the menu, and a {% year %} shortcode replacing document.write. - assets/js/nav.js: extracted sticky-nav + accessible mobile-menu dialog (focus trap, Escape, focus return) + rAF-throttled hero parallax. - assets/js/form.js: Basin AJAX submit with an accessible status region. - assets/css/*.css: per-page inline <style> extracted into page CSS files (home/about/services/careers/jobs/contact/social/legal/404); skip-link + :focus-visible added to main.css. - index.njk: homepage converted as the reference page. * fix(css): make hero-bg url root-relative after extraction Inline CSS used a document-relative url('assets/...') that resolves correctly from / but breaks once moved into /assets/css/home.css. Rewrite to /assets/images/. * refactor(pages): convert 12 pages to Eleventy templates Convert about, services, careers (listing + 3 jobs), contact, social- accountability, privacy-policy, terms-of-service, eula, and 404 from standalone HTML to .njk against base.njk. Each page now carries only front-matter (title/description/SEO) + its <main> content; shared head/ nav/footer/scripts come from the layout. JobPosting + LocalBusiness JSON-LD preserved. Images use the {% image %} shortcode (width/height). Contact gets an accessible #form-status region. form.js generalized to wire BOTH the contact form and the .apply-form job application forms (was contact-only), preserving each submit button's own label. * fix(a11y): footer contrast to WCAG AA + scope services .form-group Raise footer text colors (footer-bottom/col/brand/social/contact) and darken --text-muted so muted text clears 4.5:1 on the dark footer and warm-gray surfaces. Scope services' flex .form-group override to .contact-form .form-group so it can't leak to the global rule. * perf(seo): og-cover image, webp logos, hero preload Add a real 1200x630 og-cover.jpg (was a 153x49 favicon) wired site-wide via base.njk og:image/twitter:image. Convert nav/footer logos to webp (nav 58KB->22KB); PNGs kept as passthrough so old URLs still resolve. Preload the LCP hero image on the homepage (fetchpriority=high). All <img> carry width/height via the image shortcode (CLS). * ci(deploy): build-then-sync, cache headers, safe concurrency Rename main.yml -> deploy.yaml (org convention). Build with Eleventy (npm ci && npm run build) and sync _site/ instead of the repo root, so only built output ships (no source/templates/node_modules). Split Cache-Control (1-day assets, no-cache HTML) and keep /* invalidation since filenames are not yet fingerprinted. concurrency cancel-in-progress false so a deploy is never cut mid sync. ci.yaml validates _site/ via ci-static build mode. * docs: README for the Eleventy build and structure * fix(security): wire services form, guard build, harden deploy Fable build-review findings: - BLOCK: services puts .contact-form on the <form> itself (contact uses a wrapper div), so form.js selector '.contact-form form' never matched it — the services lead form submitted natively with an empty reCAPTCHA token. Selector now also matches form.contact-form. - Guard the build before the --delete S3 sync: require index/contact/404 and >=40 files, so a silently-empty build can never wipe the live bucket. - npm ci --ignore-scripts on deploy (build verified to pass) to shrink the supply-chain window on the OIDC-credentialed runner. - Escape quotes in the image shortcode alt text.
2026-06-12 17:02:06 -04:00
(function () {
"use strict";
const SITE_KEY = "6Les66kUAAAAANyLrgkl7iuN4JUpNlB5upaMovI4";
const forms = document.querySelectorAll(".contact-form form, form.contact-form, form.apply-form");
if (!forms.length) return;
// ── Lazy reCAPTCHA loader ──────────────────────────────────────
let loadStarted = false;
function loadRecaptcha() {
if (loadStarted) return;
loadStarted = true;
const s = document.createElement("script");
s.src = "https://www.google.com/recaptcha/api.js?render=" + SITE_KEY;
s.async = true;
document.head.appendChild(s);
}
// Resolves once grecaptcha is ready; loads the script if not already loading.
function readyRecaptcha() {
loadRecaptcha();
return new Promise((resolve, reject) => {
const start = Date.now();
(function check() {
if (window.grecaptcha && window.grecaptcha.execute) {
window.grecaptcha.ready(resolve);
} else if (Date.now() - start > 10000) {
reject(new Error("reCAPTCHA failed to load"));
} else {
setTimeout(check, 100);
}
})();
});
}
Migrate to thin Eleventy build + a11y/SEO/perf/CI hardening (#20) * chore(build): add Eleventy scaffold Thin Eleventy build (v3.1.6, pinned) — passthrough-copies assets/, robots.txt, sitemap.xml; outputs flat HTML to _site/. _data/site.json holds site-wide constants; _data/images.json maps image keys to src+width+height for the {% image %} shortcode (CLS fix). Output stays flat HTML served from the same S3 bucket + CloudFront. * refactor(templates): base layout, partials, shared JS, CSS extraction - _includes/base.njk + nav/mobile-menu/footer partials reproduce the shared chrome once (was hand-duplicated across 13 pages). Adds a skip-link and <main> landmark (WCAG 2.4.1), aria-expanded/role=dialog hooks on the menu, and a {% year %} shortcode replacing document.write. - assets/js/nav.js: extracted sticky-nav + accessible mobile-menu dialog (focus trap, Escape, focus return) + rAF-throttled hero parallax. - assets/js/form.js: Basin AJAX submit with an accessible status region. - assets/css/*.css: per-page inline <style> extracted into page CSS files (home/about/services/careers/jobs/contact/social/legal/404); skip-link + :focus-visible added to main.css. - index.njk: homepage converted as the reference page. * fix(css): make hero-bg url root-relative after extraction Inline CSS used a document-relative url('assets/...') that resolves correctly from / but breaks once moved into /assets/css/home.css. Rewrite to /assets/images/. * refactor(pages): convert 12 pages to Eleventy templates Convert about, services, careers (listing + 3 jobs), contact, social- accountability, privacy-policy, terms-of-service, eula, and 404 from standalone HTML to .njk against base.njk. Each page now carries only front-matter (title/description/SEO) + its <main> content; shared head/ nav/footer/scripts come from the layout. JobPosting + LocalBusiness JSON-LD preserved. Images use the {% image %} shortcode (width/height). Contact gets an accessible #form-status region. form.js generalized to wire BOTH the contact form and the .apply-form job application forms (was contact-only), preserving each submit button's own label. * fix(a11y): footer contrast to WCAG AA + scope services .form-group Raise footer text colors (footer-bottom/col/brand/social/contact) and darken --text-muted so muted text clears 4.5:1 on the dark footer and warm-gray surfaces. Scope services' flex .form-group override to .contact-form .form-group so it can't leak to the global rule. * perf(seo): og-cover image, webp logos, hero preload Add a real 1200x630 og-cover.jpg (was a 153x49 favicon) wired site-wide via base.njk og:image/twitter:image. Convert nav/footer logos to webp (nav 58KB->22KB); PNGs kept as passthrough so old URLs still resolve. Preload the LCP hero image on the homepage (fetchpriority=high). All <img> carry width/height via the image shortcode (CLS). * ci(deploy): build-then-sync, cache headers, safe concurrency Rename main.yml -> deploy.yaml (org convention). Build with Eleventy (npm ci && npm run build) and sync _site/ instead of the repo root, so only built output ships (no source/templates/node_modules). Split Cache-Control (1-day assets, no-cache HTML) and keep /* invalidation since filenames are not yet fingerprinted. concurrency cancel-in-progress false so a deploy is never cut mid sync. ci.yaml validates _site/ via ci-static build mode. * docs: README for the Eleventy build and structure * fix(security): wire services form, guard build, harden deploy Fable build-review findings: - BLOCK: services puts .contact-form on the <form> itself (contact uses a wrapper div), so form.js selector '.contact-form form' never matched it — the services lead form submitted natively with an empty reCAPTCHA token. Selector now also matches form.contact-form. - Guard the build before the --delete S3 sync: require index/contact/404 and >=40 files, so a silently-empty build can never wipe the live bucket. - npm ci --ignore-scripts on deploy (build verified to pass) to shrink the supply-chain window on the OIDC-credentialed runner. - Escape quotes in the image shortcode alt text.
2026-06-12 17:02:06 -04:00
forms.forEach((form) => {
// Warm the loader as soon as the user engages with the form.
form.addEventListener("focusin", loadRecaptcha, { once: true });
Migrate to thin Eleventy build + a11y/SEO/perf/CI hardening (#20) * chore(build): add Eleventy scaffold Thin Eleventy build (v3.1.6, pinned) — passthrough-copies assets/, robots.txt, sitemap.xml; outputs flat HTML to _site/. _data/site.json holds site-wide constants; _data/images.json maps image keys to src+width+height for the {% image %} shortcode (CLS fix). Output stays flat HTML served from the same S3 bucket + CloudFront. * refactor(templates): base layout, partials, shared JS, CSS extraction - _includes/base.njk + nav/mobile-menu/footer partials reproduce the shared chrome once (was hand-duplicated across 13 pages). Adds a skip-link and <main> landmark (WCAG 2.4.1), aria-expanded/role=dialog hooks on the menu, and a {% year %} shortcode replacing document.write. - assets/js/nav.js: extracted sticky-nav + accessible mobile-menu dialog (focus trap, Escape, focus return) + rAF-throttled hero parallax. - assets/js/form.js: Basin AJAX submit with an accessible status region. - assets/css/*.css: per-page inline <style> extracted into page CSS files (home/about/services/careers/jobs/contact/social/legal/404); skip-link + :focus-visible added to main.css. - index.njk: homepage converted as the reference page. * fix(css): make hero-bg url root-relative after extraction Inline CSS used a document-relative url('assets/...') that resolves correctly from / but breaks once moved into /assets/css/home.css. Rewrite to /assets/images/. * refactor(pages): convert 12 pages to Eleventy templates Convert about, services, careers (listing + 3 jobs), contact, social- accountability, privacy-policy, terms-of-service, eula, and 404 from standalone HTML to .njk against base.njk. Each page now carries only front-matter (title/description/SEO) + its <main> content; shared head/ nav/footer/scripts come from the layout. JobPosting + LocalBusiness JSON-LD preserved. Images use the {% image %} shortcode (width/height). Contact gets an accessible #form-status region. form.js generalized to wire BOTH the contact form and the .apply-form job application forms (was contact-only), preserving each submit button's own label. * fix(a11y): footer contrast to WCAG AA + scope services .form-group Raise footer text colors (footer-bottom/col/brand/social/contact) and darken --text-muted so muted text clears 4.5:1 on the dark footer and warm-gray surfaces. Scope services' flex .form-group override to .contact-form .form-group so it can't leak to the global rule. * perf(seo): og-cover image, webp logos, hero preload Add a real 1200x630 og-cover.jpg (was a 153x49 favicon) wired site-wide via base.njk og:image/twitter:image. Convert nav/footer logos to webp (nav 58KB->22KB); PNGs kept as passthrough so old URLs still resolve. Preload the LCP hero image on the homepage (fetchpriority=high). All <img> carry width/height via the image shortcode (CLS). * ci(deploy): build-then-sync, cache headers, safe concurrency Rename main.yml -> deploy.yaml (org convention). Build with Eleventy (npm ci && npm run build) and sync _site/ instead of the repo root, so only built output ships (no source/templates/node_modules). Split Cache-Control (1-day assets, no-cache HTML) and keep /* invalidation since filenames are not yet fingerprinted. concurrency cancel-in-progress false so a deploy is never cut mid sync. ci.yaml validates _site/ via ci-static build mode. * docs: README for the Eleventy build and structure * fix(security): wire services form, guard build, harden deploy Fable build-review findings: - BLOCK: services puts .contact-form on the <form> itself (contact uses a wrapper div), so form.js selector '.contact-form form' never matched it — the services lead form submitted natively with an empty reCAPTCHA token. Selector now also matches form.contact-form. - Guard the build before the --delete S3 sync: require index/contact/404 and >=40 files, so a silently-empty build can never wipe the live bucket. - npm ci --ignore-scripts on deploy (build verified to pass) to shrink the supply-chain window on the OIDC-credentialed runner. - Escape quotes in the image shortcode alt text.
2026-06-12 17:02:06 -04:00
const success = document.getElementById("form-success");
const statusEl = document.getElementById("form-status"); // optional role="status" region
const tokenField = form.querySelector("#g-recaptcha-response");
const btn = form.querySelector('button[type="submit"]');
const originalLabel = btn ? btn.textContent : "";
const announce = (msg) => {
if (statusEl) statusEl.textContent = msg;
else if (msg) alert(msg);
};
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (btn) {
btn.textContent = btn.dataset.sending || "Sending…";
btn.disabled = true;
}
announce("");
try {
await readyRecaptcha();
const token = await window.grecaptcha.execute(SITE_KEY, { action: "submit" });
Migrate to thin Eleventy build + a11y/SEO/perf/CI hardening (#20) * chore(build): add Eleventy scaffold Thin Eleventy build (v3.1.6, pinned) — passthrough-copies assets/, robots.txt, sitemap.xml; outputs flat HTML to _site/. _data/site.json holds site-wide constants; _data/images.json maps image keys to src+width+height for the {% image %} shortcode (CLS fix). Output stays flat HTML served from the same S3 bucket + CloudFront. * refactor(templates): base layout, partials, shared JS, CSS extraction - _includes/base.njk + nav/mobile-menu/footer partials reproduce the shared chrome once (was hand-duplicated across 13 pages). Adds a skip-link and <main> landmark (WCAG 2.4.1), aria-expanded/role=dialog hooks on the menu, and a {% year %} shortcode replacing document.write. - assets/js/nav.js: extracted sticky-nav + accessible mobile-menu dialog (focus trap, Escape, focus return) + rAF-throttled hero parallax. - assets/js/form.js: Basin AJAX submit with an accessible status region. - assets/css/*.css: per-page inline <style> extracted into page CSS files (home/about/services/careers/jobs/contact/social/legal/404); skip-link + :focus-visible added to main.css. - index.njk: homepage converted as the reference page. * fix(css): make hero-bg url root-relative after extraction Inline CSS used a document-relative url('assets/...') that resolves correctly from / but breaks once moved into /assets/css/home.css. Rewrite to /assets/images/. * refactor(pages): convert 12 pages to Eleventy templates Convert about, services, careers (listing + 3 jobs), contact, social- accountability, privacy-policy, terms-of-service, eula, and 404 from standalone HTML to .njk against base.njk. Each page now carries only front-matter (title/description/SEO) + its <main> content; shared head/ nav/footer/scripts come from the layout. JobPosting + LocalBusiness JSON-LD preserved. Images use the {% image %} shortcode (width/height). Contact gets an accessible #form-status region. form.js generalized to wire BOTH the contact form and the .apply-form job application forms (was contact-only), preserving each submit button's own label. * fix(a11y): footer contrast to WCAG AA + scope services .form-group Raise footer text colors (footer-bottom/col/brand/social/contact) and darken --text-muted so muted text clears 4.5:1 on the dark footer and warm-gray surfaces. Scope services' flex .form-group override to .contact-form .form-group so it can't leak to the global rule. * perf(seo): og-cover image, webp logos, hero preload Add a real 1200x630 og-cover.jpg (was a 153x49 favicon) wired site-wide via base.njk og:image/twitter:image. Convert nav/footer logos to webp (nav 58KB->22KB); PNGs kept as passthrough so old URLs still resolve. Preload the LCP hero image on the homepage (fetchpriority=high). All <img> carry width/height via the image shortcode (CLS). * ci(deploy): build-then-sync, cache headers, safe concurrency Rename main.yml -> deploy.yaml (org convention). Build with Eleventy (npm ci && npm run build) and sync _site/ instead of the repo root, so only built output ships (no source/templates/node_modules). Split Cache-Control (1-day assets, no-cache HTML) and keep /* invalidation since filenames are not yet fingerprinted. concurrency cancel-in-progress false so a deploy is never cut mid sync. ci.yaml validates _site/ via ci-static build mode. * docs: README for the Eleventy build and structure * fix(security): wire services form, guard build, harden deploy Fable build-review findings: - BLOCK: services puts .contact-form on the <form> itself (contact uses a wrapper div), so form.js selector '.contact-form form' never matched it — the services lead form submitted natively with an empty reCAPTCHA token. Selector now also matches form.contact-form. - Guard the build before the --delete S3 sync: require index/contact/404 and >=40 files, so a silently-empty build can never wipe the live bucket. - npm ci --ignore-scripts on deploy (build verified to pass) to shrink the supply-chain window on the OIDC-credentialed runner. - Escape quotes in the image shortcode alt text.
2026-06-12 17:02:06 -04:00
if (tokenField) tokenField.value = token;
const res = await fetch(form.action, {
method: "POST",
body: new FormData(form),
headers: { Accept: "application/json" },
});
if (!res.ok) throw new Error("Submission failed");
form.reset();
form.style.display = "none";
if (success) {
success.style.display = "block";
success.scrollIntoView({ behavior: "smooth", block: "center" });
}
} catch (err) {
if (btn) {
btn.textContent = originalLabel;
btn.disabled = false;
}
announce(
"Something went wrong. Please try again or email us directly at work-orders@seahaven.com"
);
}
});
});
})();