http: adds scripts

This commit is contained in:
2026-06-04 15:15:02 +01:00
parent fac1199cd6
commit 813836fbba
7 changed files with 320 additions and 0 deletions
@@ -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 = "";
}
}
+31
View File
@@ -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;
}
@@ -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 };
}
+25
View File
@@ -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
*/
@@ -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);
}
@@ -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();
}
+68
View File
@@ -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);