mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-07-30 13:15:24 -04:00
@@ -0,0 +1,96 @@
|
||||
.form {
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
select,
|
||||
textarea {
|
||||
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);
|
||||
|
||||
&:focus {
|
||||
outline: none; // disable browsers' outer border
|
||||
border-color: var(--link);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 12em;
|
||||
min-height: 6em;
|
||||
margin-bottom: 0;
|
||||
line-height: 1.5;
|
||||
|
||||
// allow vertical resizing & disable horizontal
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown_tip {
|
||||
font-size: 0.825em;
|
||||
line-height: 1.75;
|
||||
|
||||
a {
|
||||
background: none;
|
||||
padding-bottom: 0;
|
||||
|
||||
&:first-of-type {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.captcha {
|
||||
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;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--text);
|
||||
background-color: var(--kinda-light);
|
||||
|
||||
&:hover {
|
||||
color: var(--super-duper-light);
|
||||
background-color: var(--link);
|
||||
}
|
||||
|
||||
.send_icon {
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
vertical-align: -0.22em;
|
||||
border: 0;
|
||||
display: inline-block;
|
||||
margin-right: 0.4em;
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.result_success,
|
||||
.result_error {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.result_success {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.result_error {
|
||||
color: var(--error);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { useState } from "react";
|
||||
import HCaptcha from "@hcaptcha/react-hcaptcha";
|
||||
import { CheckIcon, XIcon } from "@primer/octicons-react";
|
||||
import { SendIcon } from "../icons";
|
||||
|
||||
import styles from "./ContactForm.module.scss";
|
||||
|
||||
export default function ContactForm() {
|
||||
// status/feedback:
|
||||
const [status, setStatus] = useState({ success: false, message: "" });
|
||||
// keep track of fetch:
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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(formData),
|
||||
})
|
||||
.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." });
|
||||
} else {
|
||||
// pass on any error sent by the server
|
||||
throw new Error(data.message);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const message = error instanceof Error ? error.message : "UNKNOWN_EXCEPTION";
|
||||
|
||||
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.",
|
||||
});
|
||||
} else {
|
||||
// something else went wrong, and it's probably my fault...
|
||||
setStatus({ success: false, message: "Internal server error. Try again later?" });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form className={styles.form} onSubmit={onSubmit} action="/api/contact/" method="POST">
|
||||
<input type="text" name="name" placeholder="Name" required disabled={status.success} />
|
||||
<input type="email" name="email" placeholder="Email" required disabled={status.success} />
|
||||
<textarea name="message" placeholder="Write something..." required disabled={status.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.captcha}>
|
||||
<HCaptcha
|
||||
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY}
|
||||
size="normal"
|
||||
onVerify={() => true} // this is allegedly optional but a function undefined error is thrown without it
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.action_row}>
|
||||
<button
|
||||
className={styles.btn_submit}
|
||||
title="Send Message"
|
||||
aria-label="Send Message"
|
||||
disabled={sending}
|
||||
style={{ display: status.success ? "none" : null }}
|
||||
>
|
||||
{sending ? (
|
||||
<span>Sending...</span>
|
||||
) : (
|
||||
<>
|
||||
<SendIcon className={styles.send_icon} /> <span>Send</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<span
|
||||
className={status.success ? styles.result_success : styles.result_error}
|
||||
style={{ display: !status.message || sending ? "none" : null }}
|
||||
>
|
||||
{status.success ? <CheckIcon size={16} /> : <XIcon size={16} />} {status.message}
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user