Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95887551b2 | ||
|
|
30dc6f2c7b | ||
|
|
f916f058fc | ||
|
|
f6846ebaae | ||
|
|
1ec5088652 | ||
|
|
275bce3b87 | ||
|
|
4d901b351e | ||
|
|
9f5cf387dc | ||
|
|
40d3a08050 | ||
|
|
7043b47de1 | ||
|
|
c085e2b031 |
No files matched your search
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"shadcn": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"shadcn@latest",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,23 @@ const nextConfig = {
|
||||
images: {
|
||||
qualities: [75, 100],
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
// Cloudflare already does this; kept here as a fallback.
|
||||
{
|
||||
source: "/:path*",
|
||||
has: [{ type: "host", value: "www.teatatucomputers.com" }],
|
||||
destination: "https://teatatucomputers.com/:path*",
|
||||
permanent: true,
|
||||
},
|
||||
// Web design is no longer offered.
|
||||
{
|
||||
source: "/services/web-design",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
Generated
+2126
-1261
File diff suppressed because it is too large.
Load diff
+12
-18
@@ -9,27 +9,21 @@
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/env": "^16.2.10",
|
||||
"@next/third-parties": "^16.2.10",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@vercel/analytics": "^1.4.1",
|
||||
"@vercel/speed-insights": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.487.0",
|
||||
"motion": "^11.16.0",
|
||||
"next": "^16.2.10",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"sharp": "^0.33.5",
|
||||
"tailwind-merge": "^3.1.0",
|
||||
"tw-animate-css": "^1.2.5"
|
||||
"cn": "^0.4.0",
|
||||
"lucide-react": "^1.49.0",
|
||||
"next": "^16.3.7",
|
||||
"radix-ui": "^1.6.7",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"sharp": "^0.35.5",
|
||||
"tw-animate-css": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.1",
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-next": "^16.2.10",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^4.1.1"
|
||||
"eslint-config-next": "^16.3.7",
|
||||
"postcss": "^8.5.28",
|
||||
"tailwindcss": "^4.3.3"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,86 @@
|
||||
import Link from "next/link";
|
||||
import HeaderSm from "../components/HeaderSm";
|
||||
import CtaBand from "../components/CtaBand";
|
||||
import { business, pageMetadata } from "@/lib/site";
|
||||
import { areas } from "@/lib/areas";
|
||||
|
||||
export const metadata = pageMetadata({
|
||||
title: "About John | Te Atatu Computers",
|
||||
description:
|
||||
"Te Atatu Computers is John, a local computer repairer on Te Atatū Peninsula. One person, straight answers and a price before any work starts.",
|
||||
path: "/about",
|
||||
});
|
||||
|
||||
const areaSlugs = Object.fromEntries(areas.map(({ name, slug }) => [name, slug]));
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div>
|
||||
<HeaderSm text={`Hi, I'm ${business.ownerName}`} />
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-12">
|
||||
<p className="text-lg mb-4">
|
||||
Te Atatu Computers is me, {business.ownerName}. I fix laptops and
|
||||
PCs, upgrade slow computers, remove viruses and help homes and small
|
||||
businesses with their IT, from my base on Te Atatū Peninsula.
|
||||
</p>
|
||||
<p className="text-lg mb-4">
|
||||
Because it's just me, the person you call or text is the person
|
||||
who fixes your computer. There's no front desk and no call
|
||||
centre, and you won't have to explain the problem twice.
|
||||
</p>
|
||||
<p className="text-lg">
|
||||
I keep things simple: a $35 diagnosis, a clear price before I start,
|
||||
and if I can't fix it, you don't pay the diagnosis fee.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-12">
|
||||
<h2 className="text-2xl font-bold mb-4">How I work</h2>
|
||||
<ul className="list-disc pl-6 space-y-2">
|
||||
<li>
|
||||
<strong>Drop-off</strong> at {business.address.street},{" "}
|
||||
{business.address.suburb}, by appointment.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Onsite visits and pickup</strong> across my service area,
|
||||
with a travel fee based on distance.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Remote support</strong> for software problems.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Hours:</strong> Monday to Friday, 9am to 6pm. Weekends by
|
||||
appointment.
|
||||
</li>
|
||||
<li>
|
||||
I work on Windows laptops and PCs, Chromebooks and custom-built
|
||||
computers. I don't repair Apple devices.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-2xl font-bold mb-4">Where I work</h2>
|
||||
<ul className="grid grid-cols-2 gap-2 list-disc pl-6">
|
||||
{business.serviceArea.map((name) => (
|
||||
<li key={name}>
|
||||
{areaSlugs[name] ? (
|
||||
<Link
|
||||
href={`/areas/${areaSlugs[name]}`}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
) : (
|
||||
name
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<CtaBand />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import HeaderSm from "../../components/HeaderSm";
|
||||
import ContactButtons from "../../components/ContactButtons";
|
||||
import CtaBand from "../../components/CtaBand";
|
||||
import FaqList from "../../components/FaqList";
|
||||
import JsonLd from "../../components/JsonLd";
|
||||
import { areas, getArea } from "@/lib/areas";
|
||||
import { services } from "@/lib/services";
|
||||
import { business, pageMetadata, serviceSchema } from "@/lib/site";
|
||||
|
||||
export const dynamicParams = false;
|
||||
|
||||
export function generateStaticParams() {
|
||||
return areas.map(({ slug }) => ({ suburb: slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const area = getArea((await params).suburb);
|
||||
return pageMetadata({
|
||||
title: `Computer Repair ${area.name} | Te Atatu Computers`,
|
||||
description: `Laptop & PC repairs, upgrades, virus removal and IT support in ${area.name}. $35 diagnosis, no fix no fee. Call or text ${business.phone}.`,
|
||||
path: `/areas/${area.slug}`,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function AreaPage({ params }) {
|
||||
const area = getArea((await params).suburb);
|
||||
if (!area) notFound();
|
||||
|
||||
const faqs = [
|
||||
area.faq,
|
||||
{
|
||||
question: "How much will it cost?",
|
||||
answer:
|
||||
"A diagnosis is $35, and if I can't fix it you don't pay it. Labour is $35 an hour and parts are extra. I'll tell you the cost before I start.",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<JsonLd
|
||||
data={serviceSchema({
|
||||
name: `Computer repair in ${area.name}`,
|
||||
description: area.intro,
|
||||
path: `/areas/${area.slug}`,
|
||||
areas: [area.name],
|
||||
})}
|
||||
/>
|
||||
<HeaderSm text={`Computer & Laptop Repair in ${area.name}`} />
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<p className="text-lg mb-6">{area.intro}</p>
|
||||
<ContactButtons />
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-4">
|
||||
Getting your computer to me
|
||||
</h2>
|
||||
<p className="mb-4">{area.gettingThere}</p>
|
||||
<p>
|
||||
I work Monday to Friday, 9am to 6pm, and weekends by appointment.
|
||||
Most repairs are done within 24–48 hours, although this isn't
|
||||
guaranteed.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-4">
|
||||
What I can help with in {area.name}
|
||||
</h2>
|
||||
<ul className="space-y-2 mb-4">
|
||||
{Object.entries(services).map(([slug, service]) => (
|
||||
<li key={slug}>
|
||||
<Link
|
||||
href={`/services/${slug}`}
|
||||
className="inline-flex items-center gap-1 font-bold text-primary hover:underline"
|
||||
>
|
||||
{service.name}
|
||||
<ArrowRight aria-hidden="true" className="size-4" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>
|
||||
See all prices on the{" "}
|
||||
<Link href="/pricing" className="text-primary underline">
|
||||
pricing page
|
||||
</Link>
|
||||
. I don't repair Apple devices.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-4">Questions</h2>
|
||||
<FaqList faqs={faqs} />
|
||||
</section>
|
||||
|
||||
<CtaBand />
|
||||
|
||||
<section className="container mx-auto max-w-3xl py-16">
|
||||
<h2 className="text-2xl font-bold mb-4">Other areas I serve</h2>
|
||||
<ul className="flex flex-wrap gap-x-6 gap-y-2">
|
||||
{areas
|
||||
.filter(({ slug }) => slug !== area.slug)
|
||||
.map(({ slug, name }) => (
|
||||
<li key={slug}>
|
||||
<Link
|
||||
href={`/areas/${slug}`}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import Link from "next/link";
|
||||
import { Check } from "lucide-react";
|
||||
import HeaderSm from "../components/HeaderSm";
|
||||
import ContactButtons from "../components/ContactButtons";
|
||||
import CtaBand from "../components/CtaBand";
|
||||
import FaqList from "../components/FaqList";
|
||||
import JsonLd from "../components/JsonLd";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
businessPlan,
|
||||
pageMetadata,
|
||||
serviceSchema,
|
||||
} from "@/lib/site";
|
||||
|
||||
const page = {
|
||||
title: "Small Business IT Support Plan West Auckland",
|
||||
description:
|
||||
"IT support for West Auckland small businesses: $65 a month for 2 hours of remote or onsite help. No lock-in. Wi-Fi, email, Microsoft 365, backups.",
|
||||
path: "/business",
|
||||
};
|
||||
|
||||
export const metadata = pageMetadata(page);
|
||||
|
||||
const covered = [
|
||||
"Wi-Fi, routers and network problems",
|
||||
"Email setup and problems",
|
||||
"Microsoft 365 and Google Workspace accounts",
|
||||
"Printers and scanners",
|
||||
"New computer setup and moving files across",
|
||||
"Backups for your business files",
|
||||
"Security basics: antivirus, updates and passwords",
|
||||
"Software installation and everyday troubleshooting",
|
||||
];
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What happens if I need more than 2 hours in a month?",
|
||||
answer:
|
||||
"Extra time is charged at the normal $35 an hour.",
|
||||
},
|
||||
{
|
||||
question: "Do unused hours carry over?",
|
||||
answer:
|
||||
"No. The 2 hours are for that month and don't roll over to the next one.",
|
||||
},
|
||||
{
|
||||
question: "Is there a contract?",
|
||||
answer: "No lock-in. You can cancel any time.",
|
||||
},
|
||||
{
|
||||
question: "Do I have to be on the plan?",
|
||||
answer:
|
||||
"No. You can also book support whenever you need it at $35 an hour, onsite or remote.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function BusinessPage() {
|
||||
return (
|
||||
<div>
|
||||
<JsonLd
|
||||
data={serviceSchema({
|
||||
name: "Small business IT support",
|
||||
description: page.description,
|
||||
path: page.path,
|
||||
})}
|
||||
/>
|
||||
<HeaderSm text="Small Business IT Support in West Auckland" />
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<p className="text-lg mb-4">
|
||||
Most small businesses don't need a full-time IT person. They
|
||||
need someone local to call when the Wi-Fi drops, email stops
|
||||
working or a new staff member needs a laptop set up.
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
I support small businesses around Te Atatū and West Auckland, onsite
|
||||
or remotely. You deal with me directly, every time.
|
||||
</p>
|
||||
<ContactButtons />
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<Card className="border-primary shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
<h2 className="text-2xl font-bold">Business IT plan</h2>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<span className="text-4xl font-bold text-primary">
|
||||
{businessPlan.price}
|
||||
</span>{" "}
|
||||
a month
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2">
|
||||
{businessPlan.includes.map((item) => (
|
||||
<li key={item} className="flex gap-2">
|
||||
<Check
|
||||
aria-hidden="true"
|
||||
className="size-5 shrink-0 mt-0.5 text-primary"
|
||||
/>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<p className="text-sm text-muted-foreground">{businessPlan.terms}</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<p className="mt-4">
|
||||
Prefer to pay as you go? Support is $35 an hour, onsite or remote.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-4">What's covered</h2>
|
||||
<ul className="space-y-2 mb-4">
|
||||
{covered.map((item) => (
|
||||
<li key={item} className="flex gap-2">
|
||||
<Check
|
||||
aria-hidden="true"
|
||||
className="size-5 shrink-0 mt-0.5 text-primary"
|
||||
/>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>
|
||||
Hardware repairs and upgrades are covered too. Parts are extra. See{" "}
|
||||
<Link
|
||||
href="/services/it-support"
|
||||
className="text-primary underline"
|
||||
>
|
||||
IT support
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="/services/computer-repair"
|
||||
className="text-primary underline"
|
||||
>
|
||||
computer repairs
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-4">Questions</h2>
|
||||
<FaqList faqs={faqs} />
|
||||
</section>
|
||||
|
||||
<CtaBand />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { eventForHref, trackEvent } from "@/lib/analytics";
|
||||
|
||||
// One site-wide listener, so every call/text/email/review link is tracked.
|
||||
export default function AnalyticsEvents() {
|
||||
useEffect(() => {
|
||||
function handleClick(event) {
|
||||
const link = event.target.closest?.("a[href]");
|
||||
const name = link && eventForHref(link.getAttribute("href"));
|
||||
if (name) trackEvent(name);
|
||||
}
|
||||
document.addEventListener("click", handleClick);
|
||||
return () => document.removeEventListener("click", handleClick);
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import styles from "./Button.module.css";
|
||||
|
||||
export default function Button({ children, onClick }) {
|
||||
return (
|
||||
<button className={styles.button} onClick={onClick}>{children}</button>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +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;
|
||||
}
|
||||
@@ -9,7 +9,8 @@ class ChatwootWidget extends React.Component {
|
||||
hideMessageBubble: false,
|
||||
position: "right", // This can be left or right
|
||||
locale: "en", // Language to be set
|
||||
type: "expanded_bubble", // [standard, expanded_bubble]
|
||||
// Small round bubble on phones so it doesn't cover the page's buttons.
|
||||
type: window.innerWidth < 768 ? "standard" : "expanded_bubble", // [standard, expanded_bubble]
|
||||
};
|
||||
|
||||
// Paste the script from inbox settings except the <script> tag
|
||||
|
||||
@@ -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 "cn";
|
||||
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,68 +1,214 @@
|
||||
import styles from "./ContactForm.module.css";
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { CircleAlert, CircleCheck } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption,
|
||||
} from "@/components/ui/native-select";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { business, links } from "@/lib/site";
|
||||
import { trackEvent } from "@/lib/analytics";
|
||||
|
||||
const FORM_ACTION = "https://form.taxi/s/es9ddva1";
|
||||
|
||||
const devices = ["Laptop", "Desktop PC", "Chromebook", "Other"];
|
||||
const problems = [
|
||||
"Won't turn on or won't start",
|
||||
"Cracked or broken screen",
|
||||
"Running slow / upgrade",
|
||||
"Virus, pop-ups or scam",
|
||||
"Battery or charging",
|
||||
"Moving files to a new computer",
|
||||
"Windows 10 / Windows 11",
|
||||
"Business IT support",
|
||||
"Something else",
|
||||
];
|
||||
const contactMethods = ["Call", "Text", "Email"];
|
||||
|
||||
function Field({ children }) {
|
||||
return <div className="grid gap-2">{children}</div>;
|
||||
}
|
||||
|
||||
// Submits to form.taxi with JavaScript for an inline success message; without
|
||||
// JavaScript the form still posts normally.
|
||||
export default function ContactForm() {
|
||||
const [status, setStatus] = useState("idle");
|
||||
|
||||
async function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
setStatus("sending");
|
||||
try {
|
||||
const response = await fetch(FORM_ACTION, {
|
||||
method: "POST",
|
||||
headers: { Accept: "application/json" },
|
||||
body: new FormData(event.currentTarget),
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !result.success) throw new Error("Submit failed");
|
||||
setStatus("sent");
|
||||
trackEvent("submit_quote_form");
|
||||
} catch {
|
||||
setStatus("error");
|
||||
}
|
||||
}
|
||||
|
||||
if (status === "sent") {
|
||||
return (
|
||||
<Alert role="status" className="border-primary">
|
||||
<CircleCheck aria-hidden="true" />
|
||||
<AlertTitle>Thanks, I've got it</AlertTitle>
|
||||
<AlertDescription>
|
||||
<p>
|
||||
I'll usually reply the same day during business hours (Monday
|
||||
to Friday, 9am to 6pm). If it's urgent, call or text{" "}
|
||||
<a href={links.tel} className="font-semibold text-primary underline">
|
||||
{business.phone}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-4">Contact Us</h2>
|
||||
<div className={styles.contactDetails}>
|
||||
<h3 className="text-lg font-bold hover:underline">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
>
|
||||
<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>
|
||||
<a href="tel:+640211077483">021 107 7483</a>
|
||||
</h3>
|
||||
<h3 className="text-lg font-bold hover:underline">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.404 14.596A6.5 6.5 0 1 1 16.5 10a1.25 1.25 0 0 1-2.5 0 4 4 0 1 0-.571 2.06A2.75 2.75 0 0 0 18 10a8 8 0 1 0-2.343 5.657.75.75 0 0 0-1.06-1.06 6.5 6.5 0 0 1-9.193 0ZM10 7.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<a href="mailto:[email protected]">
|
||||
info@teatatucomputers.com
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold mb-2">Get a quote</h2>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Tell me what's going on and I'll get back to you, usually the
|
||||
same day during business hours.
|
||||
</p>
|
||||
<form
|
||||
action="https://form.taxi/s/es9ddva1"
|
||||
action={FORM_ACTION}
|
||||
method="POST"
|
||||
className={styles.form}
|
||||
className="grid gap-5"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<label htmlFor="Name">Name*</label>
|
||||
<input type="text" name="Name" required />
|
||||
<Field>
|
||||
<Label htmlFor="cf-name">Name*</Label>
|
||||
<Input id="cf-name" name="Name" autoComplete="name" required />
|
||||
</Field>
|
||||
|
||||
<label htmlFor="Phone">Phone Number </label>
|
||||
<input type="tel" name="Phone" />
|
||||
|
||||
<label htmlFor="Email">Email*</label>
|
||||
<input type="email" name="Email" required />
|
||||
|
||||
<label htmlFor="firstname">Message*</label>
|
||||
<textarea name="Message"></textarea>
|
||||
|
||||
<p className={styles.sm}>* required</p>
|
||||
|
||||
<input
|
||||
type="submit"
|
||||
value="Submit"
|
||||
<div className="grid gap-5 sm:grid-cols-2">
|
||||
<Field>
|
||||
<Label htmlFor="cf-email">Email*</Label>
|
||||
<Input
|
||||
id="cf-email"
|
||||
type="email"
|
||||
name="Email"
|
||||
autoComplete="email"
|
||||
required
|
||||
className="text-white bg-sky-800 hover:bg-sky-700 transition"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<Label htmlFor="cf-phone">Phone number</Label>
|
||||
<Input id="cf-phone" type="tel" name="Phone" autoComplete="tel" />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<fieldset className="grid gap-3">
|
||||
<legend className="mb-3 text-sm font-medium">
|
||||
Best way to reach you
|
||||
</legend>
|
||||
<RadioGroup
|
||||
name="Preferred contact"
|
||||
defaultValue="Email"
|
||||
className="flex gap-6"
|
||||
>
|
||||
{contactMethods.map((method) => (
|
||||
<div key={method} className="flex items-center gap-2">
|
||||
<RadioGroupItem value={method} id={`cf-contact-${method}`} />
|
||||
<Label htmlFor={`cf-contact-${method}`} className="font-normal">
|
||||
{method}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
|
||||
<div className="grid gap-5 sm:grid-cols-2">
|
||||
<Field>
|
||||
<Label htmlFor="cf-device">Device</Label>
|
||||
<NativeSelect
|
||||
id="cf-device"
|
||||
name="Device"
|
||||
defaultValue="Laptop"
|
||||
wrapperClassName="w-full"
|
||||
className="text-base md:text-sm"
|
||||
>
|
||||
{devices.map((device) => (
|
||||
<NativeSelectOption key={device}>{device}</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
<Field>
|
||||
<Label htmlFor="cf-problem">Problem</Label>
|
||||
<NativeSelect
|
||||
id="cf-problem"
|
||||
name="Problem"
|
||||
defaultValue=""
|
||||
wrapperClassName="w-full"
|
||||
className="text-base md:text-sm"
|
||||
>
|
||||
<NativeSelectOption value="" disabled>
|
||||
Choose one
|
||||
</NativeSelectOption>
|
||||
{problems.map((problem) => (
|
||||
<NativeSelectOption key={problem}>{problem}</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field>
|
||||
<Label htmlFor="cf-message">What's going on?*</Label>
|
||||
<Textarea
|
||||
id="cf-message"
|
||||
name="Message"
|
||||
rows={5}
|
||||
placeholder="e.g. HP laptop, screen cracked in the corner, still turns on"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Honeypot: hidden from people, filled in by bots. Enable "_gotcha" in form.taxi's Spam Shield. */}
|
||||
<input
|
||||
type="text"
|
||||
name="_gotcha"
|
||||
className="hidden"
|
||||
tabIndex={-1}
|
||||
autoComplete="off"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
* required. I don't repair Apple devices.
|
||||
</p>
|
||||
|
||||
{status === "error" && (
|
||||
<Alert variant="destructive" role="alert">
|
||||
<CircleAlert aria-hidden="true" />
|
||||
<AlertTitle>That didn't send</AlertTitle>
|
||||
<AlertDescription>
|
||||
Please try again, or call or text {business.phone}.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
disabled={status === "sending"}
|
||||
className="justify-self-start"
|
||||
>
|
||||
{status === "sending" ? "Sending…" : "Send"}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,41 +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;
|
||||
}
|
||||
|
||||
.contactDetails {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.contactDetails h3 {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contactDetails h3 a {
|
||||
color: black;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import ContactButtons from "./ContactButtons";
|
||||
|
||||
export default function CtaBand({ quoteHref = "/contact" }) {
|
||||
return (
|
||||
<section className="bg-sky-900 text-white py-16">
|
||||
<div className="container mx-auto text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-3">
|
||||
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
|
||||
with a quote.
|
||||
</p>
|
||||
<ContactButtons
|
||||
onDark
|
||||
whatsapp
|
||||
quoteHref={quoteHref}
|
||||
className="justify-center"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import JsonLd from "./JsonLd";
|
||||
import { faqSchema } from "@/lib/site";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
|
||||
// FAQ accordion plus matching FAQPage JSON-LD. Answers stay in the HTML.
|
||||
export default function FaqList({ faqs }) {
|
||||
return (
|
||||
<>
|
||||
<JsonLd data={faqSchema(faqs)} />
|
||||
<Accordion className="mb-8">
|
||||
{faqs.map(({ question, answer, list }, i) => (
|
||||
<AccordionItem key={question} value={`item-${i + 1}`}>
|
||||
<AccordionTrigger>{question}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{answer}
|
||||
{list && (
|
||||
<ul className="list-disc pl-6 my-2">
|
||||
{list.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +1,98 @@
|
||||
import Link from "next/link";
|
||||
import { Mail, MapPin, Phone } from "lucide-react";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { business, links } from "@/lib/site";
|
||||
import { services } from "@/lib/services";
|
||||
import { areas } from "@/lib/areas";
|
||||
|
||||
const pages = [
|
||||
{ href: "/pricing", label: "Pricing" },
|
||||
{ href: "/business", label: "Business IT plans" },
|
||||
{ href: "/windows-10", label: "Windows 10 help" },
|
||||
{ href: "/about", label: "About" },
|
||||
{ href: "/contact", label: "Contact" },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
const { address } = business;
|
||||
return (
|
||||
<footer className="bg-slate-800 text-white py-8 text-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<h3 className="flex flex-row items-center justify-center mb-4 space-x-2 hover:underline text-lg font-bold">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
<footer className="bg-slate-800 text-white py-12">
|
||||
<div className="container mx-auto grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<p className="text-lg font-bold mb-4">{business.name}</p>
|
||||
<ul className="space-y-3">
|
||||
<li>
|
||||
<a
|
||||
href={links.tel}
|
||||
className="flex items-center gap-2 hover:underline"
|
||||
>
|
||||
<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>
|
||||
<a href="tel:+640211077483">
|
||||
021 107 7483
|
||||
<Phone aria-hidden="true" className="size-4 shrink-0" />
|
||||
{business.phone}
|
||||
</a>
|
||||
</h3>
|
||||
<h3 className="flex flex-row items-center justify-center mb-4 space-x-2 hover:underline text-lg font-bold">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={links.email}
|
||||
className="flex items-center gap-2 hover:underline"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.404 14.596A6.5 6.5 0 1 1 16.5 10a1.25 1.25 0 0 1-2.5 0 4 4 0 1 0-.571 2.06A2.75 2.75 0 0 0 18 10a8 8 0 1 0-2.343 5.657.75.75 0 0 0-1.06-1.06 6.5 6.5 0 0 1-9.193 0ZM10 7.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<a href="mailto:[email protected]">
|
||||
info@teatatucomputers.com
|
||||
<Mail aria-hidden="true" className="size-4 shrink-0" />
|
||||
{business.email}
|
||||
</a>
|
||||
</h3>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<MapPin aria-hidden="true" className="size-4 shrink-0 mt-1" />
|
||||
<span>
|
||||
{address.street}, {address.suburb}, {address.city}{" "}
|
||||
{address.postcode}
|
||||
<br />
|
||||
{business.hours}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<nav aria-label="Services">
|
||||
<p className="text-lg font-bold mb-4">Services</p>
|
||||
<ul className="space-y-2">
|
||||
{Object.entries(services).map(([slug, { name }]) => (
|
||||
<li key={slug}>
|
||||
<Link href={`/services/${slug}`} className="hover:underline">
|
||||
{name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<nav aria-label="Areas">
|
||||
<p className="text-lg font-bold mb-4">Areas</p>
|
||||
<ul className="space-y-2">
|
||||
{areas.map(({ slug, name }) => (
|
||||
<li key={slug}>
|
||||
<Link href={`/areas/${slug}`} className="hover:underline">
|
||||
{name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<nav aria-label="More">
|
||||
<p className="text-lg font-bold mb-4">More</p>
|
||||
<ul className="space-y-2">
|
||||
{pages.map(({ href, label }) => (
|
||||
<li key={href}>
|
||||
<Link href={href} className="hover:underline">
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="container mx-auto mt-10">
|
||||
<Separator className="mb-6 bg-slate-700" />
|
||||
<p className="text-sm text-slate-400">
|
||||
Copyright © {business.name}, {new Date().getFullYear()}
|
||||
</p>
|
||||
</div>
|
||||
<p>Copyright © Te Atatu Computer Repair, {new Date().getFullYear()}</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -8,10 +8,10 @@ export default function HeaderSm({ text }) {
|
||||
<div className={styles.header}>
|
||||
<Image
|
||||
src={bgimage}
|
||||
alt="Header Background"
|
||||
alt=""
|
||||
placeholder="blur"
|
||||
quality={100}
|
||||
fill
|
||||
priority
|
||||
sizes="100vw"
|
||||
style={{
|
||||
objectFit: "cover",
|
||||
@@ -20,7 +20,7 @@ export default function HeaderSm({ text }) {
|
||||
<div className={styles.textbox}>
|
||||
<div className="container mx-auto">
|
||||
<div className={styles.textboxInner}>
|
||||
<h1 className="text-4xl font-bold">{text}</h1>
|
||||
<h1 className="text-2xl md:text-4xl font-bold">{text}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function JsonLd({ data }) {
|
||||
return (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
// Escape "<" so the JSON can't close the script tag early.
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(data).replace(/</g, "\\u003c"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// This key is public by design; restrict it by HTTP referrer in Google Cloud Console.
|
||||
const MAPS_API_KEY = "AIzaSyDbgQY55TQY5CefsCUwKbc8RfoOUe8U5gY";
|
||||
const PLACE_ID = "ChIJz0ZoDYhADW0R0TDgWoWMq0M";
|
||||
|
||||
// Plain iframe so it can have an accessible title.
|
||||
export default function MapEmbed({ height = "400px" }) {
|
||||
return (
|
||||
<iframe
|
||||
title="Map showing Te Atatu Computers on Graham Avenue, Te Atatū Peninsula"
|
||||
src={`https://www.google.com/maps/embed/v1/place?key=${MAPS_API_KEY}&q=place_id:${PLACE_ID}`}
|
||||
width="100%"
|
||||
height={height}
|
||||
style={{ border: 0 }}
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
allowFullScreen
|
||||
/>
|
||||
);
|
||||
}
|
||||
+141
-164
@@ -1,186 +1,163 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
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 { ChevronDownIcon } from "lucide-react";
|
||||
// import { motion } from "motion/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 "cn";
|
||||
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() {
|
||||
const pathname = usePathname();
|
||||
const [isActive, setActive] = useState("false");
|
||||
const [isDropdownActive, setDropdownActive] = useState(false);
|
||||
|
||||
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);
|
||||
};
|
||||
}, []);
|
||||
const isActive = (href) => pathname === href;
|
||||
const inServices = serviceLinks.some(({ href }) => isActive(href));
|
||||
|
||||
return (
|
||||
<nav className={`${styles.navbar} shadow-sm`}>
|
||||
<div className={`${styles.container} container mx-auto`} ref={wrapperRef}>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<Link href="/" className={styles.logo}>
|
||||
<Image src={logo} width="32" height="32" alt="Logo" />
|
||||
<p>Te Atatu Computers</p>
|
||||
<header className="sticky top-0 z-50 border-b bg-white/90 backdrop-blur">
|
||||
<div className="container mx-auto flex h-16 items-center justify-between gap-4">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<Image src={logo} width={32} height={32} alt="" />
|
||||
<span className="font-mono">{business.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<button
|
||||
className={styles.menuIcon}
|
||||
aria-label="Menu"
|
||||
onClick={handleToggle}
|
||||
|
||||
{/* Desktop */}
|
||||
<NavigationMenu viewport={false} className="hidden lg:flex">
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
active={isActive("/")}
|
||||
className={navigationMenuTriggerStyle()}
|
||||
>
|
||||
<Image
|
||||
src={isActive ? menu : close}
|
||||
width="32"
|
||||
height="32"
|
||||
alt="Menu"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<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"
|
||||
>
|
||||
<a
|
||||
className={`${styles.dropdownTitle}
|
||||
${pathname === "/services" ? styles.active : ""}`}
|
||||
onClick={handleDropdownToggle}
|
||||
<Link href="/">Home</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger
|
||||
className={cn(inServices && "text-primary")}
|
||||
>
|
||||
Services
|
||||
</a>
|
||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</div>
|
||||
<ul className={styles.dropdownMenu}>
|
||||
<li>
|
||||
<Link
|
||||
href="/services/computer-repair"
|
||||
className={`link ${
|
||||
pathname === "/services/computer-repair"
|
||||
? styles.active
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
Computer & Laptop Repairs
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/services/computer-upgrades"
|
||||
className={`link ${
|
||||
pathname === "/services/computer-upgrades"
|
||||
? styles.active
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
Computer Upgrades & Speedup
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/services/it-support"
|
||||
className={`link ${
|
||||
pathname === "/services/it-support" ? styles.active : ""
|
||||
}`}
|
||||
>
|
||||
IT Support
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/services/web-design"
|
||||
className={`link ${
|
||||
pathname === "/services/web-design" ? styles.active : ""
|
||||
}`}
|
||||
>
|
||||
Web Design
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/services/virus-removal"
|
||||
className={`link ${
|
||||
pathname === "/services/virus-removal" ? styles.active : ""
|
||||
}`}
|
||||
>
|
||||
Virus Removal
|
||||
</Link>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="grid w-64 gap-1">
|
||||
{serviceLinks.map(({ href, label }) => (
|
||||
<li key={href}>
|
||||
<NavigationMenuLink asChild active={isActive(href)}>
|
||||
<Link href={href}>{label}</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/#faq"
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
{pageLinks.map(({ href, label }) => (
|
||||
<NavigationMenuItem key={href}>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
active={isActive(href)}
|
||||
className={navigationMenuTriggerStyle()}
|
||||
>
|
||||
FAQ
|
||||
<Link href={href}>{label}</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
))}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
<Button asChild className="hidden lg:inline-flex">
|
||||
<Link href="/contact">
|
||||
<Phone aria-hidden="true" />
|
||||
Contact
|
||||
</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>
|
||||
</Button>
|
||||
|
||||
{/* Mobile */}
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="lg:hidden">
|
||||
<MenuIcon className="size-6" />
|
||||
<span className="sr-only">Menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="right" className="w-full max-w-xs overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Menu</SheetTitle>
|
||||
</SheetHeader>
|
||||
<nav className="grid gap-6 px-4 pb-8">
|
||||
<MobileList links={[{ href: "/", label: "Home" }]} isActive={isActive} />
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-muted-foreground">Services</p>
|
||||
<MobileList links={serviceLinks} isActive={isActive} />
|
||||
</div>
|
||||
<MobileList links={pageLinks} isActive={isActive} />
|
||||
<SheetClose asChild>
|
||||
<Button asChild size="lg">
|
||||
<Link href="/contact">
|
||||
<Phone aria-hidden="true" />
|
||||
Contact
|
||||
</Link>
|
||||
</Button>
|
||||
</SheetClose>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</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,219 +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: 500px) {
|
||||
.navbar {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0;
|
||||
|
||||
position: absolute;
|
||||
top: -14em;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: -2;
|
||||
|
||||
font-size: 1rem;
|
||||
background-color: white;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
|
||||
transition-property: top 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;
|
||||
}
|
||||
|
||||
.active {
|
||||
top: 4rem;
|
||||
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: 20em;
|
||||
transition: max-height 0.5s cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Styles for desktop view */
|
||||
@media (min-width: 501px) {
|
||||
.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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, Check } from "lucide-react";
|
||||
import HeaderSm from "./HeaderSm";
|
||||
import ContactButtons from "./ContactButtons";
|
||||
import CtaBand from "./CtaBand";
|
||||
import FaqList from "./FaqList";
|
||||
import JsonLd from "./JsonLd";
|
||||
import PriceTable from "./PriceTable";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { business, pageMetadata, serviceSchema } from "@/lib/site";
|
||||
import { services } from "@/lib/services";
|
||||
|
||||
const promises = [
|
||||
{
|
||||
title: "No fix, no fee",
|
||||
text: "If I can't fix it, you don't pay the $35 diagnosis.",
|
||||
},
|
||||
{
|
||||
title: "Price before I start",
|
||||
text: "I'll tell you what's wrong and what it costs before any work begins.",
|
||||
},
|
||||
{
|
||||
title: "You deal with me",
|
||||
text: "The person you talk to is the person fixing your computer.",
|
||||
},
|
||||
{
|
||||
title: "Local to Te Atatū",
|
||||
text: "Based on Te Atatū Peninsula, serving West Auckland and the CBD.",
|
||||
},
|
||||
];
|
||||
|
||||
export function serviceMetadata(slug) {
|
||||
const { title, description } = services[slug];
|
||||
return pageMetadata({ title, description, path: `/services/${slug}` });
|
||||
}
|
||||
|
||||
export default function ServicePage({ slug }) {
|
||||
const service = services[slug];
|
||||
const path = `/services/${slug}`;
|
||||
const testimonial = business.testimonials.find(
|
||||
({ name }) => name === service.testimonial
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<JsonLd
|
||||
data={serviceSchema({
|
||||
name: service.name,
|
||||
description: service.description,
|
||||
path,
|
||||
})}
|
||||
/>
|
||||
<HeaderSm text={service.h1} />
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
{service.intro.map((p) => (
|
||||
<p key={p} className="text-lg mb-4">
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
<ContactButtons className="mt-6" />
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-4">What I fix</h2>
|
||||
<ul className="space-y-2">
|
||||
{service.fixes.map((fix) => (
|
||||
<li key={fix} className="flex gap-2">
|
||||
<Check
|
||||
aria-hidden="true"
|
||||
className="size-5 shrink-0 mt-0.5 text-primary"
|
||||
/>
|
||||
{fix}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{service.fixesNote && <p className="mt-4">{service.fixesNote}</p>}
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-4">Pricing</h2>
|
||||
<PriceTable items={service.prices} className="mb-4" />
|
||||
<p className="mb-2">
|
||||
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
|
||||
cost before I start. If you decide not to go ahead, the $35 still
|
||||
applies.
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Labour is $35 an hour. Parts are extra.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-4">How it works</h2>
|
||||
<ul className="list-disc pl-6 space-y-2 mb-4">
|
||||
<li>
|
||||
<strong>Drop-off:</strong> bring it to {business.address.street},{" "}
|
||||
{business.address.suburb}, by appointment.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Pickup or onsite:</strong> I can collect it or come to you
|
||||
anywhere in my service area. The travel fee depends on distance.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Remote:</strong> many software problems can be fixed over
|
||||
the internet.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Most repairs are done within 24–48 hours, although this isn't
|
||||
guaranteed. Parts that need ordering usually take about a week. I
|
||||
work Monday to Friday, 9am to 6pm, and weekends by appointment.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="bg-slate-100 py-16 mb-16">
|
||||
<div className="container mx-auto max-w-3xl">
|
||||
<h2 className="text-3xl font-bold mb-6">Why choose me</h2>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{promises.map(({ title, text }) => (
|
||||
<li key={title}>
|
||||
<h3 className="font-bold text-lg mb-1">{title}</h3>
|
||||
<p>{text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{testimonial && (
|
||||
<Card className="mt-8 shadow-none">
|
||||
<CardContent>
|
||||
<figure>
|
||||
<blockquote className="mb-3">
|
||||
“{testimonial.quote}”
|
||||
</blockquote>
|
||||
<figcaption className="font-semibold">
|
||||
{testimonial.name}
|
||||
</figcaption>
|
||||
</figure>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-4">Questions</h2>
|
||||
<FaqList faqs={service.faqs} />
|
||||
</section>
|
||||
|
||||
<CtaBand />
|
||||
|
||||
<section className="container mx-auto max-w-3xl py-16">
|
||||
<h2 className="text-2xl font-bold mb-4">Related services</h2>
|
||||
<ul className="space-y-2">
|
||||
{service.related.map((related) => (
|
||||
<li key={related}>
|
||||
<Link
|
||||
href={`/services/${related}`}
|
||||
className="inline-flex items-center gap-1 font-bold text-primary hover:underline"
|
||||
>
|
||||
{services[related].name}
|
||||
<ArrowRight aria-hidden="true" className="size-4" />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+73
-3
@@ -1,11 +1,81 @@
|
||||
import Link from "next/link";
|
||||
import { Clock, Mail, MapPin } from "lucide-react";
|
||||
import HeaderSm from "../components/HeaderSm";
|
||||
import ContactButtons from "../components/ContactButtons";
|
||||
import ContactForm from "../components/ContactForm";
|
||||
import MapEmbed from "../components/MapEmbed";
|
||||
import { business, links, pageMetadata } from "@/lib/site";
|
||||
|
||||
export default function Home() {
|
||||
export const metadata = pageMetadata({
|
||||
title: "Contact & Book a Repair | Te Atatu Computers",
|
||||
description:
|
||||
"Call or text 021 107 7483, or send your device and problem details. Based on Te Atatū Peninsula, serving Henderson and West Auckland.",
|
||||
path: "/contact",
|
||||
});
|
||||
|
||||
export default function ContactPage() {
|
||||
const { address } = business;
|
||||
return (
|
||||
<div>
|
||||
<HeaderSm text="Contact" />
|
||||
<section className="container mx-auto mb-64">
|
||||
<HeaderSm text="Contact & book a repair" />
|
||||
|
||||
<section className="container mx-auto max-w-5xl mb-12">
|
||||
<p className="text-lg mb-6">
|
||||
The quickest way to reach me is a call or text. I'm{" "}
|
||||
{business.ownerName}, and I'll be the one fixing your computer.
|
||||
</p>
|
||||
<ContactButtons whatsapp />
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-5xl mb-16 grid grid-cols-1 gap-8 lg:grid-cols-2">
|
||||
<div>
|
||||
<ul className="space-y-4 mb-6">
|
||||
<li className="flex gap-3">
|
||||
<Clock aria-hidden="true" className="size-5 shrink-0 mt-0.5 text-primary" />
|
||||
<span>
|
||||
<strong className="block">Hours</strong>
|
||||
Monday to Friday, 9am to 6pm. Weekends by appointment.
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<MapPin aria-hidden="true" className="size-5 shrink-0 mt-0.5 text-primary" />
|
||||
<span>
|
||||
<strong className="block">Drop-off by appointment</strong>
|
||||
{address.street}, {address.suburb}, {address.city}{" "}
|
||||
{address.postcode}
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<Mail aria-hidden="true" className="size-5 shrink-0 mt-0.5 text-primary" />
|
||||
<span>
|
||||
<strong className="block">Email</strong>
|
||||
<a href={links.email} className="text-primary hover:underline">
|
||||
{business.email}
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mb-2">
|
||||
<strong>Onsite visits and pickup</strong> across{" "}
|
||||
{business.serviceArea.join(", ")}. The travel fee depends on
|
||||
distance.
|
||||
</p>
|
||||
<p>
|
||||
See{" "}
|
||||
<Link href="/pricing" className="text-primary underline">
|
||||
prices
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link href="/#faq" className="text-primary underline">
|
||||
common questions
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<MapEmbed height="360px" />
|
||||
</section>
|
||||
|
||||
<section id="quote" className="container mx-auto max-w-2xl mb-32 scroll-mt-20">
|
||||
<ContactForm />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
+12
-4
@@ -30,14 +30,15 @@
|
||||
---break---
|
||||
*/
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--radius: 0.5rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.129 0.042 264.695);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.129 0.042 264.695);
|
||||
--popover: oklch(1 0 0);
|
||||
--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);
|
||||
--secondary: oklch(0.968 0.007 247.896);
|
||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||
@@ -48,7 +49,7 @@
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: 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-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
@@ -103,6 +104,8 @@
|
||||
---break---
|
||||
*/
|
||||
@theme inline {
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
@@ -147,6 +150,11 @@
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
@apply bg-background text-foreground antialiased;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
@apply tracking-tight text-balance;
|
||||
}
|
||||
}
|
||||
+10
-28
@@ -3,12 +3,9 @@ import "./globals.css";
|
||||
import Navbar from "./components/Navbar";
|
||||
import Footer from "./components/Footer";
|
||||
import ChatwootWidget from "./components/ChatwootWidget";
|
||||
|
||||
// Add spyware
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { GoogleAnalytics } from "@next/third-parties/google";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import Script from "next/script";
|
||||
import JsonLd from "./components/JsonLd";
|
||||
import AnalyticsEvents from "./components/AnalyticsEvents";
|
||||
import { SITE_URL, business, localBusinessSchema } from "@/lib/site";
|
||||
|
||||
const geistSans = localFont({
|
||||
src: "./fonts/GeistVF.woff",
|
||||
@@ -21,37 +18,22 @@ const geistMono = localFont({
|
||||
weight: "100 900",
|
||||
});
|
||||
|
||||
// Pages set their own title, description and canonical via pageMetadata().
|
||||
export const metadata = {
|
||||
title: "Te Atatu Computer Repair & Web Design",
|
||||
description:
|
||||
"Your Local Experts for PCs, Laptops, Web Design, IT Support & More.",
|
||||
metadataBase: new URL(SITE_URL),
|
||||
title: business.name,
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
<html lang="en-NZ" className={`${geistSans.variable} ${geistMono.variable}`}>
|
||||
<body>
|
||||
<JsonLd data={localBusinessSchema()} />
|
||||
<ChatwootWidget />
|
||||
<Navbar />
|
||||
{children}
|
||||
<Footer />
|
||||
<Analytics />
|
||||
<SpeedInsights />
|
||||
<GoogleAnalytics gaId="G-TN7ZL1Y4CC" />
|
||||
<Script id="facebook-pixel">
|
||||
{`
|
||||
!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'https://connect.facebook.net/en_US/fbevents.js');
|
||||
fbq('init', '1370044511045020');
|
||||
fbq('track', 'PageView');
|
||||
`}
|
||||
</Script>
|
||||
<AnalyticsEvents />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+313
-310
@@ -1,16 +1,113 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import styles from "./page.module.css";
|
||||
import bgimage from "./assets/header-bg.jpg";
|
||||
import Button from "./components/Button";
|
||||
import Link from "next/link";
|
||||
import { GoogleMapsEmbed } from "@next/third-parties/google";
|
||||
import ContactButtons from "./components/ContactButtons";
|
||||
import MapEmbed from "./components/MapEmbed";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import ContactForm from "./components/ContactForm";
|
||||
import FaqList from "./components/FaqList";
|
||||
import CtaBand from "./components/CtaBand";
|
||||
import { pageMetadata, business, links } from "@/lib/site";
|
||||
import { homeFaqs } from "@/lib/faqs";
|
||||
import { areas } from "@/lib/areas";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
ArrowRight,
|
||||
Briefcase,
|
||||
Cpu,
|
||||
FolderInput,
|
||||
HardDrive,
|
||||
Laptop,
|
||||
MapPin,
|
||||
Monitor,
|
||||
MonitorCog,
|
||||
Power,
|
||||
ShieldAlert,
|
||||
Star,
|
||||
} from "lucide-react";
|
||||
|
||||
export const metadata = pageMetadata({
|
||||
title: "Computer & Laptop Repair Te Atatū | Te Atatu Computers",
|
||||
description:
|
||||
"Local laptop & PC repairs, upgrades and virus removal in Te Atatū and West Auckland. $35 diagnosis, no fix no fee. Call or text 021 107 7483.",
|
||||
path: "/",
|
||||
});
|
||||
|
||||
const commonFixes = [
|
||||
{
|
||||
icon: Laptop,
|
||||
title: "Cracked laptop screen",
|
||||
price: "from $35",
|
||||
href: "/services/computer-repair",
|
||||
},
|
||||
{
|
||||
icon: Power,
|
||||
title: "Won't turn on or won't boot",
|
||||
price: "$35 diagnosis",
|
||||
href: "/services/computer-repair",
|
||||
},
|
||||
{
|
||||
icon: HardDrive,
|
||||
title: "Slow computer? SSD upgrade",
|
||||
price: "from $45 + the drive",
|
||||
href: "/services/computer-upgrades",
|
||||
},
|
||||
{
|
||||
icon: ShieldAlert,
|
||||
title: "Viruses & scam pop-ups",
|
||||
price: "from $35",
|
||||
href: "/services/virus-removal",
|
||||
},
|
||||
{
|
||||
icon: MonitorCog,
|
||||
title: "Windows reinstall",
|
||||
price: "from $45",
|
||||
href: "/services/computer-upgrades",
|
||||
},
|
||||
{
|
||||
icon: FolderInput,
|
||||
title: "Move files to a new PC",
|
||||
price: "from $45",
|
||||
href: "/services/it-support",
|
||||
},
|
||||
{
|
||||
icon: Monitor,
|
||||
title: "New computer setup",
|
||||
price: "from $35",
|
||||
href: "/services/it-support",
|
||||
},
|
||||
{
|
||||
icon: Cpu,
|
||||
title: "Custom PC build",
|
||||
price: "from $35",
|
||||
href: "/contact",
|
||||
},
|
||||
];
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: "Get in touch",
|
||||
text: "Call, text, WhatsApp or send the form. Tell me what's going on and I'll let you know the next step.",
|
||||
},
|
||||
{
|
||||
title: "Get it to me",
|
||||
text: "Drop it off at Graham Avenue by appointment, or I can pick it up or come to you (travel fee depends on distance). Many software problems can be fixed remotely.",
|
||||
},
|
||||
{
|
||||
title: "Fixed and back to you",
|
||||
text: "$35 diagnosis, and I'll tell you the cost before I start. Most repairs are done within 24–48 hours, although this isn't guaranteed.",
|
||||
},
|
||||
];
|
||||
|
||||
function SectionCta() {
|
||||
return (
|
||||
<p className="text-center mt-8">
|
||||
<a href={links.tel} className="font-semibold text-primary hover:underline">
|
||||
Call or text {business.phone}
|
||||
</a>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
@@ -20,9 +117,8 @@ export default function Home() {
|
||||
<div className={styles.header}>
|
||||
<Image
|
||||
src={bgimage}
|
||||
alt="Header Background"
|
||||
alt=""
|
||||
placeholder="blur"
|
||||
quality={100}
|
||||
fill
|
||||
priority
|
||||
sizes="100vw"
|
||||
@@ -33,329 +129,236 @@ export default function Home() {
|
||||
<div className={styles.textbox}>
|
||||
<div className="container mx-auto">
|
||||
<div className={styles.textboxInner}>
|
||||
<h1 className="mb-4 text-6xl font-bold">
|
||||
Delivering Exceptional Service.
|
||||
<h1 className="mb-4 text-4xl md:text-6xl font-bold">
|
||||
Laptop & PC repairs in Te Atatū
|
||||
</h1>
|
||||
<p className={styles.p}>
|
||||
<strong>
|
||||
Your Local Experts for PCs, Laptops, Web Design, IT
|
||||
Support & More.
|
||||
</strong>
|
||||
<br />
|
||||
From hardware fixes to software solutions, we're here
|
||||
to help. Friendly, affordable service right in your
|
||||
neighborhood!
|
||||
<strong>No fix, no fee.</strong>{" "}A $35 diagnosis, then I'll
|
||||
tell you what it costs before I start. Local, honest help
|
||||
for West Auckland.
|
||||
</p>
|
||||
<Link href="/contact">
|
||||
<Button>
|
||||
<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"
|
||||
<ContactButtons onDark className="mb-6" />
|
||||
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
|
||||
<Star
|
||||
aria-hidden="true"
|
||||
className="size-4 fill-yellow-400 text-yellow-400"
|
||||
/>
|
||||
</svg>
|
||||
Get a Quote Now
|
||||
</Button>
|
||||
</Link>
|
||||
<a
|
||||
href={business.googleBusinessProfileUrl}
|
||||
className="underline hover:no-underline"
|
||||
>
|
||||
{business.rating.value} from {business.rating.count}{" "}
|
||||
Google reviews
|
||||
</a>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>Local to Te Atatū</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto mb-32">
|
||||
<h1 className="text-center text-3xl font-bold mb-4">
|
||||
Services we Offer
|
||||
</h1>
|
||||
<div className="text-center mb-4 flex flex-row flex-wrap justify-center">
|
||||
<div className="max-w-xs p-4 text-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
width="6rem"
|
||||
className="mx-auto mb-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25"
|
||||
/>
|
||||
</svg>
|
||||
<h2 className="text-xl font-bold mb-2">
|
||||
Computer Repair/Upgrades
|
||||
<section className="container mx-auto mb-24">
|
||||
<h2 className="text-center text-3xl font-bold mb-2">
|
||||
Common fixes & prices
|
||||
</h2>
|
||||
<p>
|
||||
We can fix anything from a broken screen to a software issue, or
|
||||
upgrade your computer's parts and optimise Windows to get
|
||||
your PC running like new again.
|
||||
<p className="text-center mb-8 text-muted-foreground">
|
||||
Labour prices. Parts are extra, and I'll always quote before
|
||||
starting.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-xs p-4 text-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{commonFixes.map(({ icon: Icon, title, price, href }) => (
|
||||
<li key={title}>
|
||||
<Link href={href} className="group block h-full rounded-xl">
|
||||
<Card className="h-full flex-row items-center gap-4 p-4 shadow-none transition-colors group-hover:border-primary">
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
className="size-7 shrink-0 text-primary"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
width="6rem"
|
||||
className="mx-auto mb-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 3v1.5M4.5 8.25H3m18 0h-1.5M4.5 12H3m18 0h-1.5m-15 3.75H3m18 0h-1.5M8.25 19.5V21M12 3v1.5m0 15V21m3.75-18v1.5m0 15V21m-9-1.5h10.5a2.25 2.25 0 0 0 2.25-2.25V6.75a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 6.75v10.5a2.25 2.25 0 0 0 2.25 2.25Zm.75-12h9v9h-9v-9Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h2 className="text-xl font-bold mb-2">Desktop PC Building</h2>
|
||||
<p>
|
||||
We'll help you pick the parts and build a custom gaming or
|
||||
workstation PC for you.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-xs p-4 text-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
width="6rem"
|
||||
className="mx-auto mb-4"
|
||||
<span>
|
||||
<span className="block font-semibold">{title}</span>
|
||||
<span className="block text-sm text-muted-foreground">
|
||||
{price}
|
||||
</span>
|
||||
</span>
|
||||
</Card>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<SectionCta />
|
||||
<Link
|
||||
href="/windows-10"
|
||||
className="mt-8 flex items-center justify-between gap-4 rounded-lg bg-sky-900 p-4 text-white transition-colors hover:bg-primary"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
<span className="block font-bold">Still on Windows 10?</span>
|
||||
<span className="block text-sm">
|
||||
See your options, with a free Windows 11 compatibility check.
|
||||
</span>
|
||||
</span>
|
||||
<ArrowRight aria-hidden="true" className="size-5 shrink-0" />
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
<h2 className="text-xl font-bold mb-2">Remote Access Support</h2>
|
||||
<p>
|
||||
We can remotely connect to your computer and fix
|
||||
software/operating system related issues from the comfort of
|
||||
your own home.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-w-xs p-4 text-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
width="6rem"
|
||||
className="mx-auto mb-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h2 className="text-xl font-bold mb-2">Web Development</h2>
|
||||
<p>
|
||||
We can develop a fast, modern and secure website for your
|
||||
business using Wordpress or custom React/NextJS code.
|
||||
</p>
|
||||
</div>
|
||||
<section className="bg-slate-100 py-16 mb-24">
|
||||
<div className="container mx-auto">
|
||||
<h2 className="text-center text-3xl font-bold mb-8">
|
||||
How it works
|
||||
</h2>
|
||||
<ol className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{steps.map(({ title, text }, i) => (
|
||||
<li key={title} className="border-t border-slate-300 pt-4">
|
||||
<span className="mb-3 flex size-8 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
|
||||
{i + 1}
|
||||
</span>
|
||||
<h3 className="text-xl font-bold mb-2">{title}</h3>
|
||||
<p>{text}</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<SectionCta />
|
||||
</div>
|
||||
</section>
|
||||
<section className="container mx-auto flex flex-col gap-8 lg:flex-row mb-32">
|
||||
<ContactForm />
|
||||
|
||||
<section className="container mx-auto mb-24 max-w-3xl text-center">
|
||||
<h2 className="text-3xl font-bold mb-4">
|
||||
Hi, I'm {business.ownerName}
|
||||
</h2>
|
||||
<p className="text-lg mb-4">
|
||||
I run Te Atatu Computers from Te Atatū Peninsula. When you call or
|
||||
text, you're talking to the person who will actually fix your
|
||||
computer. There's no call centre and no being passed around.
|
||||
</p>
|
||||
<p className="text-lg">
|
||||
I'll explain what's wrong in plain language and tell you
|
||||
what it will cost before I start. If I can't fix it, you
|
||||
don't pay the diagnosis fee.
|
||||
</p>
|
||||
<SectionCta />
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto mb-24">
|
||||
<h2 className="text-center text-3xl font-bold mb-8">
|
||||
What customers say
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
|
||||
{business.testimonials.map(({ name, quote }) => (
|
||||
<Card key={name} className="shadow-none">
|
||||
<CardContent>
|
||||
<figure>
|
||||
<blockquote className="mb-3">“{quote}”</blockquote>
|
||||
<figcaption className="font-semibold">{name}</figcaption>
|
||||
</figure>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-center mt-8">
|
||||
<a
|
||||
href={business.googleBusinessProfileUrl}
|
||||
className="inline-flex items-center gap-1 font-semibold text-primary hover:underline"
|
||||
>
|
||||
Read all reviews on Google
|
||||
<ArrowRight aria-hidden="true" className="size-4" />
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto mb-24 flex flex-col gap-8 lg:flex-row">
|
||||
<div className="w-full">
|
||||
<GoogleMapsEmbed
|
||||
apiKey="AIzaSyDbgQY55TQY5CefsCUwKbc8RfoOUe8U5gY"
|
||||
width="100%"
|
||||
height="600px"
|
||||
mode="place"
|
||||
q="place_id:ChIJz0ZoDYhADW0R0TDgWoWMq0M"
|
||||
/>
|
||||
<h2 className="text-3xl font-bold mb-4">Areas I serve</h2>
|
||||
<p className="mb-4 flex items-start gap-2">
|
||||
<MapPin aria-hidden="true" className="size-5 shrink-0 mt-0.5" />
|
||||
<span>
|
||||
Based at {business.address.street}, {business.address.suburb}.
|
||||
Drop-offs by appointment.
|
||||
</span>
|
||||
</p>
|
||||
<p className="mb-4">
|
||||
Onsite visits, pickup and return, and remote support across:
|
||||
</p>
|
||||
<ul className="grid grid-cols-2 gap-2 mb-6 list-disc pl-6">
|
||||
{business.serviceArea.map((area) => {
|
||||
const page = areas.find(({ name }) => name === area);
|
||||
return (
|
||||
<li key={area}>
|
||||
{page ? (
|
||||
<Link
|
||||
href={`/areas/${page.slug}`}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{area}
|
||||
</Link>
|
||||
) : (
|
||||
area
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<p>
|
||||
Not on the list? Get in touch and I'll let you know if I can
|
||||
help.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
<a
|
||||
href={links.tel}
|
||||
className="font-semibold text-primary hover:underline"
|
||||
>
|
||||
Call or text {business.phone}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<MapEmbed />
|
||||
</div>
|
||||
</section>
|
||||
<section className="container mx-auto mb-16 max-w-3xl mb-32 scroll-mt-20" id="faq">
|
||||
<h1 className="text-center text-3xl font-bold mb-8">
|
||||
Frequently Asked Questions
|
||||
</h1>
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Computer Repair and IT Support
|
||||
</h3>
|
||||
<Accordion className="mb-8">
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>
|
||||
What types of devices do you repair?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
We repair desktops, laptops, chromebooks and PCs. We also handle
|
||||
custom-built systems and some tablets, depending on the issue.
|
||||
Currently we do not repair Apple devices.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>
|
||||
What kinds of issues can you fix?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
We handle a wide range of problems, including (but not limited
|
||||
to):
|
||||
<ul className="list-disc pl-6 my-2">
|
||||
<li>
|
||||
Software issues (e.g. Windows errors, slow performance)
|
||||
</li>
|
||||
<li>Virus removal and protection</li>
|
||||
<li>Data recovery and backup solutions</li>
|
||||
<li>Network setup and troubleshooting</li>
|
||||
<li>Hardware upgrades (e.g. RAM, SSD, graphics card)</li>
|
||||
<li>Custom PC builds</li>
|
||||
<li>Web development and design</li>
|
||||
<li>Remote support for software issues</li>
|
||||
<li>General IT support and consulting</li>
|
||||
<li>Printer setup and troubleshooting</li>
|
||||
<li>Software installation and configuration</li>
|
||||
<li>Operating system installation and upgrades</li>
|
||||
<li>Data migration from old devices to new ones</li>
|
||||
</ul>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>
|
||||
Do I need an appointment, or can I just walk in?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Currently we are operating by appointment only. Please contact
|
||||
us to schedule a time that works for you. We can also arrange
|
||||
remote support for certain issues.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-4">
|
||||
<AccordionTrigger>
|
||||
How long will the repair take?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Most basic repairs are completed within 24-48 hours. Some issues
|
||||
may take longer, depending on parts availability and complexity.
|
||||
We’ll give you an estimated turnaround time when we diagnose the
|
||||
problem. Most parts take about a week to arrive.
|
||||
</AccordionContent>
|
||||
<AccordionItem value="item-5">
|
||||
<AccordionTrigger>
|
||||
What payment methods do you accept?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
We accept cash, card (contactless ONLY), bank transfer and Bitcoin. Currently we do not accept Eftpos payments, only contactless payment methods such as Visa, Mastercard, American Express, Apple Pay, Google Pay and Samsung Pay.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<h3 className="text-lg font-bold text-slate-900">
|
||||
Web Design and Development
|
||||
</h3>
|
||||
<Accordion>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger>
|
||||
What platforms do you build websites with?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
We specialize in modern web development using WordPress for
|
||||
content-driven sites and React/Next.js for high-performance,
|
||||
custom web applications. We also offer support for almost any
|
||||
website builder, whether it's Wix, Oddo, Squarespace, or
|
||||
anything else. Whether you need a blog, business site,
|
||||
portfolio, or a fully custom web app—we've got you covered.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<section className="container mx-auto mb-24">
|
||||
<Card className="flex-col gap-6 p-6 shadow-none md:flex-row md:items-center md:p-8">
|
||||
<Briefcase
|
||||
aria-hidden="true"
|
||||
className="size-12 shrink-0 text-primary"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold mb-2">For local businesses</h2>
|
||||
<p>
|
||||
IT support for small businesses: Wi-Fi and networks, email,
|
||||
backups, security and new PC setup. Onsite or remote, $35 an
|
||||
hour, or $65 a month for 2 hours of support.
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/business"
|
||||
className="inline-flex items-center gap-1 font-semibold text-primary hover:underline"
|
||||
>
|
||||
Business IT plans
|
||||
<ArrowRight aria-hidden="true" className="size-4" />
|
||||
</Link>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger>
|
||||
How do I choose between WordPress and React/Next.js?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
It depends on your needs:
|
||||
<ul className="list-disc pl-6 my-2">
|
||||
<li>
|
||||
<strong>WordPress</strong> is best for content-heavy
|
||||
websites like blogs, portfolios, and marketing sites.
|
||||
</li>
|
||||
<li>
|
||||
<strong>React/Next.js</strong> is ideal for fast, scalable,
|
||||
custom web applications or interactive user interfaces.
|
||||
</li>
|
||||
</ul>
|
||||
We’ll help you decide what works best based on your goals and
|
||||
budget.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger>
|
||||
Do you design the website too, or just build it?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
We do both! We offer full-service web design and development.
|
||||
This includes UX/UI design, branding, responsive layouts, and
|
||||
mobile-first design—all tailored to your needs.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-4">
|
||||
<AccordionTrigger>
|
||||
Can you redesign or improve my existing site?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Absolutely. We can modernize your design, improve performance,
|
||||
migrate platforms, or fully rebuild your site with a better
|
||||
structure and codebase.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-5">
|
||||
<AccordionTrigger>
|
||||
Do you offer SEO and performance optimization?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes! All our builds include basic SEO best practices and speed
|
||||
optimization. We also offer advanced technical SEO, Core Web
|
||||
Vitals tuning, and image/code optimization.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-6">
|
||||
<AccordionTrigger>
|
||||
Do you provide ongoing maintenance and support?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Yes, we offer monthly maintenance plans for updates, backups,
|
||||
security checks, and minor edits. We can also provide on-demand
|
||||
support when needed.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-7">
|
||||
<AccordionTrigger>
|
||||
Will my site work on mobile devices?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
Definitely. Every site we build is fully responsive and tested
|
||||
on a variety of devices to ensure it looks great and works well
|
||||
on phones, tablets, and desktops.
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-8">
|
||||
<AccordionTrigger>
|
||||
What do you need from me to get started?
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
We’ll need some info about your goals, brand, content (text,
|
||||
images, logo), and examples of sites you like. Don’t worry—we’ll
|
||||
guide you through the process!
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<section
|
||||
className="container mx-auto max-w-3xl mb-24 scroll-mt-20"
|
||||
id="faq"
|
||||
>
|
||||
<h2 className="text-center text-3xl font-bold mb-8">
|
||||
Frequently asked questions
|
||||
</h2>
|
||||
<FaqList faqs={homeFaqs} />
|
||||
<SectionCta />
|
||||
</section>
|
||||
|
||||
<CtaBand quoteHref="#quote" />
|
||||
<section
|
||||
id="quote"
|
||||
className="container mx-auto max-w-2xl py-16 scroll-mt-20"
|
||||
>
|
||||
<ContactForm />
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.header {
|
||||
position: relative;
|
||||
height: 50em;
|
||||
height: 32em;
|
||||
margin-bottom: 3.5em;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import Link from "next/link";
|
||||
import HeaderSm from "../components/HeaderSm";
|
||||
import CtaBand from "../components/CtaBand";
|
||||
import PriceTable from "../components/PriceTable";
|
||||
import { business, pageMetadata, priceList } from "@/lib/site";
|
||||
|
||||
export const metadata = pageMetadata({
|
||||
title: "Computer Repair Prices | Te Atatu Computers",
|
||||
description:
|
||||
"Clear prices for laptop & PC repairs in Te Atatū: $35 diagnosis (no fix, no fee), $35/hr labour, SSD upgrades from $45, Windows reinstalls from $45.",
|
||||
path: "/pricing",
|
||||
});
|
||||
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
<div>
|
||||
<HeaderSm text="Prices" />
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<p className="text-lg mb-4">
|
||||
Every job starts with a $35 diagnosis. Then I'll tell you
|
||||
what's wrong and what the repair will cost before I start.
|
||||
Labour is $35 an hour. The prices below are for labour, and parts
|
||||
are extra.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{priceList.map(({ group, items }) => (
|
||||
<section key={group} className="container mx-auto max-w-3xl mb-12">
|
||||
<h2 className="text-2xl font-bold mb-4">{group}</h2>
|
||||
<PriceTable items={items} />
|
||||
</section>
|
||||
))}
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-12">
|
||||
<h2 className="text-2xl font-bold mb-4">No fix, no fee</h2>
|
||||
<ul className="list-disc pl-6 space-y-2">
|
||||
<li>If I can't fix it, you don't pay the $35 diagnosis.</li>
|
||||
<li>
|
||||
If I can fix it but you decide not to go ahead, the $35 diagnosis
|
||||
fee applies.
|
||||
</li>
|
||||
<li>
|
||||
I'll always tell you the cost before starting a repair.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-12">
|
||||
<h2 className="text-2xl font-bold mb-4">Onsite visits & pickup</h2>
|
||||
<p>
|
||||
Drop-off at {business.address.street}, {business.address.suburb},
|
||||
by appointment, has no travel fee. For onsite visits or pickup and
|
||||
return, the travel fee depends on distance. Get in touch with your
|
||||
suburb for a quote. Small businesses can also get{" "}
|
||||
<Link href="/business" className="text-primary underline">
|
||||
2 hours of support a month for $65
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-2xl font-bold mb-4">Payment methods</h2>
|
||||
<ul className="list-disc pl-6 space-y-1 mb-2">
|
||||
{business.paymentMethods.map((method) => (
|
||||
<li key={method}>{method}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>I don't accept EFTPOS.</p>
|
||||
</section>
|
||||
|
||||
<CtaBand />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { business } from "@/lib/site";
|
||||
|
||||
// Short link to text customers after a job: teatatucomputers.com/review
|
||||
export function GET() {
|
||||
return Response.redirect(business.googleReviewUrl, 307);
|
||||
}
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
import { SITE_URL } from "@/lib/site";
|
||||
|
||||
export default function robots() {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: "/private/",
|
||||
},
|
||||
sitemap: "https://www.teatatucomputers.com/sitemap.xml",
|
||||
sitemap: `${SITE_URL}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -1,33 +1,7 @@
|
||||
import HeaderSm from "../../components/HeaderSm";
|
||||
import ServicePage, { serviceMetadata } from "../../components/ServicePage";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div style={{ marginBottom: "30em" }}>
|
||||
<HeaderSm text="Computer & Laptop Repairs" />
|
||||
<section className="container mx-auto mb">
|
||||
<h1 className="text-2xl mb-4">Profesional Computer Repairs</h1>
|
||||
<p>
|
||||
Te Atatu Computers is your trusted destination for expert laptop repairs
|
||||
in Auckland. We specialize in providing top-quality repair services,
|
||||
tackling a wide range of issues—from routine maintenance to intricate
|
||||
laptop screen repairs. With a team of skilled technicians armed with
|
||||
advanced tools and extensive experience, we ensure every repair is
|
||||
carried out with precision and care. Our dedication to excellence has
|
||||
made us the preferred choice for laptop repairs in Auckland. Whether
|
||||
you're dealing with a cracked screen, a faulty keyboard, or software
|
||||
issues, we excel in diagnosing and resolving all types of laptop
|
||||
challenges. We're committed to offering fast, reliable, and
|
||||
cost-effective solutions, ensuring your device is returned in perfect
|
||||
working order. In addition to laptops, we handle PC, Mac, and hardware
|
||||
repairs, covering issues like viruses, spyware, and software
|
||||
malfunctions. From component replacements to motherboard
|
||||
repairs—thanks to our in-house motherboard specialist—Te Atatu Computers
|
||||
is equipped to address any problem with efficiency and
|
||||
professionalism. Choose Te Atatu Computers for a seamless repair
|
||||
experience where quality craftsmanship and customer satisfaction are
|
||||
always our top priorities.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
export const metadata = serviceMetadata("computer-repair");
|
||||
|
||||
export default function Page() {
|
||||
return <ServicePage slug="computer-repair" />;
|
||||
}
|
||||
@@ -1,27 +1,7 @@
|
||||
import HeaderSm from "../../components/HeaderSm";
|
||||
import ServicePage, { serviceMetadata } from "../../components/ServicePage";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div style={{ marginBottom: "30em" }}>
|
||||
<HeaderSm text="Computer & Laptop Upgrades/Speedup" />
|
||||
<section className="container mx-auto mb">
|
||||
<h1 className="text-2xl mb-4">Computer & Laptop Upgrades/Speedup</h1>
|
||||
<p>
|
||||
Upgrading a slow computer can significantly improve its performance or
|
||||
storage capacity and extend its lifespan. At Te Atatu Computers, we do
|
||||
profesional computer upgrades, wheather you need more storage or have
|
||||
a slow device. Increasing the RAM allows your system to handle more
|
||||
tasks simultaneously, reducing lag and boosting speed. Replacing an
|
||||
old hard drive (HDD) with a solid-state drive (SSD) can dramatically
|
||||
decrease boot times and make file access much faster, thanks to the
|
||||
SSD's superior data transfer speeds. Additionally, optimizing your
|
||||
Windows operating system—by removing unnecessary startup programs,
|
||||
updating drivers, and cleaning up system files—can further enhance
|
||||
your computer's responsiveness. These upgrades are cost-effective
|
||||
solutions to breathe new life into your device and improve your
|
||||
productivity.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
export const metadata = serviceMetadata("computer-upgrades");
|
||||
|
||||
export default function Page() {
|
||||
return <ServicePage slug="computer-upgrades" />;
|
||||
}
|
||||
@@ -1,33 +1,7 @@
|
||||
import HeaderSm from "../../components/HeaderSm";
|
||||
import ServicePage, { serviceMetadata } from "../../components/ServicePage";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div style={{ marginBottom: "30em" }}>
|
||||
<HeaderSm text="IT Support" />
|
||||
<section className="container mx-auto mb">
|
||||
<h1 className="text-2xl mb-4">IT Support</h1>
|
||||
<p>
|
||||
At Te Atatu Computers, we specialize in providing personalized IT
|
||||
support tailored to the needs of individuals and small businesses.
|
||||
Whether you're dealing with a sluggish computer, software glitches, or
|
||||
networking issues, our experienced technicians are here to help. We
|
||||
offer on-site and remote support, ensuring minimal disruption to your
|
||||
day-to-day activities. From troubleshooting and repairs to software
|
||||
installations and system upgrades, we provide comprehensive solutions
|
||||
to keep your technology running smoothly. Our goal is to simplify
|
||||
technology for you, so you can focus on what matters most. <br /><br />
|
||||
For small businesses, we understand that reliable IT systems are
|
||||
essential for productivity and growth. Te Atatu Computers offers a
|
||||
range of services, including network setup, data backup solutions, and
|
||||
cybersecurity protection, to safeguard your operations. We can also
|
||||
assist with cloud services and ensure your systems are up to date for
|
||||
maximum efficiency. Our team takes the time to understand your unique
|
||||
business requirements, providing proactive support and expert advice
|
||||
to help you thrive in today's digital landscape. With Te Atatu
|
||||
Computers, you get more than just IT support—you gain a trusted
|
||||
technology partner.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
export const metadata = serviceMetadata("it-support");
|
||||
|
||||
export default function Page() {
|
||||
return <ServicePage slug="it-support" />;
|
||||
}
|
||||
@@ -1,25 +1,7 @@
|
||||
import HeaderSm from "../../components/HeaderSm";
|
||||
import ServicePage, { serviceMetadata } from "../../components/ServicePage";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div style={{ marginBottom: "30em" }}>
|
||||
<HeaderSm text="Virus Removal" />
|
||||
<section className="container mx-auto mb">
|
||||
<h1 className="text-2xl mb-4">Virus & Malware Removal</h1>
|
||||
<p>
|
||||
Te Atatu Computers specializes in efficient and thorough virus removal
|
||||
services for Windows devices, ensuring your computer is free from
|
||||
harmful malware, spyware, and ransomware. Our experienced technicians
|
||||
use advanced tools to identify and eliminate threats while
|
||||
safeguarding your valuable data. We also perform a comprehensive
|
||||
system check to ensure there are no lingering vulnerabilities,
|
||||
restoring your device to optimal performance. Additionally, we provide
|
||||
advice on enhancing your system's security, including installing
|
||||
reliable antivirus software and adopting safe browsing habits, to help
|
||||
prevent future infections. With Te Atatu Computers, you can trust your
|
||||
device is in expert hands.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
export const metadata = serviceMetadata("virus-removal");
|
||||
|
||||
export default function Page() {
|
||||
return <ServicePage slug="virus-removal" />;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import HeaderSm from "../../components/HeaderSm";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div style={{ marginBottom: "30em" }}>
|
||||
<HeaderSm text="Web Design" />
|
||||
<section className="container mx-auto mb">
|
||||
<h1 className="text-2xl mb-4">Profesional Web Design</h1>
|
||||
<p>
|
||||
Te Atatu Computers offers comprehensive web design and development
|
||||
services tailored to meet the unique needs of individuals and
|
||||
businesses. Whether you're creating a brand-new website or need expert
|
||||
assistance maintaining an existing one, we are here to help. Our team
|
||||
works with popular platforms like WordPress, Wix, and Odoo to build
|
||||
visually appealing and user-friendly websites. These platforms offer a
|
||||
flexible and robust foundation for creating sites that are easy to
|
||||
manage and designed to showcase your brand effectively. We ensure your
|
||||
website is responsive, functional, and optimized for both desktop and
|
||||
mobile users, giving you the edge in today's competitive online
|
||||
landscape. <br /><br />
|
||||
|
||||
For those who require custom solutions, Te Atatu Computers
|
||||
specializes in advanced web development using modern frameworks such
|
||||
as React, NextJS, and Vue. We also leverage tools like Tailwind CSS
|
||||
and standard coding languages like HTML, CSS, and JavaScript to create
|
||||
fully customized, high-performance websites tailored to your specific
|
||||
requirements. Whether you need e-commerce functionality, seamless
|
||||
integrations, or custom features, we can deliver. Additionally, we
|
||||
provide ongoing maintenance services to ensure your website remains
|
||||
secure, up-to-date, and operating at peak performance. With Te Atatu
|
||||
Computers, you gain a reliable partner dedicated to bringing your
|
||||
online vision to life.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+12
-19
@@ -1,22 +1,15 @@
|
||||
import { SITE_URL, routes } from "@/lib/site";
|
||||
import { areas } from "@/lib/areas";
|
||||
|
||||
export default function sitemap() {
|
||||
return [
|
||||
{
|
||||
url: 'https://www.teatatucomputers.com/',
|
||||
const areaRoutes = areas.map(({ slug }) => ({
|
||||
path: `/areas/${slug}`,
|
||||
priority: 0.6,
|
||||
}));
|
||||
return [...routes, ...areaRoutes].map(({ path, priority }) => ({
|
||||
url: `${SITE_URL}${path}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: 'https://www.teatatucomputers.com/services',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.5,
|
||||
},
|
||||
{
|
||||
url: 'https://www.teatatucomputers.com/contact',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.5,
|
||||
},
|
||||
]
|
||||
changeFrequency: "monthly",
|
||||
priority,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import Link from "next/link";
|
||||
import HeaderSm from "../components/HeaderSm";
|
||||
import ContactButtons from "../components/ContactButtons";
|
||||
import CtaBand from "../components/CtaBand";
|
||||
import FaqList from "../components/FaqList";
|
||||
import { pageMetadata } from "@/lib/site";
|
||||
|
||||
export const metadata = pageMetadata({
|
||||
title: "Still on Windows 10? What to Do Now | Te Atatu Computers",
|
||||
description:
|
||||
"Windows 10 no longer gets regular updates. Free Windows 11 compatibility check, upgrades, SSD refreshes and file transfers in Te Atatū & West Auckland.",
|
||||
path: "/windows-10",
|
||||
});
|
||||
|
||||
const options = [
|
||||
{
|
||||
title: "Upgrade to Windows 11",
|
||||
text: "If your computer is eligible, upgrading to Windows 11 keeps it getting security updates. I'll check whether your computer can run it for free, and upgrades start from $35.",
|
||||
},
|
||||
{
|
||||
title: "Upgrade and speed it up",
|
||||
text: "Many older computers that can run Windows 11 are held back by a slow hard drive. Upgrading to an SSD at the same time (from $45 + the drive) can make it feel new.",
|
||||
},
|
||||
{
|
||||
title: "Move to a new computer",
|
||||
text: "If your computer can't run Windows 11, or it isn't worth upgrading, I can set up a new one and move your files, photos and email across. File transfers start from $45 and setup from $35.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "Can my computer run Windows 11?",
|
||||
answer:
|
||||
"Some older computers can't officially run Windows 11. I'll check yours for free and tell you your options.",
|
||||
},
|
||||
{
|
||||
question: "Is my Windows 10 computer still safe to use?",
|
||||
answer:
|
||||
"It will keep working, but without regular security updates it becomes more exposed to viruses and scams over time. That matters most for online banking, email and shopping.",
|
||||
},
|
||||
{
|
||||
question: "What are Extended Security Updates?",
|
||||
answer:
|
||||
"Microsoft sells Extended Security Updates for Windows 10. Businesses can buy them until October 2028. Home users have a different, shorter programme. Check Microsoft's Windows 10 ESU page, or ask me and I'll check what applies to you.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Windows10Page() {
|
||||
return (
|
||||
<div>
|
||||
<HeaderSm text="Still on Windows 10? What to do now" />
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<p className="text-lg mb-4">
|
||||
Microsoft no longer supports Windows 10 with regular updates. Your
|
||||
computer will keep working, but without security updates it gets
|
||||
riskier to use over time.
|
||||
</p>
|
||||
<p className="text-lg mb-6">
|
||||
The good news is that you probably don't need to rush out and
|
||||
buy a new computer. Here are your options, and I'm happy to
|
||||
check which one suits you for free.
|
||||
</p>
|
||||
<ContactButtons textLabel="Text for a free check" />
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-6">Your options</h2>
|
||||
<ol className="space-y-6">
|
||||
{options.map(({ title, text }, i) => (
|
||||
<li key={title} className="flex gap-4">
|
||||
<span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-primary font-bold text-white">
|
||||
{i + 1}
|
||||
</span>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-1">{title}</h3>
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
<p className="mt-6">
|
||||
Not sure? Get in touch and I'll give you a straight answer. See
|
||||
all prices on the{" "}
|
||||
<Link href="/pricing" className="text-primary underline">
|
||||
pricing page
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="container mx-auto max-w-3xl mb-16">
|
||||
<h2 className="text-3xl font-bold mb-4">Questions</h2>
|
||||
<FaqList faqs={faqs} />
|
||||
</section>
|
||||
|
||||
<CtaBand />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
|
||||
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 { cn } from "@/lib/utils"
|
||||
import { cn } from "cn"
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
@@ -34,7 +34,7 @@ function AccordionTrigger({
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
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
|
||||
)}
|
||||
{...props}>
|
||||
@@ -52,9 +52,12 @@ function AccordionContent({
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
// forceMount keeps closed answers in the server-rendered HTML (needed for
|
||||
// FAQPage schema); data-[state=closed]:hidden keeps them visually collapsed.
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
forceMount
|
||||
className="data-[state=closed]:hidden data-[state=open]:animate-accordion-down overflow-hidden text-base text-muted-foreground"
|
||||
{...props}>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as React from "react"
|
||||
import { cva } from "class-variance-authority";
|
||||
import { cn } from "cn"
|
||||
|
||||
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 "cn"
|
||||
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 "cn"
|
||||
|
||||
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 "cn"
|
||||
|
||||
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 "cn"
|
||||
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 "cn"
|
||||
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 "cn"
|
||||
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 "cn"
|
||||
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 "cn"
|
||||
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 "cn"
|
||||
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 "cn"
|
||||
|
||||
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 "cn"
|
||||
|
||||
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 }
|
||||
@@ -0,0 +1,18 @@
|
||||
// Sends a custom event to Plausible if its script is installed; otherwise does nothing.
|
||||
// Events: click_call, click_text, click_email, submit_quote_form, click_google_reviews.
|
||||
export function trackEvent(name) {
|
||||
if (typeof window !== "undefined" && typeof window.plausible === "function") {
|
||||
window.plausible(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Maps a clicked link to its event name, or null if it isn't a contact link.
|
||||
export function eventForHref(href = "") {
|
||||
if (href.startsWith("tel:")) return "click_call";
|
||||
if (href.startsWith("sms:") || href.startsWith("https://wa.me/"))
|
||||
return "click_text";
|
||||
if (href.startsWith("mailto:")) return "click_email";
|
||||
if (href.startsWith("https://share.google/") || href.startsWith("https://g.page/"))
|
||||
return "click_google_reviews";
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Area pages at /areas/[slug]. Only add suburbs from the owner's service area,
|
||||
// and give each one genuinely local content (not just a swapped name).
|
||||
|
||||
export const areas = [
|
||||
{
|
||||
slug: "te-atatu-peninsula",
|
||||
name: "Te Atatū Peninsula",
|
||||
intro:
|
||||
"I'm based on Graham Avenue, right here on Te Atatū Peninsula, so if you live on the Peninsula your local computer repairer is just down the road. Cracked laptop screen, a PC that won't start, a slow computer or a scam pop-up: bring it round, or I'll come to you.",
|
||||
gettingThere:
|
||||
"For Peninsula locals, dropping your computer off is usually the quickest and cheapest option. Book a time, bring it to 34 Graham Avenue, and I'll tell you what's wrong and what it costs before I start. If you can't get out, I can do a home visit or collect it. Because you're close by, the travel fee is at the low end.",
|
||||
faq: {
|
||||
question: "Can I drop my laptop off today?",
|
||||
answer:
|
||||
"Drop-offs are by appointment, so call or text 021 107 7483 and we'll find a time that suits you.",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "te-atatu-south",
|
||||
name: "Te Atatū South",
|
||||
intro:
|
||||
"Te Atatū South is just across the Northwestern Motorway from my base on Te Atatū Peninsula. I fix laptops and PCs, upgrade slow computers, remove viruses and help with everyday IT for homes and small businesses in Te Atatū South.",
|
||||
gettingThere:
|
||||
"Most Te Atatū South customers drop their computer off at 34 Graham Avenue on the Peninsula, by appointment. If it's easier, I can come to you or collect it and bring it back. The travel fee depends on distance, so ask for a quote with your street name.",
|
||||
faq: {
|
||||
question: "Do you do home visits in Te Atatū South?",
|
||||
answer:
|
||||
"Yes. I can come to your home or business in Te Atatū South for onsite support, or collect your computer and return it once it's fixed. There's a travel fee based on distance. Get in touch for a quote.",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "henderson",
|
||||
name: "Henderson",
|
||||
intro:
|
||||
"Need a computer repaired in Henderson? I fix Windows laptops, desktop PCs and Chromebooks for Henderson homes and businesses, from screen and battery replacements to virus removal, SSD upgrades and Wi-Fi problems.",
|
||||
gettingThere:
|
||||
"You can drop your computer off at my base on Te Atatū Peninsula by appointment, or I can come to you in Henderson, or collect and return it. Many software problems, like email, slow Windows or pop-ups, can be fixed remotely, so you might not need to go anywhere.",
|
||||
faq: {
|
||||
question: "Is it worth coming from Henderson to Te Atatū?",
|
||||
answer:
|
||||
"It's a short drive, and drop-off avoids a travel fee. If that doesn't suit, I can come to you or collect the computer for a distance-based fee, or fix software problems remotely.",
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: "massey",
|
||||
name: "Massey",
|
||||
intro:
|
||||
"I help Massey homes and small businesses with laptop and PC repairs, upgrades, virus removal and IT support. You deal with me directly: one local person who'll tell you what's wrong and what it costs before starting.",
|
||||
gettingThere:
|
||||
"Massey customers can drop off at 34 Graham Avenue, Te Atatū Peninsula, by appointment. I can also do onsite visits or pickup and return in Massey for a travel fee based on distance. Remote support is available for software problems.",
|
||||
faq: {
|
||||
question: "Can you help my Massey business with IT?",
|
||||
answer:
|
||||
"Yes. I support small businesses with networks, email, Microsoft 365 and Google Workspace, backups and new computers. That's $35 an hour, or $65 a month for 2 hours of support.",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function getArea(slug) {
|
||||
return areas.find((area) => area.slug === slug);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Homepage FAQs. Rendered in the accordion and in FAQPage JSON-LD, so keep
|
||||
// answers as plain text (plus an optional bullet list).
|
||||
|
||||
export const homeFaqs = [
|
||||
{
|
||||
question: "What types of devices do you repair?",
|
||||
answer:
|
||||
"I repair Windows laptops and desktop PCs, Chromebooks and custom-built PCs, plus some tablets depending on the problem. I don't repair Apple devices.",
|
||||
},
|
||||
{
|
||||
question: "What kinds of problems can you fix?",
|
||||
answer: "Most things that go wrong with a PC or laptop, including:",
|
||||
list: [
|
||||
"Cracked or broken laptop screens",
|
||||
"Computers that won't turn on or won't start Windows",
|
||||
"Slow computers (SSD and RAM upgrades, Windows clean-ups)",
|
||||
"Viruses, malware and scam pop-ups",
|
||||
"Windows errors, reinstalls and upgrades",
|
||||
"Data recovery, backups and moving files to a new computer",
|
||||
"Wi-Fi, network and printer problems",
|
||||
"Software installation and setup",
|
||||
"Custom PC builds",
|
||||
],
|
||||
},
|
||||
{
|
||||
question: "How much does a repair cost?",
|
||||
answer:
|
||||
"A diagnosis is $35. If I can't fix it, you don't pay the $35. Once I know what's wrong, I'll tell you what the repair will cost before I start. Labour is $35 an hour, and parts are extra. If you decide not to go ahead with the repair, the $35 diagnosis fee still applies.",
|
||||
},
|
||||
{
|
||||
question: "Do I need an appointment, or can I just walk in?",
|
||||
answer:
|
||||
"Drop-offs are by appointment at 34 Graham Avenue, Te Atatū Peninsula. I'm available Monday to Friday, 9am to 6pm, and weekends by appointment. Call or text 021 107 7483 to book a time.",
|
||||
},
|
||||
{
|
||||
question: "Can you come to me, or pick up my computer?",
|
||||
answer:
|
||||
"Yes. I do onsite visits and pickup and return across West Auckland and the Auckland CBD. The travel fee depends on distance, so get in touch for a quote. Many software problems can also be fixed remotely, without you leaving home.",
|
||||
},
|
||||
{
|
||||
question: "How long will the repair take?",
|
||||
answer:
|
||||
"Most repairs are done within 24–48 hours, although this isn't guaranteed. If a part needs ordering it usually takes about a week to arrive. I'll give you an estimated turnaround once I've diagnosed the problem.",
|
||||
},
|
||||
{
|
||||
question: "What payment methods do you accept?",
|
||||
answer:
|
||||
"Cash, NZ bank transfer and Bitcoin, plus contactless card payments: Visa, Mastercard, American Express, Apple Pay, Google Pay and Samsung Pay. I don't accept EFTPOS.",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,212 @@
|
||||
// Content for the service pages, rendered by components/ServicePage.js.
|
||||
// Only owner-confirmed facts: see PLAN.md owner inputs before adding claims.
|
||||
|
||||
export const services = {
|
||||
"computer-repair": {
|
||||
name: "Computer & Laptop Repair",
|
||||
title: "Laptop & PC Repair Te Atatū & West Auckland",
|
||||
description:
|
||||
"Cracked screens, dead laptops, PCs that won't boot and Windows faults. Upfront pricing and honest advice from your local tech in Te Atatū.",
|
||||
h1: "Laptop & PC Repair in Te Atatū & West Auckland",
|
||||
intro: [
|
||||
"Laptop won't turn on? Screen cracked, keyboard dead, or Windows stuck on a loading screen? I fix Windows laptops, desktop PCs and Chromebooks from Te Atatū Peninsula, for people all over West Auckland.",
|
||||
"You'll deal with me directly, from the first call to handing your computer back. I'll find out what's wrong, tell you the cost, and only start once you're happy with the price.",
|
||||
],
|
||||
fixes: [
|
||||
"Cracked, flickering or black laptop screens",
|
||||
"Laptops and PCs that won't turn on or won't start Windows",
|
||||
"Batteries that won't hold charge",
|
||||
"Laptops that won't charge, including faulty charging ports",
|
||||
"Broken or unresponsive keyboards",
|
||||
"Overheating, loud fans and sudden shutdowns (dust clean, fan replacement, fresh thermal paste)",
|
||||
"Liquid spills: clean-up and assessment",
|
||||
"Windows errors, blue screens and failed updates",
|
||||
"Recovering files from a computer that won't start",
|
||||
],
|
||||
fixesNote:
|
||||
"I don't repair Apple devices. That includes MacBooks, iMacs, iPhones and iPads.",
|
||||
prices: [
|
||||
{ item: "Diagnosis", price: "$35" },
|
||||
{ item: "Laptop screen replacement", price: "from $35 + the screen" },
|
||||
{ item: "Battery, charging port or keyboard", price: "from $35 + parts" },
|
||||
{ item: "Other repairs (labour)", price: "$35 an hour" },
|
||||
{ item: "Windows reinstall", price: "from $45" },
|
||||
],
|
||||
faqs: [
|
||||
{
|
||||
question: "Do you repair MacBooks or other Apple devices?",
|
||||
answer:
|
||||
"No. I only work on Windows laptops and PCs, Chromebooks and custom-built computers.",
|
||||
},
|
||||
{
|
||||
question: "What happens if you can't fix it?",
|
||||
answer:
|
||||
"You don't pay the $35 diagnosis fee. If I can fix it but you decide not to go ahead, the $35 is charged.",
|
||||
},
|
||||
{
|
||||
question: "How long does a repair take?",
|
||||
answer:
|
||||
"Most repairs are done within 24–48 hours, although this isn't guaranteed. If a part such as a screen or battery needs ordering, it usually takes about a week to arrive. I'll give you an estimate after the diagnosis.",
|
||||
},
|
||||
{
|
||||
question: "My laptop got wet. What should I do?",
|
||||
answer:
|
||||
"Turn it off straight away, unplug the charger and don't try to switch it back on. Get in touch as soon as you can so I can clean it up and check for damage.",
|
||||
},
|
||||
],
|
||||
testimonial: "Irwin",
|
||||
related: ["computer-upgrades", "virus-removal", "it-support"],
|
||||
},
|
||||
|
||||
"computer-upgrades": {
|
||||
name: "Computer Upgrades & Speed-Up",
|
||||
title: "SSD Upgrades & PC Speed-Up | Te Atatu Computers",
|
||||
description:
|
||||
"Slow computer? An SSD and RAM upgrade can make it feel new for a fraction of the cost of a new PC. Local upgrades in Te Atatū, West Auckland.",
|
||||
h1: "SSD Upgrades & PC Speed-Up in Te Atatū & West Auckland",
|
||||
intro: [
|
||||
"Does your computer take forever to start, freeze when you open a few tabs, or sit with the disk light flashing? Very often the fix isn't a new computer. It's swapping an old hard drive for an SSD, adding more memory, or clearing out the software slowing Windows down.",
|
||||
"I do upgrades from Te Atatū Peninsula for homes and businesses across West Auckland. If an upgrade isn't worth it for your computer, I'll tell you before you spend anything.",
|
||||
],
|
||||
fixes: [
|
||||
"SSD upgrades: replacing an old hard drive with a solid-state drive for much faster start-up and loading",
|
||||
"Cloning your old drive to the new SSD, so everything stays exactly as it was, just faster",
|
||||
"RAM upgrades so your computer can handle more at once",
|
||||
"Windows clean-ups: startup programs, drivers and unnecessary software",
|
||||
"Fresh Windows reinstalls",
|
||||
"Windows 11 upgrades for eligible PCs",
|
||||
"Graphics card upgrades for desktop PCs",
|
||||
"Custom PC builds: I'll help you pick the parts and build it",
|
||||
],
|
||||
prices: [
|
||||
{ item: "SSD upgrade", price: "from $45 + the drive" },
|
||||
{ item: "Windows reinstall", price: "from $45" },
|
||||
{ item: "Moving files to a new drive or PC", price: "from $45" },
|
||||
{ item: "RAM, graphics card and Windows 11 upgrades", price: "from $35 + parts" },
|
||||
{ item: "Custom PC build (labour)", price: "from $35" },
|
||||
],
|
||||
faqs: [
|
||||
{
|
||||
question: "Will I lose my files and programs with an SSD upgrade?",
|
||||
answer:
|
||||
"No, not if you don't want to. I can clone your old drive onto the new SSD, so your files, programs and settings stay as they were. If you'd rather start fresh, I can do a clean Windows install instead.",
|
||||
},
|
||||
{
|
||||
question: "Is it worth upgrading, or should I buy a new computer?",
|
||||
answer:
|
||||
"It depends on the computer. An SSD and more RAM can make an older machine feel much quicker for far less than a new one. If an upgrade won't help much, I'll tell you before you spend anything.",
|
||||
},
|
||||
{
|
||||
question: "Can my computer run Windows 11?",
|
||||
answer:
|
||||
"Some older PCs can't officially run Windows 11. I can check yours and upgrade it if it's eligible. If it isn't, I'll talk you through your options.",
|
||||
},
|
||||
],
|
||||
testimonial: "Irwin",
|
||||
related: ["computer-repair", "it-support", "virus-removal"],
|
||||
},
|
||||
|
||||
"virus-removal": {
|
||||
name: "Virus & Malware Removal",
|
||||
title: "Virus & Malware Removal West Auckland",
|
||||
description:
|
||||
"Pop-ups, scams, slow or hacked PC? I'll clean it up, usually without losing your files, and help stop it happening again. Based in Te Atatū.",
|
||||
h1: "Virus & Malware Removal in Te Atatū & West Auckland",
|
||||
intro: [
|
||||
"Pop-ups that won't go away? A browser that keeps sending you to strange sites, a \"your computer is infected\" warning with a phone number, or a PC that's suddenly slow? These are common signs of malware or a scam, and they can usually be cleaned up.",
|
||||
"I remove viruses and malware from Windows computers, usually without wiping the computer, so your files stay where they are. Then I'll show you how to avoid it happening again.",
|
||||
],
|
||||
fixes: [
|
||||
"Viruses, malware, spyware and adware",
|
||||
"Fake virus warnings and scam pop-ups",
|
||||
"Browser hijacks: changed home pages, unwanted toolbars and search engines",
|
||||
"Computers used by a scammer through remote access",
|
||||
"Checking for anything left behind once the infection is removed",
|
||||
"Setting up reliable antivirus protection",
|
||||
"Advice on safer browsing, email and passwords",
|
||||
],
|
||||
fixesNote:
|
||||
"If an infection is too deep to clean safely, I'll back up your files and reinstall Windows. I'll always tell you before I do this.",
|
||||
prices: [
|
||||
{ item: "Diagnosis", price: "$35" },
|
||||
{ item: "Virus and malware removal", price: "from $35" },
|
||||
{ item: "Windows reinstall (if needed)", price: "from $45" },
|
||||
],
|
||||
faqs: [
|
||||
{
|
||||
question: "Will I lose my files?",
|
||||
answer:
|
||||
"Usually not. Most infections can be removed without wiping the computer. If a reinstall is needed, I'll back up your files first and tell you before I start.",
|
||||
},
|
||||
{
|
||||
question: "I let a scammer onto my computer. What should I do?",
|
||||
answer:
|
||||
"Disconnect the computer from the internet, and call your bank if you shared any banking or card details. From another device, change your important passwords, starting with email and banking. Then get in touch and I'll check the computer and remove anything the scammer installed.",
|
||||
},
|
||||
{
|
||||
question: "Can you remove a virus remotely?",
|
||||
answer:
|
||||
"Sometimes. Remote support works well for many software problems. If the infection is stopping remote access or needs a closer look, I'll get you to drop the computer off or I'll come to you.",
|
||||
},
|
||||
{
|
||||
question: "Do you remove viruses from Macs?",
|
||||
answer: "No, I don't work on Apple devices. I only work on Windows computers.",
|
||||
},
|
||||
],
|
||||
testimonial: "Marlies",
|
||||
related: ["computer-repair", "it-support", "computer-upgrades"],
|
||||
},
|
||||
|
||||
"it-support": {
|
||||
name: "IT Support",
|
||||
title: "Home & Small Business IT Support West Auckland",
|
||||
description:
|
||||
"Onsite and remote IT support for homes and small businesses: Wi-Fi, email, backups, security and new-PC setup.",
|
||||
h1: "Home & Small Business IT Support in West Auckland",
|
||||
intro: [
|
||||
"Wi-Fi dropping out, email not syncing, a printer that won't print, or a new computer you need set up? I help homes and small businesses around Te Atatū and West Auckland with everyday IT problems, onsite or remotely.",
|
||||
"There's no call centre. You get the person who'll actually do the work, and you pay for the time you need.",
|
||||
],
|
||||
fixes: [
|
||||
"Wi-Fi and network setup: routers, mesh Wi-Fi and weak signal",
|
||||
"Email setup and problems (Outlook, Gmail and others)",
|
||||
"Microsoft 365 and Google Workspace setup for small businesses",
|
||||
"Printer and scanner setup and troubleshooting",
|
||||
"New computer setup, including moving files from your old one",
|
||||
"Backups, so you don't lose photos or business files",
|
||||
"Security basics: antivirus, updates and safer passwords",
|
||||
"Software installation and configuration",
|
||||
"Remote help for software problems, without leaving home or the office",
|
||||
],
|
||||
prices: [
|
||||
{ item: "Onsite or remote support", price: "$35 an hour" },
|
||||
{ item: "Onsite travel fee", price: "Depends on distance. Ask for a quote." },
|
||||
{ item: "Moving files to a new computer", price: "from $45" },
|
||||
{ item: "New computer setup", price: "from $35" },
|
||||
],
|
||||
faqs: [
|
||||
{
|
||||
question: "Do you support small businesses?",
|
||||
answer:
|
||||
"Yes. I help small businesses with networks, email, Microsoft 365 and Google Workspace, backups, printers and new computers, onsite or remotely, at $35 an hour.",
|
||||
},
|
||||
{
|
||||
question: "How does remote support work?",
|
||||
answer:
|
||||
"With your permission, I connect to your computer over the internet and fix the problem from here. It works for most software problems. Hardware problems need an onsite visit or a drop-off.",
|
||||
},
|
||||
{
|
||||
question: "Do you charge for coming to me?",
|
||||
answer:
|
||||
"Yes, there's a travel fee for onsite visits, based on distance. Get in touch with your suburb and I'll give you a quote.",
|
||||
},
|
||||
{
|
||||
question: "Can you set up my new computer and move my files across?",
|
||||
answer:
|
||||
"Yes. Moving files to a new computer starts from $45, and I can set up your email, printer and software while I'm at it.",
|
||||
},
|
||||
],
|
||||
testimonial: "Marlies",
|
||||
related: ["computer-repair", "computer-upgrades", "virus-removal"],
|
||||
},
|
||||
};
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
// Single source of truth for business facts (from the owner inputs in PLAN.md).
|
||||
// Update values here rather than hard-coding them in pages.
|
||||
|
||||
export const SITE_URL = "https://teatatucomputers.com";
|
||||
|
||||
export const business = {
|
||||
name: "Te Atatu Computers",
|
||||
ownerName: "John",
|
||||
phone: "021 107 7483",
|
||||
phoneE164: "+64211077483",
|
||||
email: "[email protected]",
|
||||
address: {
|
||||
street: "34 Graham Avenue",
|
||||
suburb: "Te Atatū Peninsula",
|
||||
city: "Auckland",
|
||||
postcode: "0610",
|
||||
country: "NZ",
|
||||
},
|
||||
hours: "Mon–Fri 9am–6pm. Weekends by appointment.",
|
||||
serviceArea: [
|
||||
"Te Atatū Peninsula",
|
||||
"Te Atatū South",
|
||||
"Henderson",
|
||||
"Massey",
|
||||
"Lincoln",
|
||||
"Glendene",
|
||||
"Westgate",
|
||||
"Auckland CBD",
|
||||
],
|
||||
paymentMethods: [
|
||||
"Cash",
|
||||
"NZ bank transfer",
|
||||
"Contactless Visa, Mastercard and American Express",
|
||||
"Apple Pay, Google Pay and Samsung Pay",
|
||||
"Bitcoin",
|
||||
],
|
||||
prices: {
|
||||
diagnostic: "$35",
|
||||
hourly: "$35",
|
||||
},
|
||||
rating: { value: "4.8", count: 4 },
|
||||
testimonials: [
|
||||
{
|
||||
name: "Irwin",
|
||||
quote:
|
||||
"John is a super star! He fixed my laptop for a reasonable price. I highly recommend him!",
|
||||
},
|
||||
{
|
||||
name: "Marlies",
|
||||
quote:
|
||||
"Very happy with the service provided. Very prompt response to my online query and further questions. John knew what he was doing, was professional and helpful. Thank you!",
|
||||
},
|
||||
],
|
||||
googleBusinessProfileUrl: "https://share.google/APzXLRzyrqAUldl4Y",
|
||||
googleReviewUrl: "https://g.page/r/CYkJobPXq4IpEAE/review",
|
||||
facebookUrl: "https://www.facebook.com/profile.php?id=61571509018351",
|
||||
};
|
||||
|
||||
export const BUSINESS_ID = `${SITE_URL}/#business`;
|
||||
|
||||
export const links = {
|
||||
tel: `tel:${business.phoneE164}`,
|
||||
sms: `sms:${business.phoneE164}`,
|
||||
whatsapp: `https://wa.me/${business.phoneE164.slice(1)}`,
|
||||
email: `mailto:${business.email}`,
|
||||
};
|
||||
|
||||
// Every public route. Used by the sitemap; add new pages here.
|
||||
export const routes = [
|
||||
{ path: "/", priority: 1 },
|
||||
{ path: "/services/computer-repair", priority: 0.8 },
|
||||
{ path: "/services/computer-upgrades", priority: 0.8 },
|
||||
{ path: "/services/virus-removal", priority: 0.8 },
|
||||
{ path: "/services/it-support", priority: 0.8 },
|
||||
{ path: "/pricing", priority: 0.8 },
|
||||
{ path: "/business", priority: 0.7 },
|
||||
{ path: "/windows-10", priority: 0.7 },
|
||||
{ path: "/about", priority: 0.6 },
|
||||
{ path: "/contact", priority: 0.6 },
|
||||
];
|
||||
|
||||
// Full price list, shown on /pricing. Labour prices; parts are extra.
|
||||
export const priceList = [
|
||||
{
|
||||
group: "Repairs",
|
||||
items: [
|
||||
{ item: "Diagnosis", price: "$35" },
|
||||
{ item: "Laptop screen replacement", price: "from $35 + the screen" },
|
||||
{ item: "Battery, charging port or keyboard", price: "from $35 + parts" },
|
||||
{ item: "Other repairs (labour)", price: "$35 an hour" },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Upgrades & Windows",
|
||||
items: [
|
||||
{ item: "SSD upgrade", price: "from $45 + the drive" },
|
||||
{ item: "Windows reinstall", price: "from $45" },
|
||||
{ item: "Windows 11 compatibility check", price: "Free" },
|
||||
{ item: "RAM, graphics card and Windows 11 upgrades", price: "from $35 + parts" },
|
||||
{ item: "Custom PC build (labour)", price: "from $35" },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: "Viruses, data & support",
|
||||
items: [
|
||||
{ item: "Virus and malware removal", price: "from $35" },
|
||||
{ item: "Moving files to a new computer", price: "from $45" },
|
||||
{ item: "New computer setup", price: "from $35" },
|
||||
{ item: "Remote support", price: "$35 an hour" },
|
||||
{ item: "Onsite support", price: "$35 an hour + travel" },
|
||||
{ item: "Small business IT plan", price: "$65 a month" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const businessPlan = {
|
||||
price: "$65",
|
||||
hours: 2,
|
||||
includes: [
|
||||
"2 hours of remote or onsite support every month",
|
||||
"Extra time at the normal $35 an hour",
|
||||
"No lock-in contract. Cancel any time.",
|
||||
],
|
||||
terms:
|
||||
"Unused hours don't roll over to the next month. Onsite visits have a travel fee based on distance.",
|
||||
};
|
||||
|
||||
// Builds per-page metadata: title, description, canonical, Open Graph and Twitter.
|
||||
export function pageMetadata({ title, description, path }) {
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: { canonical: path },
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: path,
|
||||
siteName: business.name,
|
||||
locale: "en_NZ",
|
||||
type: "website",
|
||||
images: [
|
||||
{
|
||||
url: "/og-image.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: business.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description,
|
||||
images: ["/og-image.png"],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function localBusinessSchema() {
|
||||
const { address } = business;
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ComputerRepairService",
|
||||
"@id": BUSINESS_ID,
|
||||
name: business.name,
|
||||
url: `${SITE_URL}/`,
|
||||
telephone: business.phoneE164,
|
||||
email: business.email,
|
||||
logo: `${SITE_URL}/logo.png`,
|
||||
image: `${SITE_URL}/og-image.png`,
|
||||
address: {
|
||||
"@type": "PostalAddress",
|
||||
streetAddress: address.street,
|
||||
addressLocality: address.suburb,
|
||||
addressRegion: address.city,
|
||||
postalCode: address.postcode,
|
||||
addressCountry: address.country,
|
||||
},
|
||||
areaServed: business.serviceArea.map((name) => ({
|
||||
"@type": "Place",
|
||||
name: `${name}, Auckland`,
|
||||
})),
|
||||
openingHoursSpecification: [
|
||||
{
|
||||
"@type": "OpeningHoursSpecification",
|
||||
dayOfWeek: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
|
||||
opens: "09:00",
|
||||
closes: "18:00",
|
||||
},
|
||||
],
|
||||
priceRange: "$",
|
||||
currenciesAccepted: "NZD",
|
||||
paymentAccepted: business.paymentMethods.join(", "),
|
||||
hasMap: business.googleBusinessProfileUrl,
|
||||
sameAs: [business.googleBusinessProfileUrl, business.facebookUrl],
|
||||
};
|
||||
}
|
||||
|
||||
export function serviceSchema({
|
||||
name,
|
||||
description,
|
||||
path,
|
||||
areas = business.serviceArea,
|
||||
}) {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Service",
|
||||
name,
|
||||
serviceType: name,
|
||||
description,
|
||||
url: `${SITE_URL}${path}`,
|
||||
provider: { "@id": BUSINESS_ID },
|
||||
areaServed: areas.map((area) => ({
|
||||
"@type": "Place",
|
||||
name: `${area}, Auckland`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function faqSchema(faqs) {
|
||||
return {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
mainEntity: faqs.map(({ question, answer, list }) => ({
|
||||
"@type": "Question",
|
||||
name: question,
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: list ? `${answer} ${list.join("; ")}.` : answer,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in new issue
Block a user