summaryrefslogtreecommitdiff
path: root/http
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2026-06-04 15:15:02 +0100
committerJoe Carstairs <me@joeac.net>2026-06-04 15:16:34 +0100
commit813836fbba884388e6013e9fd377430ef33b6c8d (patch)
tree056149f5c51eabaa89960a7d8d2eb68fe9b158e8 /http
parentfac1199cd61e069b1a144d2548477a2c3b3b94b0 (diff)
http: adds scripts
Diffstat (limited to 'http')
-rw-r--r--http/public/scripts/contact/post-error-message.js34
-rw-r--r--http/public/scripts/contact/resend-otp.js31
-rw-r--r--http/public/scripts/contact/reset-resend-button.js32
-rw-r--r--http/public/scripts/contact/selectors.js25
-rw-r--r--http/public/scripts/contact/submit-contact-form.js53
-rw-r--r--http/public/scripts/contact/submit-otp-form.js77
-rw-r--r--http/public/scripts/otp-form-wc.js68
7 files changed, 320 insertions, 0 deletions
diff --git a/http/public/scripts/contact/post-error-message.js b/http/public/scripts/contact/post-error-message.js
new file mode 100644
index 0000000..121cbb6
--- /dev/null
+++ b/http/public/scripts/contact/post-error-message.js
@@ -0,0 +1,34 @@
+/** @typedef {import("./selectors.js").Selectors} Selectors */
+
+/**
+ * @param {{contactForm: Selectors}} contactForm
+ * @param {string} errorMsg
+ */
+export function postErrorMessageOnContactForm({ contactForm }, errorMsg) {
+ const errorElem = contactForm.errorElem();
+ if (errorElem) {
+ errorElem.textContent = errorMsg;
+ errorElem.removeAttribute("hidden");
+ } else {
+ alert(errorMsg);
+ }
+}
+
+/**
+ * @param {{otpDialog: Selectors}} otpDialog
+ * @param {string} errorMsg
+ */
+export function postErrorMessageOnOtpForm({ otpDialog }, errorMsg) {
+ const errorElem = otpDialog.errorElem();
+ if (errorElem) {
+ errorElem.textContent = errorMsg;
+ errorElem.removeAttribute("hidden");
+ } else {
+ alert(errorMsg);
+ }
+ /** @type NodeListOf<HTMLInputElement> */
+ const inputs = otpDialog.allOtpInputs();
+ for (const input of inputs) {
+ input.value = "";
+ }
+}
diff --git a/http/public/scripts/contact/resend-otp.js b/http/public/scripts/contact/resend-otp.js
new file mode 100644
index 0000000..e083d95
--- /dev/null
+++ b/http/public/scripts/contact/resend-otp.js
@@ -0,0 +1,31 @@
+/** @typedef {import("./selectors.js").Selectors} Selectors */
+
+import { actions } from "./actions.js";
+import { resetResendButton } from "./reset-resend-button.js";
+import { postErrorMessageOnOtpForm } from "./post-error-message.js";
+
+/** @typedef {{ resendButtonInterval: NodeJS.Timeout | undefined }} Result */
+
+const fallbackErrorMsg =
+ "No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
+
+/**
+ * @param {Selectors} selectors
+ * @param {NodeJS.Timeout | undefined} resetButtonInterval
+ * @returns {Promise<Result>}
+ */
+export async function resendOtp(selectors, resetButtonInterval) {
+ const result = resetResendButton(selectors, resetButtonInterval);
+ const name = selectors.contactForm.nameElem()?.value;
+ const email = selectors.contactForm.emailElem().value;
+
+ try {
+ await actions.sendOtp({ type: "email", name, email });
+ } catch (sendOtpError) {
+ const errorMsg = sendOtpError?.toString() ?? fallbackErrorMsg;
+ postErrorMessageOnOtpForm(selectors, errorMsg);
+ throw sendOtpError;
+ }
+
+ return result;
+}
diff --git a/http/public/scripts/contact/reset-resend-button.js b/http/public/scripts/contact/reset-resend-button.js
new file mode 100644
index 0000000..47bc9ac
--- /dev/null
+++ b/http/public/scripts/contact/reset-resend-button.js
@@ -0,0 +1,32 @@
+/** @typedef {import("./selectors.js").Selectors} Selectors */
+
+/** @typedef {{ resendButtonInterval: NodeJS.Timeout | undefined }} Result */
+
+/**
+ * @param {Selectors} selectors
+ * @param {NodeJS.Timeout | undefined} resetButtonInterval
+ * @returns {Result}
+ */
+export function resetResendButton({ otpDialog }, resendButtonInterval) {
+ clearInterval(resendButtonInterval);
+
+ const resendButton = otpDialog.resendButton();
+ if (resendButton) {
+ resendButton.setAttribute("data-countdown", "60");
+ resendButton.setAttribute("disabled", "");
+
+ resendButtonInterval = setInterval(() => {
+ const countdown = +(resendButton.getAttribute("data-countdown") ?? 1) - 1;
+ resendButton.setAttribute("data-countdown", countdown.toString());
+ resendButton.textContent = `Resend (${countdown}s)`;
+ }, 1000);
+
+ setTimeout(() => {
+ clearInterval(resendButtonInterval);
+ resendButton.textContent = "Resend";
+ resendButton.removeAttribute("disabled");
+ }, 1000 * 60);
+ }
+
+ return { resendButtonInterval };
+}
diff --git a/http/public/scripts/contact/selectors.js b/http/public/scripts/contact/selectors.js
new file mode 100644
index 0000000..7676414
--- /dev/null
+++ b/http/public/scripts/contact/selectors.js
@@ -0,0 +1,25 @@
+/**
+ * @typedef {Object} Selectors
+ * @property {Object} contactForm
+ * @property {() => HTMLInputElement} contactForm.emailElem
+ * @property {() => Element | null} contactForm.errorElem
+ * @property {() => HTMLInputElement} contactForm.nameElem
+ * @property {() => HTMLTextAreaElement} contactForm.messageElem
+ * @property {() => HTMLFormElement | null} contactForm.self
+ * @property {() => HTMLInputElement | null} contactForm.submitButton
+ * @property {Object} otpDialog
+ * @property {() => NodeListOf<HTMLInputElement>} otpDialog.allOtpInputs
+ * @property {() => Element | null} otpDialog.errorElem
+ * @property {() => HTMLInputElement | null} otpDialog.firstOtpInput
+ * @property {() => HTMLFormElement | null} otpDialog.otpForm
+ * @property {() => Element | null} otpDialog.otpRecipient
+ * @property {() => Element | null} otpDialog.otpValidUntil
+ * @property {() => HTMLButtonElement | null} otpDialog.resendButton
+ * @property {() => HTMLDialogElement} otpDialog.self
+ * @property {() => HTMLInputElement | null} otpDialog.submitButton
+ * @property {Object} successSection
+ * @property {() => Element | null} successSection.email
+ * @property {() => Element | null} successSection.message
+ * @property {() => Element | null} successSection.name
+ * @property {() => Element | null} successSection.self
+ */
diff --git a/http/public/scripts/contact/submit-contact-form.js b/http/public/scripts/contact/submit-contact-form.js
new file mode 100644
index 0000000..a90f420
--- /dev/null
+++ b/http/public/scripts/contact/submit-contact-form.js
@@ -0,0 +1,53 @@
+import { actions } from "./actions.js";
+import { postErrorMessageOnContactForm } from "./post-error-message.js";
+import { resetResendButton } from "./reset-resend-button.js";
+
+/** @typedef {import("./selectors.js").Selectors} Selectors */
+
+const fallbackErrorMsg =
+ "No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
+
+/**
+ * @param {Selectors} selectors
+ * @param {NodeJS.Timeout | undefined} resetButtonInterval
+ * @returns {Promise<Result>}
+ */
+export async function submitContactForm(selectors, resendButtonInterval) {
+ const { contactForm, otpDialog } = selectors;
+
+ const name = contactForm.nameElem()?.value;
+ const email = contactForm.emailElem().value;
+
+ contactForm.submitButton()?.setAttribute("disabled", "");
+ try {
+ await actions.sendOtp({ type: "email", name, email });
+ } catch (sendOtpError) {
+ const errorMsg = sendOtpError?.toString() ?? fallbackErrorMsg;
+ postErrorMessageOnContactForm(selectors, errorMsg);
+ throw sendOtpError;
+ }
+
+ const otpRecipient = otpDialog.otpRecipient();
+ email && otpRecipient && (otpRecipient.textContent = `<${email}>`);
+ const otpValidUntil = otpDialog.otpValidUntil();
+ const validUntil = new Date(Date.now() + 1000 * 60 * 5);
+ otpValidUntil &&
+ (otpValidUntil.textContent = `until ${validUntil.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`);
+
+ const dialog = otpDialog.self();
+ dialog.showModal();
+ contactForm.submitButton()?.removeAttribute("disabled");
+ dialog.addEventListener("click", function (event) {
+ const rect = dialog.getBoundingClientRect();
+ const isInDialog =
+ rect.top <= event.clientY &&
+ event.clientY <= rect.top + rect.height &&
+ rect.left <= event.clientX &&
+ event.clientX <= rect.left + rect.width;
+ if (!isInDialog) {
+ dialog.close();
+ }
+ });
+
+ return resetResendButton(selectors, resendButtonInterval);
+}
diff --git a/http/public/scripts/contact/submit-otp-form.js b/http/public/scripts/contact/submit-otp-form.js
new file mode 100644
index 0000000..8c09314
--- /dev/null
+++ b/http/public/scripts/contact/submit-otp-form.js
@@ -0,0 +1,77 @@
+import { actions } from "./actions.js";
+import { postErrorMessageOnOtpForm } from "./post-error-message.js";
+
+/** @typedef {import("./selectors.js").Selectors} Selectors */
+
+const fallbackErrorMsg =
+ "No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
+
+/**
+ * @param {Selectors} selectors
+ */
+export async function submitOtpForm(selectors) {
+ const { contactForm, otpDialog, successSection } = selectors;
+ const otpForm = otpDialog.otpForm();
+ otpDialog.submitButton()?.setAttribute("disabled", "");
+
+ const otpFormData = new FormData(otpForm ?? undefined);
+ const guess = [
+ otpFormData.get("1"),
+ otpFormData.get("2"),
+ otpFormData.get("3"),
+ otpFormData.get("4"),
+ otpFormData.get("5"),
+ otpFormData.get("6"),
+ ].join("");
+
+ const name = contactForm.nameElem()?.value;
+ const email = contactForm.emailElem().value;
+ const message = contactForm.messageElem()?.value;
+
+ let verifyResult;
+ try {
+ verifyResult = await actions.verifyOtp({ guess, userId: email });
+ } catch (verifyError) {
+ otpDialog.submitButton()?.removeAttribute("disabled");
+ postErrorMessageOnOtpForm(
+ selectors,
+ verifyError?.toString() ?? fallbackErrorMsg,
+ );
+ return;
+ }
+
+ if (verifyResult === false) {
+ otpDialog.submitButton()?.removeAttribute("disabled");
+ postErrorMessageOnOtpForm(selectors, "Incorrect OTP. Check your email?");
+ otpDialog.firstOtpInput()?.focus();
+ return;
+ }
+ const sendmailToken = verifyResult;
+
+ try {
+ await actions.sendEmail({
+ email,
+ message: message,
+ name: name,
+ userId: email,
+ token: sendmailToken,
+ });
+ } catch (sendEmailError) {
+ const errorMsg = sendEmailError?.toString() ?? fallbackErrorMsg;
+ postErrorMessageOnOtpForm(selectors, errorMsg);
+ otpDialog.submitButton()?.removeAttribute("disabled");
+ return;
+ }
+
+ const sentName = successSection.name();
+ const sentEmail = successSection.email();
+ const sentMessage = successSection.message();
+ sentName && (sentName.textContent = name ?? "???");
+ sentEmail && (sentEmail.textContent = email ?? "???");
+ sentMessage && (sentMessage.textContent = message ?? "???");
+
+ contactForm.self()?.remove();
+ successSection.self()?.removeAttribute("hidden");
+ otpDialog.submitButton()?.removeAttribute("disabled");
+ otpDialog.self().close();
+}
diff --git a/http/public/scripts/otp-form-wc.js b/http/public/scripts/otp-form-wc.js
new file mode 100644
index 0000000..49e1b02
--- /dev/null
+++ b/http/public/scripts/otp-form-wc.js
@@ -0,0 +1,68 @@
+class OtpForm extends HTMLElement {
+ /** @type MutationObserver? */
+ observer;
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ this.observer = new MutationObserver(() => {
+ this.clearInputs();
+ this.configureInputs();
+ });
+ this.observer.observe(this, { childList: true, subtree: true });
+ }
+
+ disconnectedCallback() {
+ this.observer?.disconnect();
+ }
+
+ clearInputs() {
+ console.log("clearing all inputs");
+ /** @type NodeListOf<HTMLInputElement> */
+ const inputs = this.querySelectorAll('input:not([type="submit"])');
+ for (const input of inputs) {
+ input.value = "";
+ }
+ }
+
+ configureInputs() {
+ this.observer?.disconnect();
+ /** @type NodeListOf<HTMLInputElement> */
+ const inputs = this.querySelectorAll('input:not([type="submit"])');
+ /** @type NodeListOf<HTMLFormElement> */
+ const form = this.querySelector("form");
+
+ for (const input of inputs) {
+ input.addEventListener("focus", () => {
+ input.select();
+ });
+
+ input.addEventListener("input", () => {
+ if (input.value.length > 0) {
+ input.value = input.value.slice(0, 1).toLocaleUpperCase();
+ /** @type HTMLInputElement */
+ const nextInput = input.nextElementSibling;
+ if (nextInput) {
+ nextInput.focus();
+ return;
+ }
+
+ /** @type HTMLInputElement */
+ const submitButton = form.querySelector('input[type="submit"]');
+ submitButton.focus();
+ let areAllInputsEntered = true;
+ inputs.forEach((input) => {
+ areAllInputsEntered = areAllInputsEntered && input.value.length > 0;
+ });
+ if (areAllInputsEntered) {
+ form.requestSubmit(submitButton);
+ }
+ }
+ });
+ }
+ }
+}
+
+customElements.define("otp-form", OtpForm);