personal-site/src/pages/contact.astro
badbl0cks da4925753d
Some checks failed
Build And Deploy / build-and-deploy (push) Has been cancelled
Rename content slot to main, remove h1 from header for better
accessibility, and unify SVG fill color to #fff

Update BaseLayout to use a header <p> for the website title, rename the
main slot/id to
"main", and update all pages to match. Add CSS rule svg { fill:
currentColor }
and size header > p the same as h1. Fix a minor comment typo.
2026-03-22 13:56:24 -07:00

264 lines
8.2 KiB
Text

---
import Layout from "@layouts/BaseLayout.astro";
import { actions, isInputError } from "astro:actions";
export const prerender = false;
const result = Astro.getActionResult(actions.contact.submitForm);
const error = isInputError(result?.error)
? result.error.fields
: result?.data?.error
? { [result?.data?.field]: [result?.data?.error] }
: {};
const nextAction = result?.data?.nextAction ?? "send_otp";
const formDraft = await Astro.session?.get("contactFormDraft");
if (formDraft && Object.keys(formDraft).length) {
Astro.session?.delete("contactFormDraft");
}
const pickValue = (key: string) =>
typeof formDraft?.[key] === "string" ? formDraft[key] : undefined;
const nameValue = pickValue("name");
const phoneValue = pickValue("phone");
const msgValue = pickValue("msg");
---
<Layout>
<title slot="head">Contact</title>
<Fragment slot="head">
<script>
import Cap from "@cap.js/widget";
import "iconify-icon";
const captchaInput = document.querySelector("input[id='captcha']");
const captchaStatus = document.querySelector("#captchaStatus");
const statusText = captchaStatus?.querySelector("#statusText");
const initIcon = captchaStatus?.querySelector("#initIcon");
const completeIcon = captchaStatus?.querySelector("#completeIcon");
const errorIcon = captchaStatus?.querySelector("#errorIcon");
const progressIcon = captchaStatus?.querySelector("#progressIcon");
const cap = new Cap({
apiEndpoint: "/cap/",
});
if (
captchaStatus &&
statusText &&
initIcon &&
completeIcon &&
errorIcon &&
progressIcon
) {
cap.addEventListener("solve", function (e) {
const humanness = Math.round((85 + Math.random() * 14.9) * 10) / 10;
statusText.textContent = `${humanness}% human. Good enough!`;
progressIcon.classList.add("hidden");
errorIcon.classList.add("hidden");
initIcon.classList.add("hidden");
completeIcon.classList.remove("hidden");
});
cap.addEventListener("error", function (e) {
statusText.textContent = "Oops! We crashed!";
progressIcon.classList.add("hidden");
completeIcon.classList.add("hidden");
initIcon.classList.add("hidden");
errorIcon.classList.remove("hidden");
});
cap.addEventListener("progress", (event) => {
statusText.textContent = `Weighing your humanity... ${event.detail.progress}%`;
errorIcon.classList.add("hidden");
completeIcon.classList.add("hidden");
initIcon.classList.add("hidden");
progressIcon.classList.remove("hidden");
});
}
if (captchaInput && "value" in captchaInput) {
const { token } = await cap.solve();
captchaInput.value = token;
}
</script>
<style>
form {
display: grid;
row-gap: var(--stack-large);
column-gap: var(--gutter-large);
width: 100%;
margin-inline: auto;
grid-template-columns: repeat(2, 1fr);
grid-template-areas:
"header header"
"name phone"
"msg msg"
"captcha otp"
"lbtn rbtn";
}
#header {
grid-area: header;
}
label[for="name"] {
grid-area: name;
}
label[for="phone"] {
grid-area: phone;
}
label[for="msg"] {
grid-area: msg;
}
label[for="otp"] {
grid-area: otp;
}
label[for="captcha"] {
grid-area: captcha;
}
#captchaStatus {
padding-block: calc(2px + (var(--stack-small) * 1));
cursor: text;
}
#statusText {
margin-inline-start: 0.5rem;
}
button#reset {
grid-area: lbtn;
}
button#send_otp {
grid-area: lbtn;
}
button#send_msg {
grid-area: rbtn;
}
fieldset {
display: contents;
}
.spin {
animation: spin 4s linear infinite;
position: relative;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</Fragment>
<Fragment slot="main">
<h2>Contact</h2>
{
(nextAction != "complete" && (
<form method="post" x-data="{}" action={actions.contact.submitForm}>
<div id="header">
{(result?.error && (
<p class="error">
{result?.error.message}
Please correct any errors and try again.
</p>
)) || <p>Use this form to shoot me a quick text!</p>}
</div>
<fieldset id="send_otp_fields" disabled={nextAction != "send_otp"}>
<label for="name">
Name
<input
type="text"
id="name"
name="name"
aria-describedby="name"
placeholder="Alice Bob"
/>
{error.name && <p id="error_name">{error.name.join(",")}</p>}
</label>
<label for="phone">
Phone
<input
type="text"
id="phone"
name="phone"
aria-describedby="error_phone"
placeholder="555-555-5555"
/>
{error.phone && <p id="error_phone">{error.phone.join(",")}</p>}
</label>
<label for="msg">
Msg
<div class="textarea-wrapper">
<textarea
id="msg"
name="msg"
oninput="this.parentNode.dataset.replicatedValue = this.value"
aria-describedby="error_msg"
placeholder="I think badblocks rocks! Lorem ipsum dolor sit amet, consectetur adipiscing elit."
/>
</div>
{error.msg && <p id="error_msg">{error.msg.join(",")}</p>}
</label>
<button
id="send_otp"
name="action"
value="send_otp"
type="submit"
disabled={nextAction != "send_otp"}
class={nextAction != "send_otp" ? "hidden" : ""}
>
Verify Me!
</button>
</fieldset>
<fieldset id="send_msg_fields" disabled={nextAction != "send_msg"}>
<label for="otp">
Verification Code
<input
type="text"
id="otp"
name="otp"
aria-describedby="error_otp"
/>
{error.otp && <p id="error_otp">{error.otp.join(",")}</p>}
</label>
<button
id="reset"
name="action"
value="reset"
type="submit"
class={nextAction != "send_msg" ? "hidden" : ""}
>
Go Back!
</button>
<button id="send_msg" name="action" value="send_msg" type="submit">
Send ittt!
</button>
</fieldset>
<label for="captcha">
{/* prettier-ignore */}
<a href="https://capjs.js.org/" tabindex="-1">Captcha</a>
<input type="hidden" id="captcha" name="captcha" />
<div id="captchaStatus">
<div class="iconBox">
<iconify-icon id="initIcon" icon="line-md:loading-loop" />
<iconify-icon
id="completeIcon"
icon="line-md:circle-to-confirm-circle-transition"
class="hidden"
/>
<iconify-icon
id="errorIcon"
icon="line-md:alert-circle-loop"
class="hidden"
/>
<iconify-icon
id="progressIcon"
icon="line-md:speedometer-loop"
class="hidden"
/>
</div>
<span id="statusText">Loading...</span>
</div>
{error.captcha && <p id="error_name">{error.captcha.join(",")}</p>}
</label>
</form>
)) || <p>Your message has been sent successfully!</p>
}
</Fragment>
</Layout>