Swap to shadcn components and polish the design

Components (added via the shadcn MCP/CLI):
- Navbar: NavigationMenu on desktop, Sheet on mobile; drops
  Navbar.module.css and the hand-rolled slide-down menu
- Buttons: shadcn Button via a shared ContactButtons component
  (replaces Button.js and its CSS module)
- Card for price cards, testimonials, the business block and plan;
  Table for prices; Input/Textarea/Label/NativeSelect/RadioGroup and
  Alert in the quote form; Separator in the footer
- Components import cn from @/lib/utils (not the new "cn" package);
  accordion moved to the unified radix-ui package

Design tweaks:
- Geist Sans is actually applied (font variables were on <body>, but
  Tailwind reads them on <html>, so the site fell back to system fonts)
- Theme primary set to the existing brand blue; slightly smaller radius
- Balanced, tighter heading tracking; muted secondary text
- 44px contact buttons; 16px FAQ text; tighter homepage spacing
- Full-width selects with 16px text on phones (no iOS zoom)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
asownerytandClaude Opus 5.5 committed 2026-09-30 21:25:07 +13:00
1 parent f6846ebaae
commit f916f058fc
36 files changed
+2888 -823

No files matched your search

+126 -73
View File
@@ -1,7 +1,17 @@
"use client";
import { useState } from "react";
import styles from "./ContactForm.module.css";
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";
@@ -21,6 +31,10 @@ const problems = [
];
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() {
@@ -46,90 +60,122 @@ export default function ContactForm() {
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>
<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">
<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={styles.form}
className="grid gap-5"
onSubmit={handleSubmit}
>
<label htmlFor="cf-name">Name*</label>
<input id="cf-name" type="text" name="Name" autoComplete="name" required />
<Field>
<Label htmlFor="cf-name">Name*</Label>
<Input id="cf-name" name="Name" autoComplete="name" required />
</Field>
<label htmlFor="cf-email">Email*</label>
<input
id="cf-email"
type="email"
name="Email"
autoComplete="email"
required
/>
<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>
<label htmlFor="cf-phone">Phone number</label>
<input id="cf-phone" type="tel" name="Phone" autoComplete="tel" />
<fieldset className={styles.fieldset}>
<legend>Best way to reach you</legend>
<div className={styles.radios}>
<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) => (
<label key={method}>
<input
type="radio"
name="Preferred contact"
value={method}
defaultChecked={method === "Email"}
/>{" "}
{method}
</label>
<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>
))}
</div>
</RadioGroup>
</fieldset>
<label htmlFor="cf-device">Device</label>
<select id="cf-device" name="Device" defaultValue="Laptop">
{devices.map((device) => (
<option key={device}>{device}</option>
))}
</select>
<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>
<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>
<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
@@ -141,21 +187,28 @@ export default function ContactForm() {
aria-hidden="true"
/>
<p className={styles.sm}>* required. I don&apos;t repair Apple devices.</p>
<p className="text-sm text-muted-foreground">
* 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>
<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>
)}
<input
<Button
type="submit"
value={status === "sending" ? "Sending…" : "Send"}
size="lg"
disabled={status === "sending"}
className="text-white bg-sky-800 hover:bg-sky-700 transition disabled:opacity-60"
/>
className="justify-self-start"
>
{status === "sending" ? "Sending…" : "Send"}
</Button>
</form>
</div>
);