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
Generated
+1478
-1
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -9,11 +9,11 @@
|
|||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-accordion": "^1.2.20",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.49.0",
|
"lucide-react": "^1.49.0",
|
||||||
"next": "^16.3.7",
|
"next": "^16.3.7",
|
||||||
|
"radix-ui": "^1.6.7",
|
||||||
"react": "^19.3.0",
|
"react": "^19.3.0",
|
||||||
"react-dom": "^19.3.0",
|
"react-dom": "^19.3.0",
|
||||||
"sharp": "^0.35.5",
|
"sharp": "^0.35.5",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function AboutPage() {
|
|||||||
{areaSlugs[name] ? (
|
{areaSlugs[name] ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/areas/${areaSlugs[name]}`}
|
href={`/areas/${areaSlugs[name]}`}
|
||||||
className="text-sky-800 hover:underline"
|
className="text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ArrowRight, MessageSquare, Phone } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import HeaderSm from "../../components/HeaderSm";
|
import HeaderSm from "../../components/HeaderSm";
|
||||||
import Button from "../../components/Button";
|
import ContactButtons from "../../components/ContactButtons";
|
||||||
import CtaBand from "../../components/CtaBand";
|
import CtaBand from "../../components/CtaBand";
|
||||||
import FaqList from "../../components/FaqList";
|
import FaqList from "../../components/FaqList";
|
||||||
import JsonLd from "../../components/JsonLd";
|
import JsonLd from "../../components/JsonLd";
|
||||||
import { areas, getArea } from "@/lib/areas";
|
import { areas, getArea } from "@/lib/areas";
|
||||||
import { services } from "@/lib/services";
|
import { services } from "@/lib/services";
|
||||||
import { business, links, pageMetadata, serviceSchema } from "@/lib/site";
|
import { business, pageMetadata, serviceSchema } from "@/lib/site";
|
||||||
|
|
||||||
export const dynamicParams = false;
|
export const dynamicParams = false;
|
||||||
|
|
||||||
@@ -52,16 +52,7 @@ export default async function AreaPage({ params }) {
|
|||||||
|
|
||||||
<section className="container mx-auto max-w-3xl mb-16">
|
<section className="container mx-auto max-w-3xl mb-16">
|
||||||
<p className="text-lg mb-6">{area.intro}</p>
|
<p className="text-lg mb-6">{area.intro}</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<ContactButtons />
|
||||||
<Button href={links.tel} variant="primary">
|
|
||||||
<Phone aria-hidden="true" />
|
|
||||||
Call {business.phone}
|
|
||||||
</Button>
|
|
||||||
<Button href={links.sms} variant="outlineDark">
|
|
||||||
<MessageSquare aria-hidden="true" />
|
|
||||||
Text me
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="container mx-auto max-w-3xl mb-16">
|
<section className="container mx-auto max-w-3xl mb-16">
|
||||||
@@ -85,7 +76,7 @@ export default async function AreaPage({ params }) {
|
|||||||
<li key={slug}>
|
<li key={slug}>
|
||||||
<Link
|
<Link
|
||||||
href={`/services/${slug}`}
|
href={`/services/${slug}`}
|
||||||
className="inline-flex items-center gap-1 font-bold text-sky-800 hover:underline"
|
className="inline-flex items-center gap-1 font-bold text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{service.name}
|
{service.name}
|
||||||
<ArrowRight aria-hidden="true" className="size-4" />
|
<ArrowRight aria-hidden="true" className="size-4" />
|
||||||
@@ -95,7 +86,7 @@ export default async function AreaPage({ params }) {
|
|||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
See all prices on the{" "}
|
See all prices on the{" "}
|
||||||
<Link href="/pricing" className="text-sky-800 underline">
|
<Link href="/pricing" className="text-primary underline">
|
||||||
pricing page
|
pricing page
|
||||||
</Link>
|
</Link>
|
||||||
. I don't repair Apple devices.
|
. I don't repair Apple devices.
|
||||||
@@ -118,7 +109,7 @@ export default async function AreaPage({ params }) {
|
|||||||
<li key={slug}>
|
<li key={slug}>
|
||||||
<Link
|
<Link
|
||||||
href={`/areas/${slug}`}
|
href={`/areas/${slug}`}
|
||||||
className="text-sky-800 hover:underline"
|
className="text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
+43
-38
@@ -1,14 +1,20 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Check, MessageSquare, Phone } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import HeaderSm from "../components/HeaderSm";
|
import HeaderSm from "../components/HeaderSm";
|
||||||
import Button from "../components/Button";
|
import ContactButtons from "../components/ContactButtons";
|
||||||
import CtaBand from "../components/CtaBand";
|
import CtaBand from "../components/CtaBand";
|
||||||
import FaqList from "../components/FaqList";
|
import FaqList from "../components/FaqList";
|
||||||
import JsonLd from "../components/JsonLd";
|
import JsonLd from "../components/JsonLd";
|
||||||
import {
|
import {
|
||||||
business,
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
businessPlan,
|
businessPlan,
|
||||||
links,
|
|
||||||
pageMetadata,
|
pageMetadata,
|
||||||
serviceSchema,
|
serviceSchema,
|
||||||
} from "@/lib/site";
|
} from "@/lib/site";
|
||||||
@@ -77,40 +83,39 @@ export default function BusinessPage() {
|
|||||||
I support small businesses around Te Atatū and West Auckland, onsite
|
I support small businesses around Te Atatū and West Auckland, onsite
|
||||||
or remotely. You deal with me directly, every time.
|
or remotely. You deal with me directly, every time.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<ContactButtons />
|
||||||
<Button href={links.tel} variant="primary">
|
|
||||||
<Phone aria-hidden="true" />
|
|
||||||
Call {business.phone}
|
|
||||||
</Button>
|
|
||||||
<Button href={links.sms} variant="outlineDark">
|
|
||||||
<MessageSquare aria-hidden="true" />
|
|
||||||
Text me
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="container mx-auto max-w-3xl mb-16">
|
<section className="container mx-auto max-w-3xl mb-16">
|
||||||
<div className="rounded-lg border-2 border-sky-800 p-6 md:p-8">
|
<Card className="border-primary shadow-none">
|
||||||
<h2 className="text-2xl font-bold mb-1">Business IT plan</h2>
|
<CardHeader>
|
||||||
<p className="mb-4">
|
<CardTitle>
|
||||||
<span className="text-4xl font-bold text-sky-800">
|
<h2 className="text-2xl font-bold">Business IT plan</h2>
|
||||||
{businessPlan.price}
|
</CardTitle>
|
||||||
</span>{" "}
|
<CardDescription>
|
||||||
a month
|
<span className="text-4xl font-bold text-primary">
|
||||||
</p>
|
{businessPlan.price}
|
||||||
<ul className="space-y-2 mb-4">
|
</span>{" "}
|
||||||
{businessPlan.includes.map((item) => (
|
a month
|
||||||
<li key={item} className="flex gap-2">
|
</CardDescription>
|
||||||
<Check
|
</CardHeader>
|
||||||
aria-hidden="true"
|
<CardContent>
|
||||||
className="size-5 shrink-0 mt-0.5 text-sky-800"
|
<ul className="space-y-2">
|
||||||
/>
|
{businessPlan.includes.map((item) => (
|
||||||
{item}
|
<li key={item} className="flex gap-2">
|
||||||
</li>
|
<Check
|
||||||
))}
|
aria-hidden="true"
|
||||||
</ul>
|
className="size-5 shrink-0 mt-0.5 text-primary"
|
||||||
<p className="text-slate-600">{businessPlan.terms}</p>
|
/>
|
||||||
</div>
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<p className="text-sm text-muted-foreground">{businessPlan.terms}</p>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
<p className="mt-4">
|
<p className="mt-4">
|
||||||
Prefer to pay as you go? Support is $35 an hour, onsite or remote.
|
Prefer to pay as you go? Support is $35 an hour, onsite or remote.
|
||||||
</p>
|
</p>
|
||||||
@@ -123,7 +128,7 @@ export default function BusinessPage() {
|
|||||||
<li key={item} className="flex gap-2">
|
<li key={item} className="flex gap-2">
|
||||||
<Check
|
<Check
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="size-5 shrink-0 mt-0.5 text-sky-800"
|
className="size-5 shrink-0 mt-0.5 text-primary"
|
||||||
/>
|
/>
|
||||||
{item}
|
{item}
|
||||||
</li>
|
</li>
|
||||||
@@ -133,14 +138,14 @@ export default function BusinessPage() {
|
|||||||
Hardware repairs and upgrades are covered too. Parts are extra. See{" "}
|
Hardware repairs and upgrades are covered too. Parts are extra. See{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/services/it-support"
|
href="/services/it-support"
|
||||||
className="text-sky-800 underline"
|
className="text-primary underline"
|
||||||
>
|
>
|
||||||
IT support
|
IT support
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
and{" "}
|
and{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/services/computer-repair"
|
href="/services/computer-repair"
|
||||||
className="text-sky-800 underline"
|
className="text-primary underline"
|
||||||
>
|
>
|
||||||
computer repairs
|
computer repairs
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import styles from "./Button.module.css";
|
|
||||||
|
|
||||||
// Renders a link when given href (internal paths use next/link), otherwise a button.
|
|
||||||
export default function Button({ children, onClick, href, variant }) {
|
|
||||||
const className = `${styles.button} ${variant ? styles[variant] : ""}`;
|
|
||||||
if (href?.startsWith("/")) {
|
|
||||||
return <Link href={href} className={className}>{children}</Link>;
|
|
||||||
}
|
|
||||||
if (href) {
|
|
||||||
return <a href={href} className={className}>{children}</a>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button className={className} onClick={onClick}>{children}</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
.button {
|
|
||||||
background-color: white;
|
|
||||||
border: none;
|
|
||||||
color: black;
|
|
||||||
padding: 1em 2em;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5em;
|
|
||||||
font-size: 1em;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button svg {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.outline {
|
|
||||||
background-color: transparent;
|
|
||||||
color: white;
|
|
||||||
box-shadow: inset 0 0 0 2px white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary {
|
|
||||||
background-color: var(--color-sky-800);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.outlineDark {
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--color-sky-800);
|
|
||||||
box-shadow: inset 0 0 0 2px var(--color-sky-800);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { MessageCircle, MessageSquare, Phone } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { business, links } from "@/lib/site";
|
||||||
|
|
||||||
|
// Call / Text (and optionally WhatsApp and Get a quote) buttons.
|
||||||
|
// Use onDark over photos or the blue band, where the default styles lack contrast.
|
||||||
|
export default function ContactButtons({
|
||||||
|
onDark = false,
|
||||||
|
textLabel = "Text me",
|
||||||
|
whatsapp = false,
|
||||||
|
quoteHref,
|
||||||
|
className,
|
||||||
|
}) {
|
||||||
|
// Larger than shadcn's lg size: these are the main tap targets on phones.
|
||||||
|
const base = "h-11 px-6 text-base";
|
||||||
|
const secondary = onDark
|
||||||
|
? "border-white bg-transparent text-white hover:bg-white/10 hover:text-white"
|
||||||
|
: "border-primary text-primary hover:text-primary";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col gap-3 sm:flex-row sm:flex-wrap", className)}>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
size="lg"
|
||||||
|
variant={onDark ? "secondary" : "default"}
|
||||||
|
className={base}
|
||||||
|
>
|
||||||
|
<a href={links.tel}>
|
||||||
|
<Phone aria-hidden="true" />
|
||||||
|
Call {business.phone}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild size="lg" variant="outline" className={cn(base, secondary)}>
|
||||||
|
<a href={links.sms}>
|
||||||
|
<MessageSquare aria-hidden="true" />
|
||||||
|
{textLabel}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
{whatsapp && (
|
||||||
|
<Button asChild size="lg" variant="outline" className={cn(base, secondary)}>
|
||||||
|
<a href={links.whatsapp}>
|
||||||
|
<MessageCircle aria-hidden="true" />
|
||||||
|
WhatsApp
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{quoteHref && (
|
||||||
|
<Button asChild size="lg" variant="outline" className={cn(base, secondary)}>
|
||||||
|
<Link href={quoteHref}>Get a quote</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
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 { business, links } from "@/lib/site";
|
||||||
import { trackEvent } from "@/lib/analytics";
|
import { trackEvent } from "@/lib/analytics";
|
||||||
|
|
||||||
@@ -21,6 +31,10 @@ const problems = [
|
|||||||
];
|
];
|
||||||
const contactMethods = ["Call", "Text", "Email"];
|
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
|
// Submits to form.taxi with JavaScript for an inline success message; without
|
||||||
// JavaScript the form still posts normally.
|
// JavaScript the form still posts normally.
|
||||||
export default function ContactForm() {
|
export default function ContactForm() {
|
||||||
@@ -46,90 +60,122 @@ export default function ContactForm() {
|
|||||||
|
|
||||||
if (status === "sent") {
|
if (status === "sent") {
|
||||||
return (
|
return (
|
||||||
<div role="status" className="rounded-lg border-2 border-sky-800 p-6">
|
<Alert role="status" className="border-primary">
|
||||||
<h2 className="text-2xl font-bold mb-2">Thanks, I've got it</h2>
|
<CircleCheck aria-hidden="true" />
|
||||||
<p>
|
<AlertTitle>Thanks, I've got it</AlertTitle>
|
||||||
I'll usually reply the same day during business hours (Monday
|
<AlertDescription>
|
||||||
to Friday, 9am to 6pm). If it's urgent, call or text{" "}
|
<p>
|
||||||
<a href={links.tel} className="font-bold text-sky-800 underline">
|
I'll usually reply the same day during business hours (Monday
|
||||||
{business.phone}
|
to Friday, 9am to 6pm). If it's urgent, call or text{" "}
|
||||||
</a>
|
<a href={links.tel} className="font-semibold text-primary underline">
|
||||||
.
|
{business.phone}
|
||||||
</p>
|
</a>
|
||||||
</div>
|
.
|
||||||
|
</p>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-bold mb-2">Get a quote</h2>
|
<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
|
Tell me what's going on and I'll get back to you, usually the
|
||||||
same day during business hours.
|
same day during business hours.
|
||||||
</p>
|
</p>
|
||||||
<form
|
<form
|
||||||
action={FORM_ACTION}
|
action={FORM_ACTION}
|
||||||
method="POST"
|
method="POST"
|
||||||
className={styles.form}
|
className="grid gap-5"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
<label htmlFor="cf-name">Name*</label>
|
<Field>
|
||||||
<input id="cf-name" type="text" name="Name" autoComplete="name" required />
|
<Label htmlFor="cf-name">Name*</Label>
|
||||||
|
<Input id="cf-name" name="Name" autoComplete="name" required />
|
||||||
|
</Field>
|
||||||
|
|
||||||
<label htmlFor="cf-email">Email*</label>
|
<div className="grid gap-5 sm:grid-cols-2">
|
||||||
<input
|
<Field>
|
||||||
id="cf-email"
|
<Label htmlFor="cf-email">Email*</Label>
|
||||||
type="email"
|
<Input
|
||||||
name="Email"
|
id="cf-email"
|
||||||
autoComplete="email"
|
type="email"
|
||||||
required
|
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>
|
<fieldset className="grid gap-3">
|
||||||
<input id="cf-phone" type="tel" name="Phone" autoComplete="tel" />
|
<legend className="mb-3 text-sm font-medium">
|
||||||
|
Best way to reach you
|
||||||
<fieldset className={styles.fieldset}>
|
</legend>
|
||||||
<legend>Best way to reach you</legend>
|
<RadioGroup
|
||||||
<div className={styles.radios}>
|
name="Preferred contact"
|
||||||
|
defaultValue="Email"
|
||||||
|
className="flex gap-6"
|
||||||
|
>
|
||||||
{contactMethods.map((method) => (
|
{contactMethods.map((method) => (
|
||||||
<label key={method}>
|
<div key={method} className="flex items-center gap-2">
|
||||||
<input
|
<RadioGroupItem value={method} id={`cf-contact-${method}`} />
|
||||||
type="radio"
|
<Label htmlFor={`cf-contact-${method}`} className="font-normal">
|
||||||
name="Preferred contact"
|
{method}
|
||||||
value={method}
|
</Label>
|
||||||
defaultChecked={method === "Email"}
|
</div>
|
||||||
/>{" "}
|
|
||||||
{method}
|
|
||||||
</label>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</RadioGroup>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<label htmlFor="cf-device">Device</label>
|
<div className="grid gap-5 sm:grid-cols-2">
|
||||||
<select id="cf-device" name="Device" defaultValue="Laptop">
|
<Field>
|
||||||
{devices.map((device) => (
|
<Label htmlFor="cf-device">Device</Label>
|
||||||
<option key={device}>{device}</option>
|
<NativeSelect
|
||||||
))}
|
id="cf-device"
|
||||||
</select>
|
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>
|
<Field>
|
||||||
<select id="cf-problem" name="Problem" defaultValue="">
|
<Label htmlFor="cf-message">What's going on?*</Label>
|
||||||
<option value="" disabled>
|
<Textarea
|
||||||
Choose one
|
id="cf-message"
|
||||||
</option>
|
name="Message"
|
||||||
{problems.map((problem) => (
|
rows={5}
|
||||||
<option key={problem}>{problem}</option>
|
placeholder="e.g. HP laptop, screen cracked in the corner, still turns on"
|
||||||
))}
|
required
|
||||||
</select>
|
/>
|
||||||
|
</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
|
|
||||||
></textarea>
|
|
||||||
|
|
||||||
{/* Honeypot: hidden from people, filled in by bots. Enable "_gotcha" in form.taxi's Spam Shield. */}
|
{/* Honeypot: hidden from people, filled in by bots. Enable "_gotcha" in form.taxi's Spam Shield. */}
|
||||||
<input
|
<input
|
||||||
@@ -141,21 +187,28 @@ export default function ContactForm() {
|
|||||||
aria-hidden="true"
|
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" && (
|
{status === "error" && (
|
||||||
<p role="alert" className="mb-4 font-bold text-red-700">
|
<Alert variant="destructive" role="alert">
|
||||||
Sorry, that didn't send. Please try again, or call or text{" "}
|
<CircleAlert aria-hidden="true" />
|
||||||
{business.phone}.
|
<AlertTitle>That didn't send</AlertTitle>
|
||||||
</p>
|
<AlertDescription>
|
||||||
|
Please try again, or call or text {business.phone}.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
value={status === "sending" ? "Sending…" : "Send"}
|
size="lg"
|
||||||
disabled={status === "sending"}
|
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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
.form input[type="text"],
|
|
||||||
.form input[type="email"],
|
|
||||||
.form input[type="tel"],
|
|
||||||
.form select,
|
|
||||||
.form textarea {
|
|
||||||
width: 100%; /* Full width */
|
|
||||||
padding: 1em;
|
|
||||||
border: 1px solid #ccc; /* Border */
|
|
||||||
border-radius: 0.5em; /* Rounded corners */
|
|
||||||
box-sizing: border-box; /* Make sure that padding and width stays in place */
|
|
||||||
margin-top: 6px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
resize: vertical; /* Allow the user to vertically resize the textarea (not horizontally) */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Style the submit button */
|
|
||||||
.form input[type="submit"] {
|
|
||||||
padding: 1em 1.75em;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm {
|
|
||||||
font-size: 0.75em;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fieldset {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radios {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5em;
|
|
||||||
margin-top: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radios label {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.35em;
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,22 @@
|
|||||||
import { MessageCircle, MessageSquare, Phone } from "lucide-react";
|
import ContactButtons from "./ContactButtons";
|
||||||
import Button from "./Button";
|
|
||||||
import { business, links } from "@/lib/site";
|
|
||||||
|
|
||||||
export default function CtaBand({ quoteHref = "/contact" }) {
|
export default function CtaBand({ quoteHref = "/contact" }) {
|
||||||
return (
|
return (
|
||||||
<section className="bg-sky-900 text-white py-16">
|
<section className="bg-sky-900 text-white py-16">
|
||||||
<div className="container mx-auto text-center">
|
<div className="container mx-auto text-center">
|
||||||
<h2 className="text-3xl font-bold mb-4">Get your computer sorted</h2>
|
<h2 className="text-3xl font-bold tracking-tight mb-3">
|
||||||
<p className="mb-8">
|
Get your computer sorted
|
||||||
|
</h2>
|
||||||
|
<p className="mb-8 text-sky-100">
|
||||||
Call or text now, or send the details and I'll get back to you
|
Call or text now, or send the details and I'll get back to you
|
||||||
with a quote.
|
with a quote.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row flex-wrap justify-center gap-3">
|
<ContactButtons
|
||||||
<Button href={links.tel}>
|
onDark
|
||||||
<Phone aria-hidden="true" />
|
whatsapp
|
||||||
Call {business.phone}
|
quoteHref={quoteHref}
|
||||||
</Button>
|
className="justify-center"
|
||||||
<Button href={links.sms} variant="outline">
|
/>
|
||||||
<MessageSquare aria-hidden="true" />
|
|
||||||
Text me
|
|
||||||
</Button>
|
|
||||||
<Button href={links.whatsapp} variant="outline">
|
|
||||||
<MessageCircle aria-hidden="true" />
|
|
||||||
WhatsApp
|
|
||||||
</Button>
|
|
||||||
<Button href={quoteHref} variant="outline">
|
|
||||||
Get a quote
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Mail, MapPin, Phone } from "lucide-react";
|
import { Mail, MapPin, Phone } from "lucide-react";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { business, links } from "@/lib/site";
|
import { business, links } from "@/lib/site";
|
||||||
import { services } from "@/lib/services";
|
import { services } from "@/lib/services";
|
||||||
import { areas } from "@/lib/areas";
|
import { areas } from "@/lib/areas";
|
||||||
@@ -86,9 +87,12 @@ export default function Footer() {
|
|||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<p className="container mx-auto mt-8 text-sm text-slate-300">
|
<div className="container mx-auto mt-10">
|
||||||
Copyright © {business.name}, {new Date().getFullYear()}
|
<Separator className="mb-6 bg-slate-700" />
|
||||||
</p>
|
<p className="text-sm text-slate-400">
|
||||||
|
Copyright © {business.name}, {new Date().getFullYear()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -7,14 +7,14 @@ export default function MobileCallBar() {
|
|||||||
<div className="fixed inset-x-0 bottom-0 z-50 grid grid-cols-2 border-t bg-white md:hidden">
|
<div className="fixed inset-x-0 bottom-0 z-50 grid grid-cols-2 border-t bg-white md:hidden">
|
||||||
<a
|
<a
|
||||||
href={links.tel}
|
href={links.tel}
|
||||||
className="flex items-center justify-center gap-2 bg-sky-800 py-3 font-bold text-white"
|
className="flex items-center justify-center gap-2 bg-primary py-3 font-semibold text-primary-foreground"
|
||||||
>
|
>
|
||||||
<Phone aria-hidden="true" className="size-4" />
|
<Phone aria-hidden="true" className="size-4" />
|
||||||
Call
|
Call
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={links.sms}
|
href={links.sms}
|
||||||
className="flex items-center justify-center gap-2 py-3 font-bold text-sky-800"
|
className="flex items-center justify-center gap-2 py-3 font-semibold text-primary"
|
||||||
>
|
>
|
||||||
<MessageSquare aria-hidden="true" className="size-4" />
|
<MessageSquare aria-hidden="true" className="size-4" />
|
||||||
Text
|
Text
|
||||||
|
|||||||
+147
-197
@@ -1,213 +1,163 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import styles from "./Navbar.module.css";
|
|
||||||
import logo from "../assets/logo.svg";
|
|
||||||
import menu from "../assets/menu.svg";
|
|
||||||
import close from "../assets/close.svg";
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { ChevronDownIcon } from "lucide-react";
|
import { MenuIcon, Phone } from "lucide-react";
|
||||||
|
import logo from "../assets/logo.svg";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
} from "@/components/ui/navigation-menu";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { business } from "@/lib/site";
|
||||||
|
import { services } from "@/lib/services";
|
||||||
|
|
||||||
|
const serviceLinks = [
|
||||||
|
...Object.entries(services).map(([slug, { name }]) => ({
|
||||||
|
href: `/services/${slug}`,
|
||||||
|
label: name,
|
||||||
|
})),
|
||||||
|
{ href: "/business", label: "Business IT plans" },
|
||||||
|
{ href: "/windows-10", label: "Windows 10 help" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const pageLinks = [
|
||||||
|
{ href: "/pricing", label: "Pricing" },
|
||||||
|
{ href: "/about", label: "About" },
|
||||||
|
{ href: "/#faq", label: "FAQ" },
|
||||||
|
];
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const [isActive, setActive] = useState("false");
|
const isActive = (href) => pathname === href;
|
||||||
const [isDropdownActive, setDropdownActive] = useState(false);
|
const inServices = serviceLinks.some(({ href }) => isActive(href));
|
||||||
|
|
||||||
function handleToggle() {
|
|
||||||
setActive(!isActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDropdownToggle() {
|
|
||||||
setDropdownActive(!isDropdownActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapperRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
/**
|
|
||||||
* Close the menu if clicked on outside of element
|
|
||||||
*/
|
|
||||||
function handleClickOutside(event) {
|
|
||||||
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
|
|
||||||
setActive(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Bind the event listener
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
// Unbind the event listener on clean up
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={`${styles.navbar} shadow-sm`}>
|
<header className="sticky top-0 z-50 border-b bg-white/90 backdrop-blur">
|
||||||
<div className={`${styles.container} container mx-auto`} ref={wrapperRef}>
|
<div className="container mx-auto flex h-16 items-center justify-between gap-4">
|
||||||
<div className={styles.header}>
|
<Link href="/" className="flex items-center gap-3">
|
||||||
<div>
|
<Image src={logo} width={32} height={32} alt="" />
|
||||||
<Link href="/" className={styles.logo}>
|
<span className="font-mono">{business.name}</span>
|
||||||
<Image src={logo} width="32" height="32" alt="" />
|
</Link>
|
||||||
<p>Te Atatu Computers</p>
|
|
||||||
</Link>
|
{/* Desktop */}
|
||||||
</div>
|
<NavigationMenu viewport={false} className="hidden lg:flex">
|
||||||
<button
|
<NavigationMenuList>
|
||||||
className={styles.menuIcon}
|
<NavigationMenuItem>
|
||||||
aria-label="Menu"
|
<NavigationMenuLink
|
||||||
onClick={handleToggle}
|
asChild
|
||||||
>
|
active={isActive("/")}
|
||||||
<Image
|
className={navigationMenuTriggerStyle()}
|
||||||
src={isActive ? menu : close}
|
>
|
||||||
width="32"
|
<Link href="/">Home</Link>
|
||||||
height="32"
|
</NavigationMenuLink>
|
||||||
alt=""
|
</NavigationMenuItem>
|
||||||
/>
|
<NavigationMenuItem>
|
||||||
</button>
|
<NavigationMenuTrigger
|
||||||
</div>
|
className={cn(inServices && "text-primary")}
|
||||||
<ul className={`${styles.links} ${isActive ? "" : styles.active}`}>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href="/"
|
|
||||||
className={`link ${pathname === "/" ? styles.active : ""}`}
|
|
||||||
>
|
|
||||||
Home
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
className={`
|
|
||||||
${styles.dropdown}
|
|
||||||
${isDropdownActive && !isActive ? styles.dropdownActive : ""}
|
|
||||||
`}
|
|
||||||
onMouseEnter={() => setDropdownActive(true)}
|
|
||||||
onMouseLeave={() => setDropdownActive(false)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
data-state={isDropdownActive ? "open" : "close"}
|
|
||||||
className="flex items-center justify-center gap-1 [&[data-state=open]>svg]:rotate-180"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-expanded={isDropdownActive}
|
|
||||||
className={`${styles.dropdownTitle}
|
|
||||||
${pathname.startsWith("/services") ? styles.active : ""}`}
|
|
||||||
onClick={handleDropdownToggle}
|
|
||||||
>
|
>
|
||||||
Services
|
Services
|
||||||
</button>
|
</NavigationMenuTrigger>
|
||||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
<NavigationMenuContent>
|
||||||
</div>
|
<ul className="grid w-64 gap-1">
|
||||||
<ul className={styles.dropdownMenu}>
|
{serviceLinks.map(({ href, label }) => (
|
||||||
<li>
|
<li key={href}>
|
||||||
<Link
|
<NavigationMenuLink asChild active={isActive(href)}>
|
||||||
href="/services/computer-repair"
|
<Link href={href}>{label}</Link>
|
||||||
className={`link ${
|
</NavigationMenuLink>
|
||||||
pathname === "/services/computer-repair"
|
</li>
|
||||||
? styles.active
|
))}
|
||||||
: ""
|
</ul>
|
||||||
}`}
|
</NavigationMenuContent>
|
||||||
|
</NavigationMenuItem>
|
||||||
|
{pageLinks.map(({ href, label }) => (
|
||||||
|
<NavigationMenuItem key={href}>
|
||||||
|
<NavigationMenuLink
|
||||||
|
asChild
|
||||||
|
active={isActive(href)}
|
||||||
|
className={navigationMenuTriggerStyle()}
|
||||||
>
|
>
|
||||||
Computer & Laptop Repairs
|
<Link href={href}>{label}</Link>
|
||||||
</Link>
|
</NavigationMenuLink>
|
||||||
</li>
|
</NavigationMenuItem>
|
||||||
<li>
|
))}
|
||||||
<Link
|
</NavigationMenuList>
|
||||||
href="/services/computer-upgrades"
|
</NavigationMenu>
|
||||||
className={`link ${
|
<Button asChild className="hidden lg:inline-flex">
|
||||||
pathname === "/services/computer-upgrades"
|
<Link href="/contact">
|
||||||
? styles.active
|
<Phone aria-hidden="true" />
|
||||||
: ""
|
Contact
|
||||||
}`}
|
</Link>
|
||||||
>
|
</Button>
|
||||||
Computer Upgrades & Speedup
|
|
||||||
</Link>
|
{/* Mobile */}
|
||||||
</li>
|
<Sheet>
|
||||||
<li>
|
<SheetTrigger asChild>
|
||||||
<Link
|
<Button variant="ghost" size="icon" className="lg:hidden">
|
||||||
href="/services/it-support"
|
<MenuIcon className="size-6" />
|
||||||
className={`link ${
|
<span className="sr-only">Menu</span>
|
||||||
pathname === "/services/it-support" ? styles.active : ""
|
</Button>
|
||||||
}`}
|
</SheetTrigger>
|
||||||
>
|
<SheetContent side="right" className="w-full max-w-xs overflow-y-auto">
|
||||||
IT Support
|
<SheetHeader>
|
||||||
</Link>
|
<SheetTitle>Menu</SheetTitle>
|
||||||
</li>
|
</SheetHeader>
|
||||||
<li>
|
<nav className="grid gap-6 px-4 pb-8">
|
||||||
<Link
|
<MobileList links={[{ href: "/", label: "Home" }]} isActive={isActive} />
|
||||||
href="/services/virus-removal"
|
<div>
|
||||||
className={`link ${
|
<p className="mb-2 text-sm text-muted-foreground">Services</p>
|
||||||
pathname === "/services/virus-removal" ? styles.active : ""
|
<MobileList links={serviceLinks} isActive={isActive} />
|
||||||
}`}
|
</div>
|
||||||
>
|
<MobileList links={pageLinks} isActive={isActive} />
|
||||||
Virus Removal
|
<SheetClose asChild>
|
||||||
</Link>
|
<Button asChild size="lg">
|
||||||
</li>
|
<Link href="/contact">
|
||||||
<li>
|
<Phone aria-hidden="true" />
|
||||||
<Link
|
Contact
|
||||||
href="/business"
|
</Link>
|
||||||
className={`link ${
|
</Button>
|
||||||
pathname === "/business" ? styles.active : ""
|
</SheetClose>
|
||||||
}`}
|
</nav>
|
||||||
>
|
</SheetContent>
|
||||||
Business IT Plans
|
</Sheet>
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href="/windows-10"
|
|
||||||
className={`link ${
|
|
||||||
pathname === "/windows-10" ? styles.active : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Windows 10 Help
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href="/pricing"
|
|
||||||
className={`link ${pathname === "/pricing" ? styles.active : ""}`}
|
|
||||||
>
|
|
||||||
Pricing
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href="/about"
|
|
||||||
className={`link ${pathname === "/about" ? styles.active : ""}`}
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href="/#faq"
|
|
||||||
>
|
|
||||||
FAQ
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li className="bg-sky-900 text-white lg:ml-4">
|
|
||||||
<Link
|
|
||||||
href="/contact"
|
|
||||||
className={`link ${pathname === "/contact" ? styles.active : ""}`}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M2 3.5A1.5 1.5 0 0 1 3.5 2h1.148a1.5 1.5 0 0 1 1.465 1.175l.716 3.223a1.5 1.5 0 0 1-1.052 1.767l-.933.267c-.41.117-.643.555-.48.95a11.542 11.542 0 0 0 6.254 6.254c.395.163.833-.07.95-.48l.267-.933a1.5 1.5 0 0 1 1.767-1.052l3.223.716A1.5 1.5 0 0 1 18 15.352V16.5a1.5 1.5 0 0 1-1.5 1.5H15c-1.149 0-2.263-.15-3.326-.43A13.022 13.022 0 0 1 2.43 8.326 13.019 13.019 0 0 1 2 5V3.5Z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Contact Us
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MobileList({ links, isActive }) {
|
||||||
|
return (
|
||||||
|
<ul className="grid gap-1">
|
||||||
|
{links.map(({ href, label }) => (
|
||||||
|
<li key={href}>
|
||||||
|
<SheetClose asChild>
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={cn(
|
||||||
|
"block rounded-md px-2 py-2 hover:bg-accent",
|
||||||
|
isActive(href) && "font-semibold text-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
</SheetClose>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
.navbar {
|
|
||||||
/* Make navbar stay at top of screen (static) */
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
color: black;
|
|
||||||
background-color: rgba(255, 255, 255, 0.9);
|
|
||||||
backdrop-filter: blur(2rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset default styling on lists */
|
|
||||||
.navbar .container ul {
|
|
||||||
list-style-type: none;
|
|
||||||
margin-left: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Position nav items correctly */
|
|
||||||
.navbar .container li {
|
|
||||||
padding-left: 0.75em;
|
|
||||||
padding-right: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reset default styling on links */
|
|
||||||
.navbar .container li a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Underline links on hover */
|
|
||||||
.navbar .container li a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make active link bold */
|
|
||||||
.navbar .container li a.active {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .container li a svg {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .container li a {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight {
|
|
||||||
color: white;
|
|
||||||
background-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.menuIcon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add styles for mobile view */
|
|
||||||
@media (max-width: 899px) {
|
|
||||||
.navbar {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
/* Hidden by sliding up its own height, so it works for any number of links. */
|
|
||||||
position: absolute;
|
|
||||||
top: 4rem;
|
|
||||||
transform: translateY(-100%);
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: -2;
|
|
||||||
|
|
||||||
font-size: 1rem;
|
|
||||||
background-color: white;
|
|
||||||
padding-top: 0.5em;
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
|
|
||||||
transition-property: transform, border-radius;
|
|
||||||
transition-duration: 0.4s;
|
|
||||||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.links {
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links li,
|
|
||||||
.dropdownTitle {
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
border-radius: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links.active {
|
|
||||||
transform: translateY(0);
|
|
||||||
border-radius: 0 0 0.5em 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownMenu {
|
|
||||||
max-height: 0.5em;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.5s cubic-bezier(0.215, 0.61, 0.355, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownActive .dropdownMenu {
|
|
||||||
max-height: 30em;
|
|
||||||
transition: max-height 0.5s cubic-bezier(0.215, 0.61, 0.355, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Styles for desktop view */
|
|
||||||
@media (min-width: 900px) {
|
|
||||||
.links {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.links li {
|
|
||||||
padding-top: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menuIcon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar .container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownMenu {
|
|
||||||
position: absolute;
|
|
||||||
display: none;
|
|
||||||
background-color: white;
|
|
||||||
margin-top: 1rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown:hover .dropdownMenu {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: flex;
|
|
||||||
gap: 1em;
|
|
||||||
flex-flow: row nowrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo p {
|
|
||||||
color: black;
|
|
||||||
margin-bottom: 0;
|
|
||||||
font-family: var(--font-geist-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown a {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown li {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownMenu li:last-of-type {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown:not(.dropdownActive) {
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown:not(.dropdownActive) li {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownActive {
|
|
||||||
background-color: #f1f1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown:not(.dropdownActive) .dropdownTitle {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownTitle {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownTitle.active {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
|
||||||
|
|
||||||
|
export default function PriceTable({ items, className }) {
|
||||||
|
return (
|
||||||
|
<Table className={className}>
|
||||||
|
<TableBody>
|
||||||
|
{items.map(({ item, price }) => (
|
||||||
|
<TableRow key={item}>
|
||||||
|
<TableCell className="py-3 pl-0 whitespace-normal">{item}</TableCell>
|
||||||
|
<TableCell className="py-3 pr-0 text-right font-semibold whitespace-normal text-primary">
|
||||||
|
{price}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowRight, Check, MessageSquare, Phone } from "lucide-react";
|
import { ArrowRight, Check } from "lucide-react";
|
||||||
import HeaderSm from "./HeaderSm";
|
import HeaderSm from "./HeaderSm";
|
||||||
import Button from "./Button";
|
import ContactButtons from "./ContactButtons";
|
||||||
import CtaBand from "./CtaBand";
|
import CtaBand from "./CtaBand";
|
||||||
import FaqList from "./FaqList";
|
import FaqList from "./FaqList";
|
||||||
import JsonLd from "./JsonLd";
|
import JsonLd from "./JsonLd";
|
||||||
import { business, links, pageMetadata, serviceSchema } from "@/lib/site";
|
import PriceTable from "./PriceTable";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { business, pageMetadata, serviceSchema } from "@/lib/site";
|
||||||
import { services } from "@/lib/services";
|
import { services } from "@/lib/services";
|
||||||
|
|
||||||
const promises = [
|
const promises = [
|
||||||
@@ -56,16 +58,7 @@ export default function ServicePage({ slug }) {
|
|||||||
{p}
|
{p}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
<div className="flex flex-col sm:flex-row gap-3 mt-6">
|
<ContactButtons className="mt-6" />
|
||||||
<Button href={links.tel} variant="primary">
|
|
||||||
<Phone aria-hidden="true" />
|
|
||||||
Call {business.phone}
|
|
||||||
</Button>
|
|
||||||
<Button href={links.sms} variant="outlineDark">
|
|
||||||
<MessageSquare aria-hidden="true" />
|
|
||||||
Text me
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="container mx-auto max-w-3xl mb-16">
|
<section className="container mx-auto max-w-3xl mb-16">
|
||||||
@@ -75,7 +68,7 @@ export default function ServicePage({ slug }) {
|
|||||||
<li key={fix} className="flex gap-2">
|
<li key={fix} className="flex gap-2">
|
||||||
<Check
|
<Check
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="size-5 shrink-0 mt-0.5 text-sky-800"
|
className="size-5 shrink-0 mt-0.5 text-primary"
|
||||||
/>
|
/>
|
||||||
{fix}
|
{fix}
|
||||||
</li>
|
</li>
|
||||||
@@ -86,25 +79,14 @@ export default function ServicePage({ slug }) {
|
|||||||
|
|
||||||
<section className="container mx-auto max-w-3xl mb-16">
|
<section className="container mx-auto max-w-3xl mb-16">
|
||||||
<h2 className="text-3xl font-bold mb-4">Pricing</h2>
|
<h2 className="text-3xl font-bold mb-4">Pricing</h2>
|
||||||
<table className="w-full mb-4 text-left">
|
<PriceTable items={service.prices} className="mb-4" />
|
||||||
<tbody>
|
|
||||||
{service.prices.map(({ item, price }) => (
|
|
||||||
<tr key={item} className="border-b">
|
|
||||||
<th scope="row" className="py-3 pr-4 font-normal">
|
|
||||||
{item}
|
|
||||||
</th>
|
|
||||||
<td className="py-3 font-bold text-sky-800">{price}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
The diagnosis is $35. If I can't fix it, you don't pay it.
|
The diagnosis is $35. If I can't fix it, you don't pay it.
|
||||||
Once I know what's wrong, I'll tell you what the repair will
|
Once I know what's wrong, I'll tell you what the repair will
|
||||||
cost before I start. If you decide not to go ahead, the $35 still
|
cost before I start. If you decide not to go ahead, the $35 still
|
||||||
applies.
|
applies.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-slate-600">
|
<p className="text-muted-foreground">
|
||||||
Labour is $35 an hour. Parts are extra.
|
Labour is $35 an hour. Parts are extra.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -144,12 +126,18 @@ export default function ServicePage({ slug }) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{testimonial && (
|
{testimonial && (
|
||||||
<figure className="mt-8 rounded-lg border bg-white p-6">
|
<Card className="mt-8 shadow-none">
|
||||||
<blockquote className="mb-3">
|
<CardContent>
|
||||||
“{testimonial.quote}”
|
<figure>
|
||||||
</blockquote>
|
<blockquote className="mb-3">
|
||||||
<figcaption className="font-bold">{testimonial.name}</figcaption>
|
“{testimonial.quote}”
|
||||||
</figure>
|
</blockquote>
|
||||||
|
<figcaption className="font-semibold">
|
||||||
|
{testimonial.name}
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -168,7 +156,7 @@ export default function ServicePage({ slug }) {
|
|||||||
<li key={related}>
|
<li key={related}>
|
||||||
<Link
|
<Link
|
||||||
href={`/services/${related}`}
|
href={`/services/${related}`}
|
||||||
className="inline-flex items-center gap-1 font-bold text-sky-800 hover:underline"
|
className="inline-flex items-center gap-1 font-bold text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{services[related].name}
|
{services[related].name}
|
||||||
<ArrowRight aria-hidden="true" className="size-4" />
|
<ArrowRight aria-hidden="true" className="size-4" />
|
||||||
|
|||||||
+9
-22
@@ -1,7 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Clock, Mail, MapPin, MessageCircle, MessageSquare, Phone } from "lucide-react";
|
import { Clock, Mail, MapPin } from "lucide-react";
|
||||||
import HeaderSm from "../components/HeaderSm";
|
import HeaderSm from "../components/HeaderSm";
|
||||||
import Button from "../components/Button";
|
import ContactButtons from "../components/ContactButtons";
|
||||||
import ContactForm from "../components/ContactForm";
|
import ContactForm from "../components/ContactForm";
|
||||||
import MapEmbed from "../components/MapEmbed";
|
import MapEmbed from "../components/MapEmbed";
|
||||||
import { business, links, pageMetadata } from "@/lib/site";
|
import { business, links, pageMetadata } from "@/lib/site";
|
||||||
@@ -24,34 +24,21 @@ export default function ContactPage() {
|
|||||||
The quickest way to reach me is a call or text. I'm{" "}
|
The quickest way to reach me is a call or text. I'm{" "}
|
||||||
{business.ownerName}, and I'll be the one fixing your computer.
|
{business.ownerName}, and I'll be the one fixing your computer.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row flex-wrap gap-3">
|
<ContactButtons whatsapp />
|
||||||
<Button href={links.tel} variant="primary">
|
|
||||||
<Phone aria-hidden="true" />
|
|
||||||
Call {business.phone}
|
|
||||||
</Button>
|
|
||||||
<Button href={links.sms} variant="outlineDark">
|
|
||||||
<MessageSquare aria-hidden="true" />
|
|
||||||
Text me
|
|
||||||
</Button>
|
|
||||||
<Button href={links.whatsapp} variant="outlineDark">
|
|
||||||
<MessageCircle aria-hidden="true" />
|
|
||||||
WhatsApp
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="container mx-auto max-w-5xl mb-16 grid grid-cols-1 gap-8 lg:grid-cols-2">
|
<section className="container mx-auto max-w-5xl mb-16 grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<ul className="space-y-4 mb-6">
|
<ul className="space-y-4 mb-6">
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<Clock aria-hidden="true" className="size-5 shrink-0 mt-0.5 text-sky-800" />
|
<Clock aria-hidden="true" className="size-5 shrink-0 mt-0.5 text-primary" />
|
||||||
<span>
|
<span>
|
||||||
<strong className="block">Hours</strong>
|
<strong className="block">Hours</strong>
|
||||||
Monday to Friday, 9am to 6pm. Weekends by appointment.
|
Monday to Friday, 9am to 6pm. Weekends by appointment.
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<MapPin aria-hidden="true" className="size-5 shrink-0 mt-0.5 text-sky-800" />
|
<MapPin aria-hidden="true" className="size-5 shrink-0 mt-0.5 text-primary" />
|
||||||
<span>
|
<span>
|
||||||
<strong className="block">Drop-off by appointment</strong>
|
<strong className="block">Drop-off by appointment</strong>
|
||||||
{address.street}, {address.suburb}, {address.city}{" "}
|
{address.street}, {address.suburb}, {address.city}{" "}
|
||||||
@@ -59,10 +46,10 @@ export default function ContactPage() {
|
|||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex gap-3">
|
<li className="flex gap-3">
|
||||||
<Mail aria-hidden="true" className="size-5 shrink-0 mt-0.5 text-sky-800" />
|
<Mail aria-hidden="true" className="size-5 shrink-0 mt-0.5 text-primary" />
|
||||||
<span>
|
<span>
|
||||||
<strong className="block">Email</strong>
|
<strong className="block">Email</strong>
|
||||||
<a href={links.email} className="text-sky-800 hover:underline">
|
<a href={links.email} className="text-primary hover:underline">
|
||||||
{business.email}
|
{business.email}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
@@ -75,11 +62,11 @@ export default function ContactPage() {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
See{" "}
|
See{" "}
|
||||||
<Link href="/pricing" className="text-sky-800 underline">
|
<Link href="/pricing" className="text-primary underline">
|
||||||
prices
|
prices
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
and{" "}
|
and{" "}
|
||||||
<Link href="/#faq" className="text-sky-800 underline">
|
<Link href="/#faq" className="text-primary underline">
|
||||||
common questions
|
common questions
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
|
|||||||
+12
-4
@@ -30,14 +30,15 @@
|
|||||||
---break---
|
---break---
|
||||||
*/
|
*/
|
||||||
:root {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0.5rem;
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
--foreground: oklch(0.129 0.042 264.695);
|
--foreground: oklch(0.129 0.042 264.695);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
--card-foreground: oklch(0.129 0.042 264.695);
|
--card-foreground: oklch(0.129 0.042 264.695);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.129 0.042 264.695);
|
--popover-foreground: oklch(0.129 0.042 264.695);
|
||||||
--primary: oklch(0.208 0.042 265.755);
|
/* Brand blue (Tailwind sky-800), matching the existing buttons and bands. */
|
||||||
|
--primary: oklch(0.443 0.11 240.79);
|
||||||
--primary-foreground: oklch(0.984 0.003 247.858);
|
--primary-foreground: oklch(0.984 0.003 247.858);
|
||||||
--secondary: oklch(0.968 0.007 247.896);
|
--secondary: oklch(0.968 0.007 247.896);
|
||||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.929 0.013 255.508);
|
--border: oklch(0.929 0.013 255.508);
|
||||||
--input: oklch(0.929 0.013 255.508);
|
--input: oklch(0.929 0.013 255.508);
|
||||||
--ring: oklch(0.704 0.04 256.788);
|
--ring: oklch(0.588 0.158 241.966);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
@@ -103,6 +104,8 @@
|
|||||||
---break---
|
---break---
|
||||||
*/
|
*/
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
--font-sans: var(--font-geist-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
@@ -147,7 +150,12 @@
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground antialiased;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
@apply tracking-tight text-balance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Keep the Chatwoot bubble clear of the mobile Call/Text bar. */
|
/* Keep the Chatwoot bubble clear of the mobile Call/Text bar. */
|
||||||
|
|||||||
+2
-4
@@ -27,10 +27,8 @@ export const metadata = {
|
|||||||
|
|
||||||
export default function RootLayout({ children }) {
|
export default function RootLayout({ children }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en-NZ">
|
<html lang="en-NZ" className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||||
<body
|
<body className="pb-12 md:pb-0">
|
||||||
className={`${geistSans.variable} ${geistMono.variable} pb-12 md:pb-0`}
|
|
||||||
>
|
|
||||||
<JsonLd data={localBusinessSchema()} />
|
<JsonLd data={localBusinessSchema()} />
|
||||||
<ChatwootWidget />
|
<ChatwootWidget />
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|||||||
+44
-49
@@ -2,8 +2,9 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import styles from "./page.module.css";
|
import styles from "./page.module.css";
|
||||||
import bgimage from "./assets/header-bg.jpg";
|
import bgimage from "./assets/header-bg.jpg";
|
||||||
import Button from "./components/Button";
|
import ContactButtons from "./components/ContactButtons";
|
||||||
import MapEmbed from "./components/MapEmbed";
|
import MapEmbed from "./components/MapEmbed";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import ContactForm from "./components/ContactForm";
|
import ContactForm from "./components/ContactForm";
|
||||||
import FaqList from "./components/FaqList";
|
import FaqList from "./components/FaqList";
|
||||||
import CtaBand from "./components/CtaBand";
|
import CtaBand from "./components/CtaBand";
|
||||||
@@ -18,10 +19,8 @@ import {
|
|||||||
HardDrive,
|
HardDrive,
|
||||||
Laptop,
|
Laptop,
|
||||||
MapPin,
|
MapPin,
|
||||||
MessageSquare,
|
|
||||||
Monitor,
|
Monitor,
|
||||||
MonitorCog,
|
MonitorCog,
|
||||||
Phone,
|
|
||||||
Power,
|
Power,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
Star,
|
Star,
|
||||||
@@ -103,7 +102,7 @@ const steps = [
|
|||||||
function SectionCta() {
|
function SectionCta() {
|
||||||
return (
|
return (
|
||||||
<p className="text-center mt-8">
|
<p className="text-center mt-8">
|
||||||
<a href={links.tel} className="font-bold text-sky-800 hover:underline">
|
<a href={links.tel} className="font-semibold text-primary hover:underline">
|
||||||
Call or text {business.phone}
|
Call or text {business.phone}
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -138,16 +137,7 @@ export default function Home() {
|
|||||||
tell you what it costs before I start. Local, honest help
|
tell you what it costs before I start. Local, honest help
|
||||||
for West Auckland.
|
for West Auckland.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-3 mb-6">
|
<ContactButtons onDark className="mb-6" />
|
||||||
<Button href={links.tel}>
|
|
||||||
<Phone aria-hidden="true" />
|
|
||||||
Call {business.phone}
|
|
||||||
</Button>
|
|
||||||
<Button href={links.sms} variant="outline">
|
|
||||||
<MessageSquare aria-hidden="true" />
|
|
||||||
Text me
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
|
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
|
||||||
<Star
|
<Star
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -169,30 +159,31 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="container mx-auto mb-32">
|
<section className="container mx-auto mb-24">
|
||||||
<h2 className="text-center text-3xl font-bold mb-2">
|
<h2 className="text-center text-3xl font-bold mb-2">
|
||||||
Common fixes & prices
|
Common fixes & prices
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-center mb-8 text-slate-600">
|
<p className="text-center mb-8 text-muted-foreground">
|
||||||
Labour prices. Parts are extra, and I'll always quote before
|
Labour prices. Parts are extra, and I'll always quote before
|
||||||
starting.
|
starting.
|
||||||
</p>
|
</p>
|
||||||
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{commonFixes.map(({ icon: Icon, title, price, href }) => (
|
{commonFixes.map(({ icon: Icon, title, price, href }) => (
|
||||||
<li key={title}>
|
<li key={title}>
|
||||||
<Link
|
<Link href={href} className="group block h-full rounded-xl">
|
||||||
href={href}
|
<Card className="h-full flex-row items-center gap-4 p-4 shadow-none transition-colors group-hover:border-primary">
|
||||||
className="flex h-full items-center gap-4 rounded-lg border p-4 transition hover:border-sky-800 hover:shadow-sm"
|
<Icon
|
||||||
>
|
aria-hidden="true"
|
||||||
<Icon
|
className="size-7 shrink-0 text-primary"
|
||||||
aria-hidden="true"
|
strokeWidth={1.5}
|
||||||
className="size-8 shrink-0 text-sky-800"
|
/>
|
||||||
strokeWidth={1.5}
|
<span>
|
||||||
/>
|
<span className="block font-semibold">{title}</span>
|
||||||
<span>
|
<span className="block text-sm text-muted-foreground">
|
||||||
<span className="block font-bold">{title}</span>
|
{price}
|
||||||
<span className="block text-sky-800">{price}</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -200,7 +191,7 @@ export default function Home() {
|
|||||||
<SectionCta />
|
<SectionCta />
|
||||||
<Link
|
<Link
|
||||||
href="/windows-10"
|
href="/windows-10"
|
||||||
className="mt-8 flex items-center justify-between gap-4 rounded-lg bg-sky-900 p-4 text-white hover:bg-sky-800"
|
className="mt-8 flex items-center justify-between gap-4 rounded-lg bg-sky-900 p-4 text-white transition-colors hover:bg-primary"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<span className="block font-bold">Still on Windows 10?</span>
|
<span className="block font-bold">Still on Windows 10?</span>
|
||||||
@@ -212,15 +203,15 @@ export default function Home() {
|
|||||||
</Link>
|
</Link>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="bg-slate-100 py-16 mb-32">
|
<section className="bg-slate-100 py-16 mb-24">
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<h2 className="text-center text-3xl font-bold mb-8">
|
<h2 className="text-center text-3xl font-bold mb-8">
|
||||||
How it works
|
How it works
|
||||||
</h2>
|
</h2>
|
||||||
<ol className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
<ol className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
{steps.map(({ title, text }, i) => (
|
{steps.map(({ title, text }, i) => (
|
||||||
<li key={title} className="text-center max-w-sm mx-auto">
|
<li key={title} className="border-t border-slate-300 pt-4">
|
||||||
<span className="mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-sky-800 text-xl font-bold text-white">
|
<span className="mb-3 flex size-8 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="text-xl font-bold mb-2">{title}</h3>
|
<h3 className="text-xl font-bold mb-2">{title}</h3>
|
||||||
@@ -232,7 +223,7 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="container mx-auto mb-32 max-w-3xl text-center">
|
<section className="container mx-auto mb-24 max-w-3xl text-center">
|
||||||
<h2 className="text-3xl font-bold mb-4">
|
<h2 className="text-3xl font-bold mb-4">
|
||||||
Hi, I'm {business.ownerName}
|
Hi, I'm {business.ownerName}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -249,22 +240,26 @@ export default function Home() {
|
|||||||
<SectionCta />
|
<SectionCta />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="container mx-auto mb-32">
|
<section className="container mx-auto mb-24">
|
||||||
<h2 className="text-center text-3xl font-bold mb-8">
|
<h2 className="text-center text-3xl font-bold mb-8">
|
||||||
What customers say
|
What customers say
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||||
{business.testimonials.map(({ name, quote }) => (
|
{business.testimonials.map(({ name, quote }) => (
|
||||||
<figure key={name} className="rounded-lg border p-6">
|
<Card key={name} className="shadow-none">
|
||||||
<blockquote className="mb-3">“{quote}”</blockquote>
|
<CardContent>
|
||||||
<figcaption className="font-bold">{name}</figcaption>
|
<figure>
|
||||||
</figure>
|
<blockquote className="mb-3">“{quote}”</blockquote>
|
||||||
|
<figcaption className="font-semibold">{name}</figcaption>
|
||||||
|
</figure>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-center mt-8">
|
<p className="text-center mt-8">
|
||||||
<a
|
<a
|
||||||
href={business.googleBusinessProfileUrl}
|
href={business.googleBusinessProfileUrl}
|
||||||
className="inline-flex items-center gap-1 font-bold text-sky-800 hover:underline"
|
className="inline-flex items-center gap-1 font-semibold text-primary hover:underline"
|
||||||
>
|
>
|
||||||
Read all reviews on Google
|
Read all reviews on Google
|
||||||
<ArrowRight aria-hidden="true" className="size-4" />
|
<ArrowRight aria-hidden="true" className="size-4" />
|
||||||
@@ -272,7 +267,7 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="container mx-auto mb-32 flex flex-col gap-8 lg:flex-row">
|
<section className="container mx-auto mb-24 flex flex-col gap-8 lg:flex-row">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<h2 className="text-3xl font-bold mb-4">Areas I serve</h2>
|
<h2 className="text-3xl font-bold mb-4">Areas I serve</h2>
|
||||||
<p className="mb-4 flex items-start gap-2">
|
<p className="mb-4 flex items-start gap-2">
|
||||||
@@ -293,7 +288,7 @@ export default function Home() {
|
|||||||
{page ? (
|
{page ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/areas/${page.slug}`}
|
href={`/areas/${page.slug}`}
|
||||||
className="text-sky-800 hover:underline"
|
className="text-primary hover:underline"
|
||||||
>
|
>
|
||||||
{area}
|
{area}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -311,7 +306,7 @@ export default function Home() {
|
|||||||
<p className="mt-4">
|
<p className="mt-4">
|
||||||
<a
|
<a
|
||||||
href={links.tel}
|
href={links.tel}
|
||||||
className="font-bold text-sky-800 hover:underline"
|
className="font-semibold text-primary hover:underline"
|
||||||
>
|
>
|
||||||
Call or text {business.phone}
|
Call or text {business.phone}
|
||||||
</a>
|
</a>
|
||||||
@@ -322,11 +317,11 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="container mx-auto mb-32">
|
<section className="container mx-auto mb-24">
|
||||||
<div className="rounded-lg border p-6 md:p-8 flex flex-col md:flex-row gap-6 md:items-center">
|
<Card className="flex-col gap-6 p-6 shadow-none md:flex-row md:items-center md:p-8">
|
||||||
<Briefcase
|
<Briefcase
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="size-12 shrink-0 text-sky-800"
|
className="size-12 shrink-0 text-primary"
|
||||||
strokeWidth={1.5}
|
strokeWidth={1.5}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -339,16 +334,16 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/business"
|
href="/business"
|
||||||
className="inline-flex items-center gap-1 font-bold text-sky-800 hover:underline"
|
className="inline-flex items-center gap-1 font-semibold text-primary hover:underline"
|
||||||
>
|
>
|
||||||
Business IT plans
|
Business IT plans
|
||||||
<ArrowRight aria-hidden="true" className="size-4" />
|
<ArrowRight aria-hidden="true" className="size-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
className="container mx-auto max-w-3xl mb-32 scroll-mt-20"
|
className="container mx-auto max-w-3xl mb-24 scroll-mt-20"
|
||||||
id="faq"
|
id="faq"
|
||||||
>
|
>
|
||||||
<h2 className="text-center text-3xl font-bold mb-8">
|
<h2 className="text-center text-3xl font-bold mb-8">
|
||||||
|
|||||||
+3
-13
@@ -1,6 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import HeaderSm from "../components/HeaderSm";
|
import HeaderSm from "../components/HeaderSm";
|
||||||
import CtaBand from "../components/CtaBand";
|
import CtaBand from "../components/CtaBand";
|
||||||
|
import PriceTable from "../components/PriceTable";
|
||||||
import { business, pageMetadata, priceList } from "@/lib/site";
|
import { business, pageMetadata, priceList } from "@/lib/site";
|
||||||
|
|
||||||
export const metadata = pageMetadata({
|
export const metadata = pageMetadata({
|
||||||
@@ -27,18 +28,7 @@ export default function PricingPage() {
|
|||||||
{priceList.map(({ group, items }) => (
|
{priceList.map(({ group, items }) => (
|
||||||
<section key={group} className="container mx-auto max-w-3xl mb-12">
|
<section key={group} className="container mx-auto max-w-3xl mb-12">
|
||||||
<h2 className="text-2xl font-bold mb-4">{group}</h2>
|
<h2 className="text-2xl font-bold mb-4">{group}</h2>
|
||||||
<table className="w-full text-left">
|
<PriceTable items={items} />
|
||||||
<tbody>
|
|
||||||
{items.map(({ item, price }) => (
|
|
||||||
<tr key={item} className="border-b">
|
|
||||||
<th scope="row" className="py-3 pr-4 font-normal">
|
|
||||||
{item}
|
|
||||||
</th>
|
|
||||||
<td className="py-3 font-bold text-sky-800">{price}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -63,7 +53,7 @@ export default function PricingPage() {
|
|||||||
by appointment, has no travel fee. For onsite visits or pickup and
|
by appointment, has no travel fee. For onsite visits or pickup and
|
||||||
return, the travel fee depends on distance. Get in touch with your
|
return, the travel fee depends on distance. Get in touch with your
|
||||||
suburb for a quote. Small businesses can also get{" "}
|
suburb for a quote. Small businesses can also get{" "}
|
||||||
<Link href="/business" className="text-sky-800 underline">
|
<Link href="/business" className="text-primary underline">
|
||||||
2 hours of support a month for $65
|
2 hours of support a month for $65
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { MessageSquare, Phone } from "lucide-react";
|
|
||||||
import HeaderSm from "../components/HeaderSm";
|
import HeaderSm from "../components/HeaderSm";
|
||||||
import Button from "../components/Button";
|
import ContactButtons from "../components/ContactButtons";
|
||||||
import CtaBand from "../components/CtaBand";
|
import CtaBand from "../components/CtaBand";
|
||||||
import FaqList from "../components/FaqList";
|
import FaqList from "../components/FaqList";
|
||||||
import { business, links, pageMetadata } from "@/lib/site";
|
import { pageMetadata } from "@/lib/site";
|
||||||
|
|
||||||
export const metadata = pageMetadata({
|
export const metadata = pageMetadata({
|
||||||
title: "Still on Windows 10? What to Do Now | Te Atatu Computers",
|
title: "Still on Windows 10? What to Do Now | Te Atatu Computers",
|
||||||
@@ -62,16 +61,7 @@ export default function Windows10Page() {
|
|||||||
buy a new computer. Here are your options, and I'm happy to
|
buy a new computer. Here are your options, and I'm happy to
|
||||||
check which one suits you for free.
|
check which one suits you for free.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<ContactButtons textLabel="Text for a free check" />
|
||||||
<Button href={links.tel} variant="primary">
|
|
||||||
<Phone aria-hidden="true" />
|
|
||||||
Call {business.phone}
|
|
||||||
</Button>
|
|
||||||
<Button href={links.sms} variant="outlineDark">
|
|
||||||
<MessageSquare aria-hidden="true" />
|
|
||||||
Text for a free check
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="container mx-auto max-w-3xl mb-16">
|
<section className="container mx-auto max-w-3xl mb-16">
|
||||||
@@ -79,7 +69,7 @@ export default function Windows10Page() {
|
|||||||
<ol className="space-y-6">
|
<ol className="space-y-6">
|
||||||
{options.map(({ title, text }, i) => (
|
{options.map(({ title, text }, i) => (
|
||||||
<li key={title} className="flex gap-4">
|
<li key={title} className="flex gap-4">
|
||||||
<span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-sky-800 font-bold text-white">
|
<span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary font-bold text-white">
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -92,7 +82,7 @@ export default function Windows10Page() {
|
|||||||
<p className="mt-6">
|
<p className="mt-6">
|
||||||
Not sure? Get in touch and I'll give you a straight answer. See
|
Not sure? Get in touch and I'll give you a straight answer. See
|
||||||
all prices on the{" "}
|
all prices on the{" "}
|
||||||
<Link href="/pricing" className="text-sky-800 underline">
|
<Link href="/pricing" className="text-primary underline">
|
||||||
pricing page
|
pricing page
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
import { Accordion as AccordionPrimitive } from "radix-ui"
|
||||||
import { ChevronDownIcon } from "lucide-react"
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
@@ -34,7 +34,7 @@ function AccordionTrigger({
|
|||||||
<AccordionPrimitive.Trigger
|
<AccordionPrimitive.Trigger
|
||||||
data-slot="accordion-trigger"
|
data-slot="accordion-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-base font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}>
|
{...props}>
|
||||||
@@ -57,7 +57,7 @@ function AccordionContent({
|
|||||||
<AccordionPrimitive.Content
|
<AccordionPrimitive.Content
|
||||||
data-slot="accordion-content"
|
data-slot="accordion-content"
|
||||||
forceMount
|
forceMount
|
||||||
className="data-[state=closed]:hidden data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
className="data-[state=closed]:hidden data-[state=open]:animate-accordion-down overflow-hidden text-base text-muted-foreground"
|
||||||
{...props}>
|
{...props}>
|
||||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||||
</AccordionPrimitive.Content>
|
</AccordionPrimitive.Content>
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva } from "class-variance-authority";
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Slot } from "radix-ui"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot.Root : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Card({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({
|
||||||
|
className,
|
||||||
|
type,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||||
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Label as LabelPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function NativeSelect({
|
||||||
|
className,
|
||||||
|
wrapperClassName,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"group/native-select relative w-fit has-[select:disabled]:opacity-50",
|
||||||
|
wrapperClassName
|
||||||
|
)}
|
||||||
|
data-slot="native-select-wrapper"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
data-slot="native-select"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"h-9 w-full min-w-0 appearance-none rounded-md border border-input bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed data-[size=sm]:h-8 data-[size=sm]:py-1 dark:bg-input/30 dark:hover:bg-input/50",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
|
||||||
|
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 text-muted-foreground opacity-50 select-none"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="native-select-icon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NativeSelectOption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
data-slot="native-select-option"
|
||||||
|
className={cn("bg-[Canvas] text-[CanvasText]", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NativeSelectOptGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<optgroup
|
||||||
|
data-slot="native-select-optgroup"
|
||||||
|
className={cn("bg-[Canvas] text-[CanvasText]", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { NativeSelect, NativeSelectOptGroup, NativeSelectOption }
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { ChevronDownIcon } from "lucide-react"
|
||||||
|
import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
function NavigationMenu({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
viewport = true,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
data-slot="navigation-menu"
|
||||||
|
data-viewport={viewport}
|
||||||
|
className={cn(
|
||||||
|
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{viewport && <NavigationMenuViewport />}
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
data-slot="navigation-menu-list"
|
||||||
|
className={cn(
|
||||||
|
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Item
|
||||||
|
data-slot="navigation-menu-item"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-[color,box-shadow] outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent/50 data-[state=open]:text-accent-foreground data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
|
||||||
|
)
|
||||||
|
|
||||||
|
function NavigationMenuTrigger({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
data-slot="navigation-menu-trigger"
|
||||||
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
data-slot="navigation-menu-content"
|
||||||
|
className={cn(
|
||||||
|
"top-0 left-0 w-full p-2 pr-2.5 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out md:absolute md:w-auto",
|
||||||
|
"group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuViewport({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
data-slot="navigation-menu-viewport"
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuLink({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Link
|
||||||
|
data-slot="navigation-menu-link"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground data-[active=true]:hover:bg-accent data-[active=true]:focus:bg-accent [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NavigationMenuIndicator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
data-slot="navigation-menu-indicator"
|
||||||
|
className={cn(
|
||||||
|
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:animate-in data-[state=visible]:fade-in",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuIndicator,
|
||||||
|
NavigationMenuViewport,
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { CircleIcon } from "lucide-react"
|
||||||
|
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
function RadioGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root
|
||||||
|
data-slot="radio-group"
|
||||||
|
className={cn("grid gap-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function RadioGroupItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
data-slot="radio-group-item"
|
||||||
|
className={cn(
|
||||||
|
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<RadioGroupPrimitive.Indicator
|
||||||
|
data-slot="radio-group-indicator"
|
||||||
|
className="relative flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary" />
|
||||||
|
</RadioGroupPrimitive.Indicator>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { RadioGroup, RadioGroupItem }
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { Separator as SeparatorPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
import { Dialog as SheetPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
function Sheet({
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTrigger({
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetClose({
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetPortal({
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
data-slot="sheet-overlay"
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
side = "right",
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
data-slot="sheet-content"
|
||||||
|
className={cn(
|
||||||
|
"fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500",
|
||||||
|
side === "right" &&
|
||||||
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||||
|
side === "left" &&
|
||||||
|
"inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||||
|
side === "top" &&
|
||||||
|
"inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||||
|
side === "bottom" &&
|
||||||
|
"inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-header"
|
||||||
|
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="sheet-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
data-slot="sheet-title"
|
||||||
|
className={cn("font-semibold text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SheetDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
data-slot="sheet-description"
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetTrigger,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetHeader,
|
||||||
|
SheetFooter,
|
||||||
|
SheetTitle,
|
||||||
|
SheetDescription,
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Table({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
Reference in new issue
Block a user