mirror of
				https://github.com/jakejarvis/jarv.is.git
				synced 2025-11-04 01:50:13 -05:00 
			
		
		
		
	preact-ify contact form
This commit is contained in:
		@@ -1,89 +1,142 @@
 | 
				
			|||||||
import "vanilla-hcaptcha";
 | 
					import "vanilla-hcaptcha";
 | 
				
			||||||
 | 
					import { h, render } from "preact";
 | 
				
			||||||
 | 
					import { useState } from "preact/hooks";
 | 
				
			||||||
import fetch from "unfetch";
 | 
					import fetch from "unfetch";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// don't continue if there isn't a contact form on this page
 | 
					const CONTACT_ENDPOINT = "/api/contact/";
 | 
				
			||||||
// TODO: be better and only do any of this on /contact/
 | 
					 | 
				
			||||||
const contactForm = document.querySelector("form#contact-form");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (contactForm) {
 | 
					const ContactForm = () => {
 | 
				
			||||||
  contactForm.addEventListener("submit", (event) => {
 | 
					  // status/feedback:
 | 
				
			||||||
    // immediately prevent <form> from actually submitting to a new page
 | 
					  const [status, setStatus] = useState({ success: false, action: "Submit", message: "" });
 | 
				
			||||||
    event.preventDefault();
 | 
					  // keep track of fetch:
 | 
				
			||||||
 | 
					  const [sending, setSending] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // feedback <span>s for later
 | 
					  const onSubmit = async (e) => {
 | 
				
			||||||
    const successSpan = document.querySelector("span#contact-form-result-success");
 | 
					    // immediately prevent browser from actually navigating to a new page
 | 
				
			||||||
    const errorSpan = document.querySelector("span#contact-form-result-error");
 | 
					    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;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // change button appearance between click and server response
 | 
					    // if we've gotten here then all data is (or should be) valid and ready to post to API
 | 
				
			||||||
    submitButton.textContent = "Sending...";
 | 
					    fetch(CONTACT_ENDPOINT, {
 | 
				
			||||||
    submitButton.disabled = true; // prevent accidental multiple submissions
 | 
					      method: "POST",
 | 
				
			||||||
    submitButton.style.cursor = "default";
 | 
					      headers: {
 | 
				
			||||||
 | 
					        "Content-Type": "application/json",
 | 
				
			||||||
 | 
					        Accept: "application/json",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      body: JSON.stringify(formData),
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					      .then((response) => response.json())
 | 
				
			||||||
 | 
					      .then((data) => {
 | 
				
			||||||
 | 
					        setSending(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					        if (data.success === true) {
 | 
				
			||||||
      // https://simonplend.com/how-to-use-fetch-to-post-form-data-as-json-to-your-api/
 | 
					          // handle successful submission
 | 
				
			||||||
      const formData = Object.fromEntries(new FormData(event.currentTarget).entries());
 | 
					          // 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. :)" });
 | 
				
			||||||
      // some client-side validation. these are all also checked on the server to be safe but we can save some
 | 
					        } else {
 | 
				
			||||||
      // unnecessary requests here.
 | 
					          // pass on any error sent by the server
 | 
				
			||||||
      // we throw identical error messages to the server's so they're caught in the same way below.
 | 
					          throw new Error(data.message);
 | 
				
			||||||
      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),
 | 
					 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
        .then((response) => response.json())
 | 
					      .catch((error) => {
 | 
				
			||||||
        .then((data) => {
 | 
					        const message = error instanceof Error ? error.message : "UNKNOWN_EXCEPTION";
 | 
				
			||||||
          if (data.success === true) {
 | 
					 | 
				
			||||||
            // handle successful submission
 | 
					 | 
				
			||||||
            // we can disable submissions & hide the send button now
 | 
					 | 
				
			||||||
            submitButton.disabled = true;
 | 
					 | 
				
			||||||
            submitButton.style.display = "none";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // just in case there *was* a PEBCAK error and it was corrected
 | 
					        setSending(false);
 | 
				
			||||||
            errorSpan.style.display = "none";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // let user know we were successful
 | 
					        // give user feedback based on the error message returned
 | 
				
			||||||
            successSpan.textContent = "Success! You should hear from me soon. :)";
 | 
					        if (message === "USER_INVALID_CAPTCHA") {
 | 
				
			||||||
          } else {
 | 
					          setStatus({
 | 
				
			||||||
            // pass on an error sent by the server
 | 
					            success: false,
 | 
				
			||||||
            throw new Error(data.message);
 | 
					            action: "Try Again",
 | 
				
			||||||
          }
 | 
					            message: "Did you complete the CAPTCHA? (If you're human, that is...)",
 | 
				
			||||||
        });
 | 
					          });
 | 
				
			||||||
    } catch (error) {
 | 
					        } else if (message === "USER_MISSING_DATA") {
 | 
				
			||||||
      const message = error instanceof Error ? error.message : "UNKNOWN_EXCEPTION";
 | 
					          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
 | 
					        // remove focus from the submit button
 | 
				
			||||||
      if (message === "USER_INVALID_CAPTCHA") {
 | 
					        document.activeElement.blur();
 | 
				
			||||||
        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?";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // reset submit button to let user try again
 | 
					  return (
 | 
				
			||||||
      submitButton.textContent = "Try Again";
 | 
					    <form onSubmit={onSubmit} id="contact-form" action={CONTACT_ENDPOINT} method="POST">
 | 
				
			||||||
      submitButton.disabled = false;
 | 
					      <input type="text" name="name" placeholder="Name" disabled={status.success} />
 | 
				
			||||||
      submitButton.style.cursor = "pointer";
 | 
					      <input type="email" name="email" placeholder="Email" disabled={status.success} />
 | 
				
			||||||
      submitButton.blur(); // remove keyboard focus from the button
 | 
					      <textarea name="message" placeholder="Write something..." disabled={status.success} />
 | 
				
			||||||
    }
 | 
					
 | 
				
			||||||
  });
 | 
					      <span id="contact-form-md-info">
 | 
				
			||||||
 | 
					        Basic{" "}
 | 
				
			||||||
 | 
					        <a
 | 
				
			||||||
 | 
					          href="https://commonmark.org/help/"
 | 
				
			||||||
 | 
					          title="Markdown reference sheet"
 | 
				
			||||||
 | 
					          target="_blank"
 | 
				
			||||||
 | 
					          rel="noopener noreferrer"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Markdown syntax
 | 
				
			||||||
 | 
					        </a>{" "}
 | 
				
			||||||
 | 
					        is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [
 | 
				
			||||||
 | 
					        <a href="https://jarv.is" target="_blank" rel="noopener noreferrer">
 | 
				
			||||||
 | 
					          links
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					        ](https://jarv.is), and <code>`code`</code>.
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <h-captcha id="contact-form-captcha" site-key={process.env.HCAPTCHA_SITE_KEY} size="normal" tabindex="0" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div id="contact-form-action-row">
 | 
				
			||||||
 | 
					        <button
 | 
				
			||||||
 | 
					          id="contact-form-btn-submit"
 | 
				
			||||||
 | 
					          title={status.action}
 | 
				
			||||||
 | 
					          aria-label={status.action}
 | 
				
			||||||
 | 
					          disabled={sending}
 | 
				
			||||||
 | 
					          style={{ display: status.success ? "none" : null }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {sending ? "Sending..." : status.action}
 | 
				
			||||||
 | 
					        </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <span
 | 
				
			||||||
 | 
					          class="contact-form-result"
 | 
				
			||||||
 | 
					          id={status.success ? "contact-form-result-success" : "contact-form-result-error"}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {status.message}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// don't continue if there isn't a contact form on this page
 | 
				
			||||||
 | 
					if (typeof window !== "undefined" && document.querySelector("div#contact-form-wrapper")) {
 | 
				
			||||||
 | 
					  render(<ContactForm />, document.querySelector("div#contact-form-wrapper"));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -87,7 +87,7 @@ div.layout-contact {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    span {
 | 
					    span.contact-form-result {
 | 
				
			||||||
      font-size: 0.9em;
 | 
					      font-size: 0.9em;
 | 
				
			||||||
      font-weight: 600;
 | 
					      font-weight: 600;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,26 +5,6 @@
 | 
				
			|||||||
  <p>Fill out this quick form and I'll get back to you as soon as I can! You can also <a href="mailto:jake@jarv.is">email me directly</a>, send me a <a href="https://twitter.com/messages/compose?recipient_id=229769022" target="_blank" rel="noopener nofollow">direct message on Twitter</a>, or <a href="sms:+1-617-917-3737">text me</a>.</p>
 | 
					  <p>Fill out this quick form and I'll get back to you as soon as I can! You can also <a href="mailto:jake@jarv.is">email me directly</a>, send me a <a href="https://twitter.com/messages/compose?recipient_id=229769022" target="_blank" rel="noopener nofollow">direct message on Twitter</a>, or <a href="sms:+1-617-917-3737">text me</a>.</p>
 | 
				
			||||||
  <p>🔐 You can grab my public key here: <a href="/pubkey.asc" title="My Public PGP Key" target="_blank" rel="pgpkey authn noopener"><code>6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39</code></a>.</p>
 | 
					  <p>🔐 You can grab my public key here: <a href="/pubkey.asc" title="My Public PGP Key" target="_blank" rel="pgpkey authn noopener"><code>6BF3 79D3 6F67 1480 2B0C 9CF2 51E6 9A39</code></a>.</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <form id="contact-form" action="/api/contact/" method="POST">
 | 
					  <div id="contact-form-wrapper"></div>
 | 
				
			||||||
    <input type="text" name="name" placeholder="Name">
 | 
					 | 
				
			||||||
    <input type="email" name="email" placeholder="Email">
 | 
					 | 
				
			||||||
    <textarea name="message" placeholder="Write something..."></textarea>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <span id="contact-form-md-info">Basic <a href="https://commonmark.org/help/" title="Markdown reference sheet" target="_blank" rel="noopener">Markdown syntax</a> is allowed here, e.g.: <strong>**bold**</strong>, <em>_italics_</em>, [<a href="https://jarv.is" target="_blank" rel="noopener">links</a>](https://jarv.is), and <code>`code`</code>.</span>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <h-captcha
 | 
					 | 
				
			||||||
      id="contact-form-captcha"
 | 
					 | 
				
			||||||
      site-key="{{ getenv "HCAPTCHA_SITE_KEY" | default "10000000-ffff-ffff-ffff-000000000001" }}"
 | 
					 | 
				
			||||||
      size="normal"
 | 
					 | 
				
			||||||
      tabindex="0"></h-captcha>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div id="contact-form-action-row">
 | 
					 | 
				
			||||||
      <button title="Submit" aria-label="Submit" id="contact-form-btn-submit">📤 Send</button>
 | 
					 | 
				
			||||||
      <div id="contact-form-result">
 | 
					 | 
				
			||||||
        <span id="contact-form-result-error"></span>
 | 
					 | 
				
			||||||
        <span id="contact-form-result-success"></span>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </form>
 | 
					 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{{ end }}
 | 
					{{ end }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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({
 | 
					      new AssetsManifestPlugin({
 | 
				
			||||||
        writeToDisk: true, // allow Hugo to access file in dev mode
 | 
					        writeToDisk: true, // allow Hugo to access file in dev mode
 | 
				
			||||||
        output: path.resolve(__dirname, "data/manifest.json"),
 | 
					        output: path.resolve(__dirname, "data/manifest.json"),
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user