Phase 5: contact, conversion and tracking hooks
- Contact page: Call/Text/WhatsApp at the top, hours, drop-off address, service area, map, and the quote form - Quote form: device, problem, preferred contact; email required, phone optional; labels wired to inputs; _gotcha honeypot; AJAX submit to form.taxi with an inline "same day during business hours" message and an error fallback (still posts normally without JavaScript) - Analytics hooks for click_call, click_text, click_email, submit_quote_form and click_google_reviews; no-ops until Plausible is installed - /review redirects (307) to the Google review link - Maps embed moved to one MapEmbed component Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9f5cf387dc
commit
4d901b351e
9 files changed
+293
-68
No files matched your search
@@ -1,67 +1,160 @@
|
||||
import styles from "./ContactForm.module.css";
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import styles from "./ContactForm.module.css";
|
||||
import { business, links } from "@/lib/site";
|
||||
import { trackEvent } from "@/lib/analytics";
|
||||
|
||||
const FORM_ACTION = "https://form.taxi/s/es9ddva1";
|
||||
|
||||
const devices = ["Laptop", "Desktop PC", "Chromebook", "Other"];
|
||||
const problems = [
|
||||
"Won't turn on or won't start",
|
||||
"Cracked or broken screen",
|
||||
"Running slow / upgrade",
|
||||
"Virus, pop-ups or scam",
|
||||
"Battery or charging",
|
||||
"Moving files to a new computer",
|
||||
"Windows 10 / Windows 11",
|
||||
"Business IT support",
|
||||
"Something else",
|
||||
];
|
||||
const contactMethods = ["Call", "Text", "Email"];
|
||||
|
||||
// Submits to form.taxi with JavaScript for an inline success message; without
|
||||
// JavaScript the form still posts normally.
|
||||
export default function ContactForm() {
|
||||
const [status, setStatus] = useState("idle");
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
setStatus("sending");
|
||||
try {
|
||||
const response = await fetch(FORM_ACTION, {
|
||||
method: "POST",
|
||||
headers: { Accept: "application/json" },
|
||||
body: new FormData(event.currentTarget),
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !result.success) throw new Error("Submit failed");
|
||||
setStatus("sent");
|
||||
trackEvent("submit_quote_form");
|
||||
} catch {
|
||||
setStatus("error");
|
||||
}
|
||||
}
|
||||
|
||||
if (status === "sent") {
|
||||
return (
|
||||
<div role="status" className="rounded-lg border-2 border-sky-800 p-6">
|
||||
<h2 className="text-2xl font-bold mb-2">Thanks, I've got it</h2>
|
||||
<p>
|
||||
I'll usually reply the same day during business hours (Monday
|
||||
to Friday, 9am to 6pm). If it's urgent, call or text{" "}
|
||||
<a href={links.tel} className="font-bold text-sky-800 underline">
|
||||
{business.phone}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-4">Contact Us</h2>
|
||||
<div className={styles.contactDetails}>
|
||||
<h3 className="text-lg font-bold hover:underline">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M2 3.5A1.5 1.5 0 0 1 3.5 2h1.148a1.5 1.5 0 0 1 1.465 1.175l.716 3.223a1.5 1.5 0 0 1-1.052 1.767l-.933.267c-.41.117-.643.555-.48.95a11.542 11.542 0 0 0 6.254 6.254c.395.163.833-.07.95-.48l.267-.933a1.5 1.5 0 0 1 1.767-1.052l3.223.716A1.5 1.5 0 0 1 18 15.352V16.5a1.5 1.5 0 0 1-1.5 1.5H15c-1.149 0-2.263-.15-3.326-.43A13.022 13.022 0 0 1 2.43 8.326 13.019 13.019 0 0 1 2 5V3.5Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<a href="tel:+64211077483">021 107 7483</a>
|
||||
</h3>
|
||||
<h3 className="text-lg font-bold hover:underline">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.404 14.596A6.5 6.5 0 1 1 16.5 10a1.25 1.25 0 0 1-2.5 0 4 4 0 1 0-.571 2.06A2.75 2.75 0 0 0 18 10a8 8 0 1 0-2.343 5.657.75.75 0 0 0-1.06-1.06 6.5 6.5 0 0 1-9.193 0ZM10 7.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<a href="mailto:[email protected]">
|
||||
info@teatatucomputers.com
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold mb-2">Get a quote</h2>
|
||||
<p className="mb-6">
|
||||
Tell me what's going on and I'll get back to you, usually the
|
||||
same day during business hours.
|
||||
</p>
|
||||
<form
|
||||
action="https://form.taxi/s/es9ddva1"
|
||||
action={FORM_ACTION}
|
||||
method="POST"
|
||||
className={styles.form}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<label htmlFor="Name">Name*</label>
|
||||
<input type="text" name="Name" required />
|
||||
<label htmlFor="cf-name">Name*</label>
|
||||
<input id="cf-name" type="text" name="Name" autoComplete="name" required />
|
||||
|
||||
<label htmlFor="Phone">Phone Number </label>
|
||||
<input type="tel" name="Phone" />
|
||||
<label htmlFor="cf-email">Email*</label>
|
||||
<input
|
||||
id="cf-email"
|
||||
type="email"
|
||||
name="Email"
|
||||
autoComplete="email"
|
||||
required
|
||||
/>
|
||||
|
||||
<label htmlFor="Email">Email*</label>
|
||||
<input type="email" name="Email" required />
|
||||
<label htmlFor="cf-phone">Phone number</label>
|
||||
<input id="cf-phone" type="tel" name="Phone" autoComplete="tel" />
|
||||
|
||||
<label htmlFor="firstname">Message*</label>
|
||||
<textarea name="Message"></textarea>
|
||||
<fieldset className={styles.fieldset}>
|
||||
<legend>Best way to reach you</legend>
|
||||
<div className={styles.radios}>
|
||||
{contactMethods.map((method) => (
|
||||
<label key={method}>
|
||||
<input
|
||||
type="radio"
|
||||
name="Preferred contact"
|
||||
value={method}
|
||||
defaultChecked={method === "Email"}
|
||||
/>{" "}
|
||||
{method}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<p className={styles.sm}>* required</p>
|
||||
<label htmlFor="cf-device">Device</label>
|
||||
<select id="cf-device" name="Device" defaultValue="Laptop">
|
||||
{devices.map((device) => (
|
||||
<option key={device}>{device}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<label htmlFor="cf-problem">Problem</label>
|
||||
<select id="cf-problem" name="Problem" defaultValue="">
|
||||
<option value="" disabled>
|
||||
Choose one
|
||||
</option>
|
||||
{problems.map((problem) => (
|
||||
<option key={problem}>{problem}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<label htmlFor="cf-message">What's going on?*</label>
|
||||
<textarea
|
||||
id="cf-message"
|
||||
name="Message"
|
||||
rows={5}
|
||||
placeholder="e.g. HP laptop, screen cracked in the corner, still turns on"
|
||||
required
|
||||
></textarea>
|
||||
|
||||
{/* Honeypot: hidden from people, filled in by bots. Enable "_gotcha" in form.taxi's Spam Shield. */}
|
||||
<input
|
||||
type="text"
|
||||
name="_gotcha"
|
||||
className="hidden"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<p className={styles.sm}>* required. I don't repair Apple devices.</p>
|
||||
|
||||
{status === "error" && (
|
||||
<p role="alert" className="mb-4 font-bold text-red-700">
|
||||
Sorry, that didn't send. Please try again, or call or text{" "}
|
||||
{business.phone}.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="submit"
|
||||
value="Submit"
|
||||
required
|
||||
className="text-white bg-sky-800 hover:bg-sky-700 transition"
|
||||
value={status === "sending" ? "Sending…" : "Send"}
|
||||
disabled={status === "sending"}
|
||||
className="text-white bg-sky-800 hover:bg-sky-700 transition disabled:opacity-60"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user