From 4d901b351ec8175edec0f982f7fa07a397fd7c55 Mon Sep 17 00:00:00 2001 From: asowneryt Date: Wed, 30 Sep 2026 20:48:56 +1300 Subject: [PATCH] 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 --- src/app/components/AnalyticsEvents.js | 18 ++ src/app/components/ContactForm.js | 191 ++++++++++++++++------ src/app/components/ContactForm.module.css | 16 +- src/app/components/MapEmbed.js | 17 ++ src/app/contact/page.js | 83 +++++++++- src/app/layout.js | 2 + src/app/page.js | 10 +- src/app/review/route.js | 6 + src/lib/analytics.js | 18 ++ 9 files changed, 293 insertions(+), 68 deletions(-) create mode 100644 src/app/components/AnalyticsEvents.js create mode 100644 src/app/components/MapEmbed.js create mode 100644 src/app/review/route.js create mode 100644 src/lib/analytics.js diff --git a/src/app/components/AnalyticsEvents.js b/src/app/components/AnalyticsEvents.js new file mode 100644 index 0000000..cb9f4f9 --- /dev/null +++ b/src/app/components/AnalyticsEvents.js @@ -0,0 +1,18 @@ +"use client"; + +import { useEffect } from "react"; +import { eventForHref, trackEvent } from "@/lib/analytics"; + +// One site-wide listener, so every call/text/email/review link is tracked. +export default function AnalyticsEvents() { + useEffect(() => { + function handleClick(event) { + const link = event.target.closest?.("a[href]"); + const name = link && eventForHref(link.getAttribute("href")); + if (name) trackEvent(name); + } + document.addEventListener("click", handleClick); + return () => document.removeEventListener("click", handleClick); + }, []); + return null; +} diff --git a/src/app/components/ContactForm.js b/src/app/components/ContactForm.js index 31988e7..470a4a0 100644 --- a/src/app/components/ContactForm.js +++ b/src/app/components/ContactForm.js @@ -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 ( +
+

Thanks, I've got it

+

+ I'll usually reply the same day during business hours (Monday + to Friday, 9am to 6pm). If it's urgent, call or text{" "} + + {business.phone} + + . +

+
+ ); + } + return (
-

Contact Us

-
-

- - - - 021 107 7483 -

-

- - - - - info@teatatucomputers.com - -

-
+

Get a quote

+

+ Tell me what's going on and I'll get back to you, usually the + same day during business hours. +

- - + + - - + + - - + + - - +
+ Best way to reach you +
+ {contactMethods.map((method) => ( + + ))} +
+
-

* required

+ + + + + + + + + + {/* Honeypot: hidden from people, filled in by bots. Enable "_gotcha" in form.taxi's Spam Shield. */} + + +

* required. I don't repair Apple devices.

+ + {status === "error" && ( +

+ Sorry, that didn't send. Please try again, or call or text{" "} + {business.phone}. +

+ )}
diff --git a/src/app/components/ContactForm.module.css b/src/app/components/ContactForm.module.css index 404b7e8..7ef2501 100644 --- a/src/app/components/ContactForm.module.css +++ b/src/app/components/ContactForm.module.css @@ -26,16 +26,18 @@ font-style: italic; } -.contactDetails { - margin-bottom: 2em; +.fieldset { + margin-bottom: 16px; } -.contactDetails h3 { +.radios { display: flex; - gap: 0.5em; - align-items: center; + gap: 1.5em; + margin-top: 6px; } -.contactDetails h3 a { - color: black; +.radios label { + display: flex; + align-items: center; + gap: 0.35em; } diff --git a/src/app/components/MapEmbed.js b/src/app/components/MapEmbed.js new file mode 100644 index 0000000..d8a8fdc --- /dev/null +++ b/src/app/components/MapEmbed.js @@ -0,0 +1,17 @@ +import { GoogleMapsEmbed } from "@next/third-parties/google"; + +// This key is public by design; restrict it by HTTP referrer in Google Cloud Console. +const MAPS_API_KEY = "AIzaSyDbgQY55TQY5CefsCUwKbc8RfoOUe8U5gY"; +const PLACE_ID = "ChIJz0ZoDYhADW0R0TDgWoWMq0M"; + +export default function MapEmbed({ height = "400px" }) { + return ( + + ); +} diff --git a/src/app/contact/page.js b/src/app/contact/page.js index 56afe5f..d51a872 100644 --- a/src/app/contact/page.js +++ b/src/app/contact/page.js @@ -1,6 +1,10 @@ +import Link from "next/link"; +import { Clock, Mail, MapPin, MessageCircle, MessageSquare, Phone } from "lucide-react"; import HeaderSm from "../components/HeaderSm"; +import Button from "../components/Button"; import ContactForm from "../components/ContactForm"; -import { pageMetadata } from "@/lib/site"; +import MapEmbed from "../components/MapEmbed"; +import { business, links, pageMetadata } from "@/lib/site"; export const metadata = pageMetadata({ title: "Contact & Book a Repair | Te Atatu Computers", @@ -9,11 +13,82 @@ export const metadata = pageMetadata({ path: "/contact", }); -export default function Home() { +export default function ContactPage() { + const { address } = business; return (
- -
+ + +
+

+ The quickest way to reach me is a call or text. I'm{" "} + {business.ownerName}, and I'll be the one fixing your computer. +

+
+ + + +
+
+ +
+
+
    +
  • +
  • +
  • +
  • +
  • +
  • +
+

+ Onsite visits and pickup across{" "} + {business.serviceArea.join(", ")}. The travel fee depends on + distance. +

+

+ See{" "} + + prices + {" "} + and{" "} + + common questions + + . +

+
+ +
+ +
diff --git a/src/app/layout.js b/src/app/layout.js index 74d7013..75c4066 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -5,6 +5,7 @@ import Footer from "./components/Footer"; import ChatwootWidget from "./components/ChatwootWidget"; import JsonLd from "./components/JsonLd"; import MobileCallBar from "./components/MobileCallBar"; +import AnalyticsEvents from "./components/AnalyticsEvents"; import { SITE_URL, business, localBusinessSchema } from "@/lib/site"; const geistSans = localFont({ @@ -36,6 +37,7 @@ export default function RootLayout({ children }) { {children}