mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-10-14 02:44:28 -04:00
next-mdx-remote v4 (#737)
This commit is contained in:
86
components/ContactForm/ContactForm.module.css
Normal file
86
components/ContactForm/ContactForm.module.css
Normal file
@@ -0,0 +1,86 @@
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 0.8em;
|
||||
margin: 0.6em 0;
|
||||
border: 2px solid;
|
||||
border-radius: 0.3em;
|
||||
color: var(--text);
|
||||
background-color: var(--super-duper-light);
|
||||
border-color: var(--light);
|
||||
}
|
||||
|
||||
textarea.input {
|
||||
height: 12em;
|
||||
min-height: 6em;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--link);
|
||||
}
|
||||
|
||||
.input.missing {
|
||||
border-color: var(--error);
|
||||
}
|
||||
|
||||
.markdown_tip {
|
||||
font-size: 0.825em;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.markdown_tip a:first-of-type {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hcaptcha {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.action_row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 3.75em;
|
||||
}
|
||||
|
||||
.btn_submit {
|
||||
flex-shrink: 0;
|
||||
height: 3.25em;
|
||||
padding: 1em 1.25em;
|
||||
margin-right: 1.5em;
|
||||
border: 0;
|
||||
border-radius: 0.3em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
background-color: var(--kinda-light);
|
||||
}
|
||||
|
||||
.btn_submit:hover {
|
||||
color: var(--super-duper-light);
|
||||
background-color: var(--link);
|
||||
}
|
||||
|
||||
.btn_submit .send_icon {
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
|
||||
.result_success,
|
||||
.result_error {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result_success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.result_error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
184
components/ContactForm/ContactForm.tsx
Normal file
184
components/ContactForm/ContactForm.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
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 isEmailLike from "is-email-like";
|
||||
import { SendIcon, CheckOcticon, XOcticon } from "../Icons";
|
||||
|
||||
import type { FormikHelpers } from "formik";
|
||||
|
||||
import styles from "./ContactForm.module.css";
|
||||
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 [submitted, setSubmitted] = useState(false);
|
||||
const [success, setSuccess] = useState(null);
|
||||
const [feedback, setFeedback] = useState("");
|
||||
|
||||
const handleSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
// 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/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(values),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.success === true) {
|
||||
// handle successful submission
|
||||
// disable submissions, hide the send button, and let user know we were successful
|
||||
setSuccess(true);
|
||||
setFeedback("Thanks! You should hear from me soon.");
|
||||
} else {
|
||||
// pass on any error sent by the server to the catch block below
|
||||
throw new Error(data.message);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setSuccess(false);
|
||||
|
||||
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...
|
||||
setFeedback("Internal server error... Try again later or shoot me an old-fashioned email?");
|
||||
}
|
||||
})
|
||||
.finally(() => setSubmitting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Formik
|
||||
onSubmit={handleSubmit}
|
||||
initialValues={{
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
"h-captcha-response": "",
|
||||
}}
|
||||
validate={(values: Values) => {
|
||||
const errors: { name?: boolean; email?: boolean; message?: boolean; "h-captcha-response"?: boolean } = {};
|
||||
|
||||
errors.name = !values.name;
|
||||
errors.email = !values.email || !isEmailLike(values.email); // also loosely validate email with regex (not foolproof)
|
||||
errors.message = !values.message;
|
||||
errors["h-captcha-response"] = !values["h-captcha-response"];
|
||||
|
||||
if (!errors.name && !errors.email && !errors.message && !errors["h-captcha-response"]) {
|
||||
setFeedback("");
|
||||
return null;
|
||||
} else {
|
||||
setSuccess(false);
|
||||
setFeedback("Please make sure that all fields are properly filled in.");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}}
|
||||
>
|
||||
{({ setFieldValue, isSubmitting, touched, errors }) => (
|
||||
<Form className={styles.form} name="contact">
|
||||
<Field
|
||||
className={cx({ input: true, missing: errors.name && touched.name })}
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
disabled={success}
|
||||
/>
|
||||
<Field
|
||||
className={cx({ input: true, missing: errors.email && touched.email })}
|
||||
name="email"
|
||||
type="email"
|
||||
inputmode="email"
|
||||
placeholder="Email"
|
||||
disabled={success}
|
||||
/>
|
||||
<Field
|
||||
className={cx({ input: true, missing: errors.message && touched.message })}
|
||||
name="message"
|
||||
component="textarea"
|
||||
placeholder="Write something..."
|
||||
disabled={success}
|
||||
/>
|
||||
|
||||
<div className={styles.markdown_tip}>
|
||||
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>.
|
||||
</div>
|
||||
|
||||
<div className={styles.hcaptcha}>
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY}
|
||||
size="normal"
|
||||
theme={resolvedTheme === "dark" ? "dark" : "light"}
|
||||
onVerify={(token) => setFieldValue("h-captcha-response", token)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.action_row}>
|
||||
<button
|
||||
className={cx({ btn_submit: true, hidden: success })}
|
||||
type="submit"
|
||||
title="Send Message"
|
||||
aria-label="Send Message"
|
||||
onClick={() => setSubmitted(true)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span>Sending...</span>
|
||||
) : (
|
||||
<>
|
||||
<SendIcon className={`icon ${styles.send_icon}`} /> <span>Send</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<span
|
||||
className={cx({
|
||||
result_success: success,
|
||||
result_error: !success,
|
||||
hidden: !submitted || !feedback || isSubmitting,
|
||||
})}
|
||||
>
|
||||
{success ? <CheckOcticon fill="CurrentColor" /> : <XOcticon fill="CurrentColor" />} {feedback}
|
||||
</span>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactForm;
|
Reference in New Issue
Block a user