Docker / build (push) Successful in 1m16s
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]>
216 lines
6.6 KiB
JavaScript
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've got it</AlertTitle>
|
|
<AlertDescription>
|
|
<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-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's going on and I'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'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'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>
|
|
);
|
|
}
|