diff --git a/assets/js/src/contact.js b/assets/js/src/contact.js index 80f2e904..a03d40de 100644 --- a/assets/js/src/contact.js +++ b/assets/js/src/contact.js @@ -1,89 +1,142 @@ import "vanilla-hcaptcha"; +import { h, render } from "preact"; +import { useState } from "preact/hooks"; import fetch from "unfetch"; -// don't continue if there isn't a contact form on this page -// TODO: be better and only do any of this on /contact/ -const contactForm = document.querySelector("form#contact-form"); +const CONTACT_ENDPOINT = "/api/contact/"; -if (contactForm) { - contactForm.addEventListener("submit", (event) => { - // immediately prevent
from actually submitting to a new page - event.preventDefault(); +const ContactForm = () => { + // status/feedback: + const [status, setStatus] = useState({ success: false, action: "Submit", message: "" }); + // keep track of fetch: + const [sending, setSending] = useState(false); - // feedback s for later - const successSpan = document.querySelector("span#contact-form-result-success"); - const errorSpan = document.querySelector("span#contact-form-result-error"); + const onSubmit = async (e) => { + // immediately prevent browser from actually navigating to a new page + e.preventDefault(); + + // begin the process + setSending(true); + + // extract data from form fields + const { name, email, message } = e.target.elements; + const formData = { + name: name.value, + email: email.value, + message: message.value, + "h-captcha-response": e.target.elements["h-captcha-response"].value, + }; + + // some client-side validation. these are all also checked on the server to be safe but we can save some + // unnecessary requests here. + if (!(formData.name && formData.email && formData.message && formData["h-captcha-response"])) { + setSending(false); + setStatus({ success: false, action: "Try Again", message: "Please make sure that all fields are filled in." }); + + // remove focus from the submit button + document.activeElement.blur(); - // disable the whole form if the button has been disabled below (on success) - const submitButton = document.querySelector("button#contact-form-btn-submit"); - if (submitButton.disabled === true) { return; } - // change button appearance between click and server response - submitButton.textContent = "Sending..."; - submitButton.disabled = true; // prevent accidental multiple submissions - submitButton.style.cursor = "default"; + // if we've gotten here then all data is (or should be) valid and ready to post to API + fetch(CONTACT_ENDPOINT, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(formData), + }) + .then((response) => response.json()) + .then((data) => { + setSending(false); - try { - // https://simonplend.com/how-to-use-fetch-to-post-form-data-as-json-to-your-api/ - const formData = Object.fromEntries(new FormData(event.currentTarget).entries()); - - // some client-side validation. these are all also checked on the server to be safe but we can save some - // unnecessary requests here. - // we throw identical error messages to the server's so they're caught in the same way below. - if (!formData.name || !formData.email || !formData.message) { - throw new Error("USER_MISSING_DATA"); - } - if (!formData["h-captcha-response"]) { - throw new Error("USER_INVALID_CAPTCHA"); - } - - // post JSONified form input to /api/contact/ - fetch(contactForm.action, { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(formData), + if (data.success === true) { + // handle successful submission + // disable submissions, hide the send button, and let user know we were successful + setStatus({ success: true, action: "", message: "Success! You should hear from me soon. :)" }); + } else { + // pass on any error sent by the server + throw new Error(data.message); + } }) - .then((response) => response.json()) - .then((data) => { - if (data.success === true) { - // handle successful submission - // we can disable submissions & hide the send button now - submitButton.disabled = true; - submitButton.style.display = "none"; + .catch((error) => { + const message = error instanceof Error ? error.message : "UNKNOWN_EXCEPTION"; - // just in case there *was* a PEBCAK error and it was corrected - errorSpan.style.display = "none"; + setSending(false); - // let user know we were successful - successSpan.textContent = "Success! You should hear from me soon. :)"; - } else { - // pass on an error sent by the server - throw new Error(data.message); - } - }); - } catch (error) { - const message = error instanceof Error ? error.message : "UNKNOWN_EXCEPTION"; + // give user feedback based on the error message returned + if (message === "USER_INVALID_CAPTCHA") { + setStatus({ + success: false, + action: "Try Again", + message: "Did you complete the CAPTCHA? (If you're human, that is...)", + }); + } else if (message === "USER_MISSING_DATA") { + setStatus({ + success: false, + action: "Try Again", + message: "Please make sure that all fields are filled in.", + }); + } else { + // something else went wrong, and it's probably my fault... + setStatus({ success: false, action: "Try Again", message: "Internal server error. Try again later?" }); + } - // give user feedback based on the error message returned - if (message === "USER_INVALID_CAPTCHA") { - errorSpan.textContent = "Did you complete the CAPTCHA? (If you're human, that is...)"; - } else if (message === "USER_MISSING_DATA") { - errorSpan.textContent = "Please make sure that all fields are filled in."; - } else { - // something else went wrong, and it's probably my fault... - errorSpan.textContent = "Internal server error. Try again later?"; - } + // remove focus from the submit button + document.activeElement.blur(); + }); + }; - // reset submit button to let user try again - submitButton.textContent = "Try Again"; - submitButton.disabled = false; - submitButton.style.cursor = "pointer"; - submitButton.blur(); // remove keyboard focus from the button - } - }); + return ( + + + + - - Basic Markdown syntax is allowed here, e.g.: **bold**, _italics_, [links](https://jarv.is), and `code`. - - - -
- -
- - -
-
- +
{{ end }} diff --git a/webpack.config.js b/webpack.config.js index f8f626b0..58610665 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -90,6 +90,10 @@ export default (env, argv) => { }, ], }), + new webpack.EnvironmentPlugin([ + // we need to dynamically inject the hcaptcha site key into the contact form + "HCAPTCHA_SITE_KEY", + ]), new AssetsManifestPlugin({ writeToDisk: true, // allow Hugo to access file in dev mode output: path.resolve(__dirname, "data/manifest.json"),