mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-27 15:16:21 -04:00
preact-ify contact form
This commit is contained in:
parent
9a247ddb8d
commit
a338a05ce8
@ -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"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user