Files
computer-repair/src/app/components/ContactForm.js
T
asownerytandClaude Opus 5.5 2b2ac742dd
Docker / build (push) Successful in 1m16s
Add Mac and console repairs with a footer disclaimer
Mac (MacBook, iMac) and PlayStation/Xbox repairs are now offered;
phones and tablets are not. The Apple, Sony and Microsoft
non-affiliation disclaimer is shown once in the footer, with
footnote markers on the mentions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 16:09:49 +13:00

216 lines
6.6 KiB
JavaScript

"use client";
import { useState } from "react";
import { CircleAlert, CircleCheck } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
NativeSelect,
NativeSelectOption,
} from "@/components/ui/native-select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea";
import { business, links } from "@/lib/site";
import { trackEvent } from "@/lib/analytics";
const FORM_ACTION = "https://form.taxi/s/es9ddva1";
const devices = ["Laptop", "Desktop PC", "Mac", "Chromebook", "PlayStation or Xbox", "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"];
function Field({ children }) {
return <div className="grid gap-2">{children}</div>;
}
// 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 (
<Alert role="status" className="border-primary">
<CircleCheck aria-hidden="true" />
<AlertTitle>Thanks, I&apos;ve got it</AlertTitle>
<AlertDescription>
<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-semibold text-primary underline">
{business.phone}
</a>
.
</p>
</AlertDescription>
</Alert>
);
}
return (
<div>
<h2 className="text-3xl font-bold mb-2">Get a quote</h2>
<p className="mb-6 text-muted-foreground">
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={FORM_ACTION}
method="POST"
className="grid gap-5"
onSubmit={handleSubmit}
>
<Field>
<Label htmlFor="cf-name">Name*</Label>
<Input id="cf-name" name="Name" autoComplete="name" required />
</Field>
<div className="grid gap-5 sm:grid-cols-2">
<Field>
<Label htmlFor="cf-email">Email*</Label>
<Input
id="cf-email"
type="email"
name="Email"
autoComplete="email"
required
/>
</Field>
<Field>
<Label htmlFor="cf-phone">Phone number</Label>
<Input id="cf-phone" type="tel" name="Phone" autoComplete="tel" />
</Field>
</div>
<fieldset className="grid gap-3">
<legend className="mb-3 text-sm font-medium">
Best way to reach you
</legend>
<RadioGroup
name="Preferred contact"
defaultValue="Email"
className="flex gap-6"
>
{contactMethods.map((method) => (
<div key={method} className="flex items-center gap-2">
<RadioGroupItem value={method} id={`cf-contact-${method}`} />
<Label htmlFor={`cf-contact-${method}`} className="font-normal">
{method}
</Label>
</div>
))}
</RadioGroup>
</fieldset>
<div className="grid gap-5 sm:grid-cols-2">
<Field>
<Label htmlFor="cf-device">Device</Label>
<NativeSelect
id="cf-device"
name="Device"
defaultValue="Laptop"
wrapperClassName="w-full"
className="text-base md:text-sm"
>
{devices.map((device) => (
<NativeSelectOption key={device}>{device}</NativeSelectOption>
))}
</NativeSelect>
</Field>
<Field>
<Label htmlFor="cf-problem">Problem</Label>
<NativeSelect
id="cf-problem"
name="Problem"
defaultValue=""
wrapperClassName="w-full"
className="text-base md:text-sm"
>
<NativeSelectOption value="" disabled>
Choose one
</NativeSelectOption>
{problems.map((problem) => (
<NativeSelectOption key={problem}>{problem}</NativeSelectOption>
))}
</NativeSelect>
</Field>
</div>
<Field>
<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
/>
</Field>
{/* 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="text-sm text-muted-foreground">
* required.
</p>
{status === "error" && (
<Alert variant="destructive" role="alert">
<CircleAlert aria-hidden="true" />
<AlertTitle>That didn&apos;t send</AlertTitle>
<AlertDescription>
Please try again, or call or text {business.phone}.
</AlertDescription>
</Alert>
)}
<Button
type="submit"
size="lg"
disabled={status === "sending"}
className="justify-self-start"
>
{status === "sending" ? "Sending…" : "Send"}
</Button>
</form>
</div>
);
}