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:
1 parent
f6846ebaae
commit
f916f058fc
36 files changed
+2888
-823
No files matched your search
@@ -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've got it</h2>
|
||||
<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-bold text-sky-800 underline">
|
||||
{business.phone}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<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">
|
||||
<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={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'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'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't repair Apple devices.</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
* required. I don't repair Apple devices.
|
||||
</p>
|
||||
|
||||
{status === "error" && (
|
||||
<p role="alert" className="mb-4 font-bold text-red-700">
|
||||
Sorry, that didn't send. Please try again, or call or text{" "}
|
||||
{business.phone}.
|
||||
</p>
|
||||
<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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user