mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-07-23 01:11:16 -04:00
contact form cleanup
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"color-hex-length": "long",
|
"color-hex-length": "long",
|
||||||
"max-nesting-depth": 6,
|
"max-nesting-depth": 6,
|
||||||
|
"no-descending-specificity": null,
|
||||||
"order/order": null,
|
"order/order": null,
|
||||||
"order/properties-alphabetical-order": null,
|
"order/properties-alphabetical-order": null,
|
||||||
"plugin/no-unsupported-browser-features": [true, { "severity": "warning", "ignore": ["flexbox"] }],
|
"plugin/no-unsupported-browser-features": [true, { "severity": "warning", "ignore": ["flexbox"] }],
|
||||||
|
@@ -2,16 +2,17 @@ import "vanilla-hcaptcha";
|
|||||||
import { h, render } from "preact";
|
import { h, render } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import fetch from "unfetch";
|
import fetch from "unfetch";
|
||||||
|
import parseEmoji from "./emoji.js";
|
||||||
|
|
||||||
const CONTACT_ENDPOINT = "/api/contact/";
|
const CONTACT_ENDPOINT = "/api/contact/";
|
||||||
|
|
||||||
const ContactForm = () => {
|
const ContactForm = () => {
|
||||||
// status/feedback:
|
// status/feedback:
|
||||||
const [status, setStatus] = useState({ success: false, action: "Submit", message: "" });
|
const [status, setStatus] = useState({ success: false, message: "" });
|
||||||
// keep track of fetch:
|
// keep track of fetch:
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
|
|
||||||
const onSubmit = async (e) => {
|
const onSubmit = (e) => {
|
||||||
// immediately prevent browser from actually navigating to a new page
|
// immediately prevent browser from actually navigating to a new page
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -19,19 +20,18 @@ const ContactForm = () => {
|
|||||||
setSending(true);
|
setSending(true);
|
||||||
|
|
||||||
// extract data from form fields
|
// extract data from form fields
|
||||||
const { name, email, message } = e.target.elements;
|
|
||||||
const formData = {
|
const formData = {
|
||||||
name: name.value,
|
name: e.target.elements.name?.value,
|
||||||
email: email.value,
|
email: e.target.elements.email?.value,
|
||||||
message: message.value,
|
message: e.target.elements.message?.value,
|
||||||
"h-captcha-response": e.target.elements["h-captcha-response"].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
|
// some client-side validation to save requests (these are also checked on the server to be safe)
|
||||||
// unnecessary requests here.
|
// TODO: change border color of the specific empty/missing field(s) to red
|
||||||
if (!(formData.name && formData.email && formData.message && formData["h-captcha-response"])) {
|
if (!(formData.name && formData.email && formData.message && formData["h-captcha-response"])) {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
setStatus({ success: false, action: "Try Again", message: "Please make sure that all fields are filled in." });
|
setStatus({ success: false, message: "❗ Please make sure that all fields are filled in." });
|
||||||
|
|
||||||
// remove focus from the submit button
|
// remove focus from the submit button
|
||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
@@ -55,7 +55,7 @@ const ContactForm = () => {
|
|||||||
if (data.success === true) {
|
if (data.success === true) {
|
||||||
// handle successful submission
|
// handle successful submission
|
||||||
// disable submissions, hide the send button, and let user know we were successful
|
// 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. :)" });
|
setStatus({ success: true, message: "Thanks! You should hear from me soon. 😊" });
|
||||||
} else {
|
} else {
|
||||||
// pass on any error sent by the server
|
// pass on any error sent by the server
|
||||||
throw new Error(data.message);
|
throw new Error(data.message);
|
||||||
@@ -70,18 +70,16 @@ const ContactForm = () => {
|
|||||||
if (message === "USER_INVALID_CAPTCHA") {
|
if (message === "USER_INVALID_CAPTCHA") {
|
||||||
setStatus({
|
setStatus({
|
||||||
success: false,
|
success: false,
|
||||||
action: "Try Again",
|
message: "❗ Did you complete the CAPTCHA? (If you're human, that is...)",
|
||||||
message: "Did you complete the CAPTCHA? (If you're human, that is...)",
|
|
||||||
});
|
});
|
||||||
} else if (message === "USER_MISSING_DATA") {
|
} else if (message === "USER_MISSING_DATA") {
|
||||||
setStatus({
|
setStatus({
|
||||||
success: false,
|
success: false,
|
||||||
action: "Try Again",
|
message: "❗ Please make sure that all fields are filled in.",
|
||||||
message: "Please make sure that all fields are filled in.",
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// something else went wrong, and it's probably my fault...
|
// something else went wrong, and it's probably my fault...
|
||||||
setStatus({ success: false, action: "Try Again", message: "Internal server error. Try again later?" });
|
setStatus({ success: false, message: "❗ Internal server error. Try again later?" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove focus from the submit button
|
// remove focus from the submit button
|
||||||
@@ -95,7 +93,7 @@ const ContactForm = () => {
|
|||||||
<input type="email" name="email" placeholder="Email" disabled={status.success} />
|
<input type="email" name="email" placeholder="Email" disabled={status.success} />
|
||||||
<textarea name="message" placeholder="Write something..." disabled={status.success} />
|
<textarea name="message" placeholder="Write something..." disabled={status.success} />
|
||||||
|
|
||||||
<span id="contact-form-md-info">
|
<div id="contact-form-md-info">
|
||||||
Basic{" "}
|
Basic{" "}
|
||||||
<a
|
<a
|
||||||
href="https://commonmark.org/help/"
|
href="https://commonmark.org/help/"
|
||||||
@@ -110,27 +108,29 @@ const ContactForm = () => {
|
|||||||
links
|
links
|
||||||
</a>
|
</a>
|
||||||
](https://jarv.is), and <code>`code`</code>.
|
](https://jarv.is), and <code>`code`</code>.
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
<h-captcha id="contact-form-captcha" site-key={process.env.HCAPTCHA_SITE_KEY} size="normal" tabindex="0" />
|
<div id="contact-form-captcha">
|
||||||
|
<h-captcha site-key={process.env.HCAPTCHA_SITE_KEY} size="normal" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="contact-form-action-row">
|
<div id="contact-form-action-row">
|
||||||
<button
|
<button
|
||||||
id="contact-form-btn-submit"
|
id="contact-form-btn-submit"
|
||||||
title={status.action}
|
title="Send Message"
|
||||||
aria-label={status.action}
|
aria-label="Send Message"
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
style={{ display: status.success ? "none" : null }}
|
style={{ display: status.success ? "none" : null }}
|
||||||
>
|
// eslint-disable-next-line react/no-danger
|
||||||
{sending ? "Sending..." : status.action}
|
dangerouslySetInnerHTML={{ __html: parseEmoji(sending ? "Sending..." : "📤 Send") }}
|
||||||
</button>
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="contact-form-result"
|
class="contact-form-result"
|
||||||
id={status.success ? "contact-form-result-success" : "contact-form-result-error"}
|
id={status.success ? "contact-form-result-success" : "contact-form-result-error"}
|
||||||
>
|
// eslint-disable-next-line react/no-danger
|
||||||
{status.message}
|
dangerouslySetInnerHTML={{ __html: parseEmoji(status.message) }}
|
||||||
</span>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@@ -38,12 +38,12 @@
|
|||||||
30% {
|
30% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
// stylelint-enable rule-empty-line-before
|
||||||
|
|
||||||
// pause for 3.5 out of 5 seconds
|
// pause for 3.5 out of 5 seconds
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
// stylelint-enable rule-empty-line-before
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes beat {
|
@keyframes beat {
|
||||||
@@ -63,12 +63,12 @@
|
|||||||
8% {
|
8% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
// stylelint-enable rule-empty-line-before
|
||||||
|
|
||||||
// pause for ~9 out of 10 seconds
|
// pause for ~9 out of 10 seconds
|
||||||
100% {
|
100% {
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
// stylelint-enable rule-empty-line-before
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// modified from https://tobiasahlin.com/spinkit/
|
// modified from https://tobiasahlin.com/spinkit/
|
||||||
|
@@ -50,9 +50,7 @@ div#content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AnchorJS styles
|
// AnchorJS styles
|
||||||
// stylelint-disable-next-line no-descending-specificity
|
|
||||||
a.anchorjs-link {
|
a.anchorjs-link {
|
||||||
display: inline-block;
|
|
||||||
margin-left: 0.25em;
|
margin-left: 0.25em;
|
||||||
padding: 0 0.5em 0 0.25em;
|
padding: 0 0.5em 0 0.25em;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -70,7 +68,6 @@ div#content {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// stylelint-disable-next-line no-descending-specificity
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@include themes.themed(
|
@include themes.themed(
|
||||||
(
|
(
|
||||||
|
@@ -127,7 +127,6 @@ div.highlight-clipboard-enabled {
|
|||||||
|
|
||||||
// Syntax Highlighting (light) - modified from Monokai Light: https://github.com/mlgill/pygments-style-monokailight
|
// Syntax Highlighting (light) - modified from Monokai Light: https://github.com/mlgill/pygments-style-monokailight
|
||||||
body.light {
|
body.light {
|
||||||
// stylelint-disable no-descending-specificity
|
|
||||||
div.highlight,
|
div.highlight,
|
||||||
button.copy-button,
|
button.copy-button,
|
||||||
:not(pre) > code {
|
:not(pre) > code {
|
||||||
@@ -139,7 +138,6 @@ body.light {
|
|||||||
button.copy-button {
|
button.copy-button {
|
||||||
color: #313131;
|
color: #313131;
|
||||||
}
|
}
|
||||||
// stylelint-enable no-descending-specificity
|
|
||||||
|
|
||||||
.chroma {
|
.chroma {
|
||||||
.k,
|
.k,
|
||||||
@@ -215,7 +213,6 @@ body.light {
|
|||||||
|
|
||||||
// Syntax Highlighting (dark) - modified from Dracula: https://github.com/dracula/pygments
|
// Syntax Highlighting (dark) - modified from Dracula: https://github.com/dracula/pygments
|
||||||
body.dark {
|
body.dark {
|
||||||
// stylelint-disable no-descending-specificity
|
|
||||||
div.highlight,
|
div.highlight,
|
||||||
button.copy-button,
|
button.copy-button,
|
||||||
:not(pre) > code {
|
:not(pre) > code {
|
||||||
@@ -227,7 +224,6 @@ body.dark {
|
|||||||
button.copy-button {
|
button.copy-button {
|
||||||
color: #e4e4e4;
|
color: #e4e4e4;
|
||||||
}
|
}
|
||||||
// stylelint-enable no-descending-specificity
|
|
||||||
|
|
||||||
.chroma {
|
.chroma {
|
||||||
.k,
|
.k,
|
||||||
|
@@ -10,11 +10,6 @@ div.layout-contact {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 0.9em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background: none !important;
|
background: none !important;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -30,7 +25,7 @@ div.layout-contact {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.8em;
|
padding: 0.8em;
|
||||||
margin: 0.6em 0;
|
margin: 0.6em 0;
|
||||||
border: 1px solid;
|
border: 2px solid;
|
||||||
border-radius: 0.3em;
|
border-radius: 0.3em;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
|
||||||
@@ -41,6 +36,16 @@ div.layout-contact {
|
|||||||
border-color: "light",
|
border-color: "light",
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none; // disable browsers' outer border
|
||||||
|
|
||||||
|
@include themes.themed(
|
||||||
|
(
|
||||||
|
border-color: "links",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
@@ -54,8 +59,10 @@ div.layout-contact {
|
|||||||
div#contact-form-action-row {
|
div#contact-form-action-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-height: 3.75em;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
flex-shrink: 0;
|
||||||
padding: 0.8em 1.2em;
|
padding: 0.8em 1.2em;
|
||||||
margin-right: 1.5em;
|
margin-right: 1.5em;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -71,8 +78,7 @@ div.layout-contact {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
&:hover,
|
&:hover {
|
||||||
&:focus {
|
|
||||||
@include themes.themed(
|
@include themes.themed(
|
||||||
(
|
(
|
||||||
color: "super-duper-light",
|
color: "super-duper-light",
|
||||||
@@ -82,13 +88,14 @@ div.layout-contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
img.emoji {
|
img.emoji {
|
||||||
|
margin-left: 0;
|
||||||
margin-right: 0.4em;
|
margin-right: 0.4em;
|
||||||
cursor: inherit;
|
cursor: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span.contact-form-result {
|
span.contact-form-result {
|
||||||
font-size: 0.9em;
|
font-size: 0.925em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
||||||
&#contact-form-result-success {
|
&#contact-form-result-success {
|
||||||
@@ -110,16 +117,14 @@ div.layout-contact {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hcaptcha widget
|
// hcaptcha widget
|
||||||
#contact-form-captcha {
|
div#contact-form-captcha {
|
||||||
display: block;
|
margin: 1em 0;
|
||||||
margin: 1.2em 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
span#contact-form-md-info {
|
div#contact-form-md-info {
|
||||||
display: block;
|
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
margin-top: 0.25em;
|
line-height: 1.75;
|
||||||
margin-left: 0.75em;
|
margin: 0.2em 0 0.6em 0;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
// disable fancy underline without `.no-underline`
|
// disable fancy underline without `.no-underline`
|
||||||
|
@@ -18,8 +18,7 @@ div.layout-list {
|
|||||||
ul {
|
ul {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 0;
|
padding: 0;
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
@@ -6,3 +6,11 @@ sitemap:
|
|||||||
changefreq: never
|
changefreq: never
|
||||||
priority: 0.0
|
priority: 0.0
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
|
||||||
|
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>.
|
||||||
|
|
||||||
|
🔐 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>.
|
||||||
|
|
||||||
|
<!-- markdownlint-enable -->
|
||||||
|
@@ -2,8 +2,9 @@
|
|||||||
<div class="layout layout-contact">
|
<div class="layout layout-contact">
|
||||||
<h1><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
|
<h1><a href="{{ .Permalink }}">{{ .Title | markdownify }}</a></h1>
|
||||||
|
|
||||||
<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>
|
<div id="content">
|
||||||
<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>
|
{{ .Content }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="contact-form-wrapper"></div>
|
<div id="contact-form-wrapper"></div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user