founders-break/public/js/main.js

119 lines
4.2 KiB
JavaScript

/* The Founder's Break - nav state, scroll reveal, RSVP submit.
No scroll listeners: nav state and reveal both ride IntersectionObserver. */
(function () {
"use strict";
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ── Nav background once the page has moved off the top ── */
var nav = document.getElementById("nav");
var sentinel = document.createElement("div");
sentinel.style.cssText = "position:absolute;top:0;left:0;width:1px;height:1px;pointer-events:none";
document.body.prepend(sentinel);
new IntersectionObserver(function (entries) {
nav.classList.toggle("scrolled", !entries[0].isIntersecting);
}).observe(sentinel);
/* ── Scroll reveal: sequences sections in as they arrive ── */
var reveals = document.querySelectorAll(".reveal");
if (reduceMotion) {
reveals.forEach(function (el) { el.classList.add("in"); });
} else {
var io = new IntersectionObserver(function (entries, obs) {
entries.forEach(function (entry, i) {
if (!entry.isIntersecting) return;
var delay = Math.min(i, 4) * 70;
setTimeout(function () { entry.target.classList.add("in"); }, delay);
obs.unobserve(entry.target);
});
}, { threshold: 0.15, rootMargin: "0px 0px -8% 0px" });
reveals.forEach(function (el) { io.observe(el); });
}
/* ── RSVP ── */
var form = document.getElementById("rsvp-form");
if (!form) return;
var submit = document.getElementById("rsvp-submit");
var status = document.getElementById("form-status");
var done = document.getElementById("rsvp-done");
var nameField = document.getElementById("f-name");
var emailField = document.getElementById("f-email");
function setError(input, message) {
var field = input.closest(".field");
field.classList.toggle("has-error", Boolean(message));
var slot = field.querySelector(".field-error");
if (slot) slot.textContent = message || "";
input.setAttribute("aria-invalid", message ? "true" : "false");
}
function validate() {
var ok = true;
if (!nameField.value.trim()) { setError(nameField, "We need a name to put on the list."); ok = false; }
else setError(nameField, "");
var email = emailField.value.trim();
if (!email) { setError(emailField, "We need an email to send the reminder to."); ok = false; }
else if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) { setError(emailField, "That email does not look right."); ok = false; }
else setError(emailField, "");
return ok;
}
[nameField, emailField].forEach(function (input) {
input.addEventListener("input", function () {
if (input.closest(".field").classList.contains("has-error")) validate();
});
});
form.addEventListener("submit", function (event) {
event.preventDefault();
status.textContent = "";
if (!validate()) {
form.querySelector(".has-error input").focus();
return;
}
submit.setAttribute("aria-busy", "true");
var payload = new URLSearchParams();
new FormData(form).forEach(function (value, key) {
if (key !== "_redirect") payload.append(key, value);
});
fetch(form.action, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
Accept: "application/json",
},
body: payload.toString(),
})
.then(function (res) {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json().catch(function () { return { ok: true }; });
})
.then(function (body) {
if (body && body.ok === false) throw new Error(body.error || "rejected");
showDone();
})
.catch(function () {
submit.removeAttribute("aria-busy");
status.textContent =
"That did not go through. Try again, or email contact@onetouch.studio and we will add you by hand.";
});
});
function showDone() {
form.hidden = true;
done.hidden = false;
done.setAttribute("tabindex", "-1");
done.focus({ preventScroll: true });
}
/* No-JS fallback lands back here with ?rsvp=ok after the plain form POST. */
if (new URLSearchParams(window.location.search).get("rsvp") === "ok") showDone();
})();