Some checks failed
Build And Deploy / build-and-deploy (push) Has been cancelled
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.
264 lines
8.2 KiB
Text
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>
|