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:
asownerytandClaude Opus 5.5 committed 2026-09-30 20:48:56 +13:00
1 parent 9f5cf387dc
commit 4d901b351e
9 files changed
+293 -68

No files matched your search

+142 -49
View File
@@ -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&apos;ve got it</h2>
<p>
I&apos;ll usually reply the same day during business hours (Monday
to Friday, 9am to 6pm). If it&apos;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&apos;s going on and I&apos;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&apos;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&apos;t repair Apple devices.</p>
{status === "error" && (
<p role="alert" className="mb-4 font-bold text-red-700">
Sorry, that didn&apos;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>