From 66a244a9c11089906d0e00c95b9c6e76178d4963 Mon Sep 17 00:00:00 2001 From: Jake Jarvis Date: Thu, 6 Jan 2022 13:13:41 -0500 Subject: [PATCH] formik-powered contact page (#725) --- components/contact/ContactForm.module.scss | 16 +- components/contact/ContactForm.tsx | 238 ++++++++++++--------- package.json | 3 + yarn.lock | 61 +++++- 4 files changed, 210 insertions(+), 108 deletions(-) diff --git a/components/contact/ContactForm.module.scss b/components/contact/ContactForm.module.scss index 6b448a53..c020a2c5 100644 --- a/components/contact/ContactForm.module.scss +++ b/components/contact/ContactForm.module.scss @@ -16,6 +16,10 @@ outline: none; // disable browsers' outer border border-color: var(--link); } + + &.missing { + border-color: var(--error); + } } textarea { @@ -43,7 +47,7 @@ } } -.captcha { +.hcaptcha { margin: 1em 0; } @@ -71,13 +75,7 @@ } .send_icon { - height: 1.2em; - width: 1.2em; - vertical-align: -0.22em; - border: 0; - display: inline-block; margin-right: 0.4em; - cursor: inherit; } } @@ -93,3 +91,7 @@ .result_error { color: var(--error); } + +.hidden { + display: none !important; +} diff --git a/components/contact/ContactForm.tsx b/components/contact/ContactForm.tsx index f3fb3e9e..b6782aa2 100644 --- a/components/contact/ContactForm.tsx +++ b/components/contact/ContactForm.tsx @@ -1,40 +1,35 @@ import { useState } from "react"; import { useTheme } from "next-themes"; +import classNames from "classnames/bind"; +import { Formik, Form, Field } from "formik"; import HCaptcha from "@hcaptcha/react-hcaptcha"; -import { CheckOcticon, XOcticon } from "../icons/octicons"; +import isEmailLike from "is-email-like"; import { SendIcon } from "../icons"; +import { CheckOcticon, XOcticon } from "../icons/octicons"; + +import type { FormikHelpers } from "formik"; import styles from "./ContactForm.module.scss"; +const cx = classNames.bind(styles); + +type Values = { + name: string; + email: string; + message: string; + "h-captcha-response": string; +}; const ContactForm = () => { const { resolvedTheme } = useTheme(); + // status/feedback: - const [status, setStatus] = useState({ success: false, message: "" }); - // keep track of fetch: - const [sending, setSending] = useState(false); + const [submitted, setSubmitted] = useState(false); + const [success, setSuccess] = useState(null); + const [feedback, setFeedback] = useState(""); - const onSubmit = (e) => { - // immediately prevent browser from actually navigating to a new page - e.preventDefault(); - - // begin the process - setSending(true); - - // extract data from form fields - const formData = { - name: e.target.elements.name?.value, - email: e.target.elements.email?.value, - message: e.target.elements.message?.value, - "h-captcha-response": e.target.elements["h-captcha-response"]?.value, - }; - - // some client-side validation to save requests (these are also checked on the server to be safe) - if (!(formData.name && formData.email && formData.message && formData["h-captcha-response"])) { - setSending(false); - setStatus({ success: false, message: "Please make sure that all fields are filled in." }); - - return; - } + const handleSubmit = (values: Values, { setSubmitting }: FormikHelpers) => { + // once a user attempts a submission, this is true and stays true whether or not the next attempt(s) are successful + setSubmitted(true); // if we've gotten here then all data is (or should be) valid and ready to post to API fetch("/api/contact/", { @@ -43,101 +38,146 @@ const ContactForm = () => { "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify(formData), + body: JSON.stringify(values), }) .then((response) => response.json()) .then((data) => { - setSending(false); - if (data.success === true) { // handle successful submission // disable submissions, hide the send button, and let user know we were successful - setStatus({ success: true, message: "Thanks! You should hear from me soon." }); + setSuccess(true); + setFeedback("Thanks! You should hear from me soon."); } else { - // pass on any error sent by the server + // pass on any error sent by the server to the catch block below throw new Error(data.message); } }) .catch((error) => { - const message = error instanceof Error ? error.message : "UNKNOWN_EXCEPTION"; + setSuccess(false); - setSending(false); - - // give user feedback based on the error message returned - if (message === "USER_INVALID_CAPTCHA") { - setStatus({ - success: false, - message: "Did you complete the CAPTCHA? (If you're human, that is...)", - }); - } else if (message === "USER_MISSING_DATA") { - setStatus({ - success: false, - message: "Please make sure that all fields are filled in.", - }); + if (error.message === "USER_MISSING_DATA") { + // this should be validated client-side but it's also checked server-side just in case someone slipped past + setFeedback("Please make sure that all fields are properly filled in."); + } else if (error.message === "USER_INVALID_CAPTCHA") { + // missing/invalid captcha + setFeedback("Did you complete the CAPTCHA? (If you're human, that is...)"); } else { // something else went wrong, and it's probably my fault... - setStatus({ success: false, message: "Internal server error. Try again later?" }); + setFeedback("Internal server error... Try again later or shoot me an old-fashioned email?"); } - }); + }) + .finally(() => setSubmitting(false)); }; return ( -
- - -