Compare commits

...
15 Commits
Author SHA1 Message Date
asownerytandClaude Opus 5.5 2b2ac742dd Add Mac and console repairs with a footer disclaimer
Docker / build (push) Successful in 1m16s
Mac (MacBook, iMac) and PlayStation/Xbox repairs are now offered;
phones and tablets are not. The Apple, Sony and Microsoft
non-affiliation disclaimer is shown once in the footer, with
footnote markers on the mentions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 16:09:49 +13:00
asownerytandClaude Opus 5.5 5be5bda079 Add Rybbit analytics and send click events to it
Docker / build (push) Successful in 1m31s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 15:35:34 +13:00
asownerytandClaude Opus 5.5 2fff286afb Use LocalBusiness for the business schema
Docker / build (push) Successful in 1m17s
ComputerRepairService isn't a schema.org type (schema.org/ComputerRepairService
is a 404), so Google ignored the business data and the Rich Results Test
found no items. LocalBusiness is the type Google supports for this.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:56:32 +13:00
asownerytandClaude Opus 5.5 3940cf3379 Remove the Arcane deployment webhook trigger
Docker / build (push) Successful in 1m17s
The workflow now only builds and pushes the Docker image.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:40:02 +13:00
asownerytandClaude Opus 5.5 95887551b2 Remove the sticky mobile Call/Text bar
Docker / build (push) Failing after 1m43s
Also drops the body padding and Chatwoot bubble offset that only
existed to make room for it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:31:24 +13:00
asownerytandClaude Opus 5.5 30dc6f2c7b Migrate to the cn package
- Ran `npx shadcn@latest migrate cn`: swaps clsx + tailwind-merge for cn
- Import cn from "cn" everywhere (matches what `shadcn add` now writes)
  and remove the src/lib/utils.js re-export shim

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:26:38 +13:00
asownerytandClaude Opus 5.5 f916f058fc Swap to shadcn components and polish the design
Components (added via the shadcn MCP/CLI):
- Navbar: NavigationMenu on desktop, Sheet on mobile; drops
  Navbar.module.css and the hand-rolled slide-down menu
- Buttons: shadcn Button via a shared ContactButtons component
  (replaces Button.js and its CSS module)
- Card for price cards, testimonials, the business block and plan;
  Table for prices; Input/Textarea/Label/NativeSelect/RadioGroup and
  Alert in the quote form; Separator in the footer
- Components import cn from @/lib/utils (not the new "cn" package);
  accordion moved to the unified radix-ui package

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

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:25:07 +13:00
asownerytandClaude Opus 5.5 f6846ebaae Add shadcn MCP server config
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:15:46 +13:00
asownerytandClaude Opus 5.5 1ec5088652 Update dependencies and fix security advisories
- next 16.3.7, react/react-dom 19.3.0, tailwindcss 4.3.3, sharp 0.35.5,
  lucide-react 1.49.0 and other minor updates
- npm audit: 10 vulnerabilities (2 critical: next, tar) -> 0
- Remove unused motion and @next/env
- Stay on ESLint 9: ESLint 10 crashes in eslint-config-next's plugins

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 21:14:25 +13:00
asownerytandClaude Opus 5.5 275bce3b87 Phase 6: QA fixes for Lighthouse and navigation
- Map is a titled iframe (accessibility); drop @next/third-parties
- "Services" nav toggle is a real button with aria-expanded (SEO
  crawlable-anchors, accessibility)
- Hero images use default quality; page-header image is preloaded (LCP)
- Mobile services submenu no longer clips its last item

Mobile Lighthouse: / 94/100/100/100, /services/computer-repair
96/100/100/100, /areas/henderson 97/100/100/100, /contact 95/100/100/100
(performance/accessibility/best practices/SEO).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 20:52:53 +13:00
asownerytandClaude Opus 5.5 4d901b351e Phase 5: contact, conversion and tracking hooks
- Contact page: Call/Text/WhatsApp at the top, hours, drop-off address,
  service area, map, and the quote form
- Quote form: device, problem, preferred contact; email required, phone
  optional; labels wired to inputs; _gotcha honeypot; AJAX submit to
  form.taxi with an inline "same day during business hours" message and
  an error fallback (still posts normally without JavaScript)
- Analytics hooks for click_call, click_text, click_email,
  submit_quote_form and click_google_reviews; no-ops until Plausible
  is installed
- /review redirects (307) to the Google review link
- Maps embed moved to one MapEmbed component

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 20:48:56 +13:00
asownerytandClaude Opus 5.5 9f5cf387dc Phase 4: pricing, about, area, Windows 10 and business pages
- /pricing: full price list, no-fix-no-fee terms, travel and payments
- /about: first-person page from confirmed facts, links to area pages
- /areas/[suburb]: Te Atatū Peninsula, Te Atatū South, Henderson and
  Massey, generated from src/lib/areas.js with suburb-specific copy
- /windows-10: options page with free Windows 11 check; October 2028
  used for business Extended Security Updates
- /business: $65/month plan (2 hours, no lock-in, no rollover)
- Nav gains Pricing/About and Business/Windows 10 links; footer rebuilt
  with address, hours and links; homepage links areas, /business and
  a Windows 10 banner; sitemap includes all new routes
- Mobile menu hides by its own height (fixes overflow with more links);
  hamburger now used below 900px so the link row never overlaps

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 20:47:06 +13:00
asownerytandClaude Opus 5.5 40d3a08050 Phase 3: service pages on a shared template
- ServicePage template: H1 with service + area, intro with Call/Text
  above the fold, What I fix, Pricing, How it works, Why choose me,
  a testimonial, FAQs (with FAQPage schema), CTA band, related services
- Service copy moved to src/lib/services.js, rewritten in first person
  with owner-confirmed jobs and prices only; removes the "team of
  technicians", "preferred choice", Mac and motherboard claims
- FaqList and CtaBand extracted and shared with the homepage
- Button gains primary/outlineDark variants; responsive page-header H1
- Compact Chatwoot bubble on phones so it doesn't cover CTAs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 20:37:42 +13:00
asownerytandClaude Opus 5.5 7043b47de1 Phase 2: homepage that converts
- Hero says what and where, with No fix no fee / $35 diagnosis, Call and
  Text CTAs, and the real Google rating
- Common fixes grid with owner-confirmed labour prices, linking to
  service pages
- How it works, About (first person), testimonials, areas served with
  map, a short business IT block, rewritten FAQ and a final CTA band
- Sticky Call/Text bar on mobile (site-wide); Chatwoot bubble moved above it
- Button renders a link when given href (no more button-inside-link)
- Business facts (prices, rating, testimonials, contact links) in site.js

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 20:32:46 +13:00
asownerytandClaude Opus 5.5 c085e2b031 Phase 1: technical SEO foundations
- Per-page titles, descriptions, canonicals, Open Graph/Twitter tags
  via a shared pageMetadata() helper; metadataBase on the root layout
- Sitemap generated from a route list on the canonical non-www host;
  robots.txt points to it; /services removed
- One H1 per page; decorative images get empty alt text
- JSON-LD: site-wide ComputerRepairService, FAQPage on the homepage,
  Service on each service page
- FAQ answers are now server-rendered (forceMount) so the schema matches
  the HTML; fixed the nested payment FAQ item
- Drop web design (page, nav link, homepage card and FAQs); 301 the old URL
- Remove GA, Facebook Pixel and Vercel Analytics/Speed Insights
- Fix tel: links (+640211... -> +64211...)
- Add og-image.png and logo.png

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 20:24:29 +13:00
62 changed files with 5485 additions and 2385 deletions

No files matched your search

-27
View File
@@ -74,30 +74,3 @@ jobs:
# cache import timed out (~30s) on every run. The Dockerfile's BuildKit # cache import timed out (~30s) on every run. The Dockerfile's BuildKit
# cache mounts still speed up dependency installs within a build. # cache mounts still speed up dependency installs within a build.
platforms: linux/amd64 platforms: linux/amd64
# Tell Arcane to pull the freshly-pushed image and recreate the container.
#
# Create the webhook in Arcane: Settings -> Webhooks -> New, targeting the
# compose PROJECT with the "update" action (pulls new images + recreates
# changed services). Arcane shows the token (arc_wh_...) exactly once; the
# full trigger URL is
# https://<arcane-host>/api/webhooks/trigger/arc_wh_xxxxxxxx...
# Store that whole URL as the Actions secret ARCANE_WEBHOOK_URL. The
# endpoint is unauthenticated apart from the token in the path (rate-limited
# to 60 req / 10s per IP), takes no body, and only fires on real pushes to
# master — not on PRs, and skipped automatically if the secret is unset.
- name: Trigger Arcane deployment
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
env:
ARCANE_WEBHOOK_URL: ${{ secrets.ARCANE_WEBHOOK_URL }}
run: |
if [ -z "$ARCANE_WEBHOOK_URL" ]; then
echo "ARCANE_WEBHOOK_URL not set; skipping deploy trigger."
exit 0
fi
code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST "$ARCANE_WEBHOOK_URL")
echo "Arcane webhook responded with HTTP $code"
case "$code" in
2*) echo "Deployment triggered." ;;
*) echo "::error::Arcane webhook failed (HTTP $code)"; exit 1 ;;
esac
+11
View File
@@ -0,0 +1,11 @@
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": [
"shadcn@latest",
"mcp"
]
}
}
}
+17
View File
@@ -4,6 +4,23 @@ const nextConfig = {
images: { images: {
qualities: [75, 100], 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; export default nextConfig;
+2125 -1260
View File
File diff suppressed because it is too large. Load diff
+12 -18
View File
@@ -9,27 +9,21 @@
"lint": "eslint ." "lint": "eslint ."
}, },
"dependencies": { "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", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "cn": "^0.4.0",
"lucide-react": "^0.487.0", "lucide-react": "^1.49.0",
"motion": "^11.16.0", "next": "^16.3.7",
"next": "^16.2.10", "radix-ui": "^1.6.7",
"react": "^19.2.7", "react": "^19.3.0",
"react-dom": "^19.2.7", "react-dom": "^19.3.0",
"sharp": "^0.33.5", "sharp": "^0.35.5",
"tailwind-merge": "^3.1.0", "tw-animate-css": "^1.4.0"
"tw-animate-css": "^1.2.5"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.1", "@tailwindcss/postcss": "^4.3.3",
"eslint": "^9.39.5", "eslint": "^9.39.5",
"eslint-config-next": "^16.2.10", "eslint-config-next": "^16.3.7",
"postcss": "^8.4.49", "postcss": "^8.5.28",
"tailwindcss": "^4.1.1" "tailwindcss": "^4.3.3"
} }
} }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+88
View File
@@ -0,0 +1,88 @@
import Link from "next/link";
import HeaderSm from "../components/HeaderSm";
import CtaBand from "../components/CtaBand";
import Footnote from "../components/Footnote";
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&apos;s just me, the person you call or text is the person
who fixes your computer. There&apos;s no front desk and no call
centre, and you won&apos;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&apos;t fix it, you don&apos;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, Macs, Chromebooks, custom-built
computers, and PlayStation and Xbox consoles.
<Footnote /> I don&apos;t repair phones or tablets.
</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>
);
}
+124
View File
@@ -0,0 +1,124 @@
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 Footnote from "../../components/Footnote";
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&apos;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 also repair Macs and PlayStation and Xbox consoles,
<Footnote /> but not phones or tablets.
</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>
);
}
+164
View File
@@ -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&apos;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&apos;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>
);
}
+18
View File
@@ -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;
}
-7
View File
@@ -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>
);
}
-20
View File
@@ -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;
}
+2 -1
View File
@@ -9,7 +9,8 @@ class ChatwootWidget extends React.Component {
hideMessageBubble: false, hideMessageBubble: false,
position: "right", // This can be left or right position: "right", // This can be left or right
locale: "en", // Language to be set 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 // Paste the script from inbox settings except the <script> tag
+56
View File
@@ -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>
);
}
+197 -51
View File
@@ -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", "Mac", "Chromebook", "PlayStation or Xbox", "Other"];
const problems = [
"Won't turn on or won't start",
"Cracked or broken screen",
"Running slow / upgrade",
"Virus, pop-ups or scam",
"Battery or charging",
"Moving files to a new computer",
"Windows 10 / Windows 11",
"Business IT support",
"Something else",
];
const contactMethods = ["Call", "Text", "Email"];
function Field({ children }) {
return <div className="grid gap-2">{children}</div>;
}
// Submits to form.taxi with JavaScript for an inline success message; without
// JavaScript the form still posts normally.
export default function ContactForm() { 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&apos;ve got it</AlertTitle>
<AlertDescription>
<p>
I&apos;ll usually reply the same day during business hours (Monday
to Friday, 9am to 6pm). If it&apos;s urgent, call or text{" "}
<a href={links.tel} className="font-semibold text-primary underline">
{business.phone}
</a>
.
</p>
</AlertDescription>
</Alert>
);
}
return ( return (
<div> <div>
<h2 className="text-3xl font-bold mb-4">Contact Us</h2> <h2 className="text-3xl font-bold mb-2">Get a quote</h2>
<div className={styles.contactDetails}> <p className="mb-6 text-muted-foreground">
<h3 className="text-lg font-bold hover:underline"> Tell me what&apos;s going on and I&apos;ll get back to you, usually the
<svg same day during business hours.
xmlns="http://www.w3.org/2000/svg" </p>
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>
<form <form
action="https://form.taxi/s/es9ddva1" action={FORM_ACTION}
method="POST" method="POST"
className={styles.form} className="grid gap-5"
onSubmit={handleSubmit}
> >
<label htmlFor="Name">Name*</label> <Field>
<input type="text" name="Name" required /> <Label htmlFor="cf-name">Name*</Label>
<Input id="cf-name" name="Name" autoComplete="name" required />
</Field>
<label htmlFor="Phone">Phone Number </label> <div className="grid gap-5 sm:grid-cols-2">
<input type="tel" name="Phone" /> <Field>
<Label htmlFor="cf-email">Email*</Label>
<Input
id="cf-email"
type="email"
name="Email"
autoComplete="email"
required
/>
</Field>
<Field>
<Label htmlFor="cf-phone">Phone number</Label>
<Input id="cf-phone" type="tel" name="Phone" autoComplete="tel" />
</Field>
</div>
<label htmlFor="Email">Email*</label> <fieldset className="grid gap-3">
<input type="email" name="Email" required /> <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>
<label htmlFor="firstname">Message*</label> <div className="grid gap-5 sm:grid-cols-2">
<textarea name="Message"></textarea> <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>
<p className={styles.sm}>* required</p> <Field>
<Label htmlFor="cf-message">What&apos;s going on?*</Label>
<Textarea
id="cf-message"
name="Message"
rows={5}
placeholder="e.g. HP laptop, screen cracked in the corner, still turns on"
required
/>
</Field>
{/* Honeypot: hidden from people, filled in by bots. Enable "_gotcha" in form.taxi's Spam Shield. */}
<input <input
type="submit" type="text"
value="Submit" name="_gotcha"
required className="hidden"
className="text-white bg-sky-800 hover:bg-sky-700 transition" tabIndex={-1}
autoComplete="off"
aria-hidden="true"
/> />
<p className="text-sm text-muted-foreground">
* required.
</p>
{status === "error" && (
<Alert variant="destructive" role="alert">
<CircleAlert aria-hidden="true" />
<AlertTitle>That didn&apos;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> </form>
</div> </div>
); );
-41
View File
@@ -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;
}
+23
View File
@@ -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&apos;ll get back to you
with a quote.
</p>
<ContactButtons
onDark
whatsapp
quoteHref={quoteHref}
className="justify-center"
/>
</div>
</section>
);
}
+36
View File
@@ -0,0 +1,36 @@
import JsonLd from "./JsonLd";
import Footnote from "./Footnote";
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, footnote }, i) => (
<AccordionItem key={question} value={`item-${i + 1}`}>
<AccordionTrigger>{question}</AccordionTrigger>
<AccordionContent>
{answer}
{footnote && <Footnote />}
{list && (
<ul className="list-disc pl-6 my-2">
{list.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
)}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</>
);
}
+95 -37
View File
@@ -1,43 +1,101 @@
import Link from "next/link";
import { Mail, MapPin, Phone } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { brandDisclaimer, 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() { export default function Footer() {
const { address } = business;
return ( return (
<footer className="bg-slate-800 text-white py-8 text-center"> <footer className="bg-slate-800 text-white py-12">
<div className="flex flex-col items-center"> <div className="container mx-auto grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
<h3 className="flex flex-row items-center justify-center mb-4 space-x-2 hover:underline text-lg font-bold"> <div>
<svg <p className="text-lg font-bold mb-4">{business.name}</p>
xmlns="http://www.w3.org/2000/svg" <ul className="space-y-3">
viewBox="0 0 20 20" <li>
fill="currentColor" <a
width="1em" href={links.tel}
> className="flex items-center gap-2 hover:underline"
<path >
fillRule="evenodd" <Phone aria-hidden="true" className="size-4 shrink-0" />
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" {business.phone}
clipRule="evenodd" </a>
/> </li>
</svg> <li>
<a href="tel:+640211077483"> <a
021 107 7483 href={links.email}
</a> className="flex items-center gap-2 hover:underline"
</h3> >
<h3 className="flex flex-row items-center justify-center mb-4 space-x-2 hover:underline text-lg font-bold"> <Mail aria-hidden="true" className="size-4 shrink-0" />
<svg {business.email}
xmlns="http://www.w3.org/2000/svg" </a>
viewBox="0 0 20 20" </li>
fill="currentColor" <li className="flex items-start gap-2">
width="1em" <MapPin aria-hidden="true" className="size-4 shrink-0 mt-1" />
> <span>
<path {address.street}, {address.suburb}, {address.city}{" "}
fillRule="evenodd" {address.postcode}
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" <br />
clipRule="evenodd" {business.hours}
/> </span>
</svg> </li>
<a href="mailto:[email protected]"> </ul>
info@teatatucomputers.com </div>
</a> <nav aria-label="Services">
</h3> <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 id="disclaimer" className="text-sm text-slate-400 mb-4 scroll-mt-24">
† {brandDisclaimer}
</p>
<p className="text-sm text-slate-400">
Copyright © {business.name}, {new Date().getFullYear()}
</p>
</div> </div>
<p>Copyright © Te Atatu Computer Repair, {new Date().getFullYear()}</p>
</footer> </footer>
); );
} }
+15
View File
@@ -0,0 +1,15 @@
// Marker linking to the brand disclaimer in the footer. Add it after
// mentions of Mac or console repairs instead of repeating the disclaimer.
export default function Footnote() {
return (
<sup>
<a
href="#disclaimer"
aria-label="See disclaimer"
className="text-primary no-underline"
>
†
</a>
</sup>
);
}
+3 -3
View File
@@ -8,10 +8,10 @@ export default function HeaderSm({ text }) {
<div className={styles.header}> <div className={styles.header}>
<Image <Image
src={bgimage} src={bgimage}
alt="Header Background" alt=""
placeholder="blur" placeholder="blur"
quality={100}
fill fill
priority
sizes="100vw" sizes="100vw"
style={{ style={{
objectFit: "cover", objectFit: "cover",
@@ -20,7 +20,7 @@ export default function HeaderSm({ text }) {
<div className={styles.textbox}> <div className={styles.textbox}>
<div className="container mx-auto"> <div className="container mx-auto">
<div className={styles.textboxInner}> <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> </div>
</div> </div>
+11
View File
@@ -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"),
}}
/>
);
}
+19
View File
@@ -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
/>
);
}
+147 -170
View File
@@ -1,186 +1,163 @@
"use client"; "use client";
import { useState, useRef, useEffect } from "react";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import styles from "./Navbar.module.css";
import logo from "../assets/logo.svg";
import menu from "../assets/menu.svg";
import close from "../assets/close.svg";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { ChevronDownIcon } from "lucide-react"; import { MenuIcon, Phone } from "lucide-react";
// import { motion } from "motion/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() { export default function Navbar() {
const pathname = usePathname(); const pathname = usePathname();
const [isActive, setActive] = useState("false"); const isActive = (href) => pathname === href;
const [isDropdownActive, setDropdownActive] = useState(false); const inServices = serviceLinks.some(({ href }) => isActive(href));
function handleToggle() {
setActive(!isActive);
}
function handleDropdownToggle() {
setDropdownActive(!isDropdownActive);
}
const wrapperRef = useRef(null);
useEffect(() => {
/**
* Close the menu if clicked on outside of element
*/
function handleClickOutside(event) {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
setActive(true);
}
}
// Bind the event listener
document.addEventListener("mousedown", handleClickOutside);
return () => {
// Unbind the event listener on clean up
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return ( return (
<nav className={`${styles.navbar} shadow-sm`}> <header className="sticky top-0 z-50 border-b bg-white/90 backdrop-blur">
<div className={`${styles.container} container mx-auto`} ref={wrapperRef}> <div className="container mx-auto flex h-16 items-center justify-between gap-4">
<div className={styles.header}> <Link href="/" className="flex items-center gap-3">
<div> <Image src={logo} width={32} height={32} alt="" />
<Link href="/" className={styles.logo}> <span className="font-mono">{business.name}</span>
<Image src={logo} width="32" height="32" alt="Logo" /> </Link>
<p>Te Atatu Computers</p>
</Link> {/* Desktop */}
</div> <NavigationMenu viewport={false} className="hidden lg:flex">
<button <NavigationMenuList>
className={styles.menuIcon} <NavigationMenuItem>
aria-label="Menu" <NavigationMenuLink
onClick={handleToggle} asChild
> active={isActive("/")}
<Image className={navigationMenuTriggerStyle()}
src={isActive ? menu : close} >
width="32" <Link href="/">Home</Link>
height="32" </NavigationMenuLink>
alt="Menu" </NavigationMenuItem>
/> <NavigationMenuItem>
</button> <NavigationMenuTrigger
</div> className={cn(inServices && "text-primary")}
<ul className={`${styles.links} ${isActive ? "" : styles.active}`}>
<li>
<Link
href="/"
className={`link ${pathname === "/" ? styles.active : ""}`}
>
Home
</Link>
</li>
<li
className={`
${styles.dropdown}
${isDropdownActive && !isActive ? styles.dropdownActive : ""}
`}
onMouseEnter={() => setDropdownActive(true)}
onMouseLeave={() => setDropdownActive(false)}
>
<div
data-state={isDropdownActive ? "open" : "close"}
className="flex items-center justify-center gap-1 [&[data-state=open]>svg]:rotate-180"
>
<a
className={`${styles.dropdownTitle}
${pathname === "/services" ? styles.active : ""}`}
onClick={handleDropdownToggle}
> >
Services Services
</a> </NavigationMenuTrigger>
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" /> <NavigationMenuContent>
</div> <ul className="grid w-64 gap-1">
<ul className={styles.dropdownMenu}> {serviceLinks.map(({ href, label }) => (
<li> <li key={href}>
<Link <NavigationMenuLink asChild active={isActive(href)}>
href="/services/computer-repair" <Link href={href}>{label}</Link>
className={`link ${ </NavigationMenuLink>
pathname === "/services/computer-repair" </li>
? styles.active ))}
: "" </ul>
}`} </NavigationMenuContent>
</NavigationMenuItem>
{pageLinks.map(({ href, label }) => (
<NavigationMenuItem key={href}>
<NavigationMenuLink
asChild
active={isActive(href)}
className={navigationMenuTriggerStyle()}
> >
Computer & Laptop Repairs <Link href={href}>{label}</Link>
</Link> </NavigationMenuLink>
</li> </NavigationMenuItem>
<li> ))}
<Link </NavigationMenuList>
href="/services/computer-upgrades" </NavigationMenu>
className={`link ${ <Button asChild className="hidden lg:inline-flex">
pathname === "/services/computer-upgrades" <Link href="/contact">
? styles.active <Phone aria-hidden="true" />
: "" Contact
}`} </Link>
> </Button>
Computer Upgrades & Speedup
</Link> {/* Mobile */}
</li> <Sheet>
<li> <SheetTrigger asChild>
<Link <Button variant="ghost" size="icon" className="lg:hidden">
href="/services/it-support" <MenuIcon className="size-6" />
className={`link ${ <span className="sr-only">Menu</span>
pathname === "/services/it-support" ? styles.active : "" </Button>
}`} </SheetTrigger>
> <SheetContent side="right" className="w-full max-w-xs overflow-y-auto">
IT Support <SheetHeader>
</Link> <SheetTitle>Menu</SheetTitle>
</li> </SheetHeader>
<li> <nav className="grid gap-6 px-4 pb-8">
<Link <MobileList links={[{ href: "/", label: "Home" }]} isActive={isActive} />
href="/services/web-design" <div>
className={`link ${ <p className="mb-2 text-sm text-muted-foreground">Services</p>
pathname === "/services/web-design" ? styles.active : "" <MobileList links={serviceLinks} isActive={isActive} />
}`} </div>
> <MobileList links={pageLinks} isActive={isActive} />
Web Design <SheetClose asChild>
</Link> <Button asChild size="lg">
</li> <Link href="/contact">
<li> <Phone aria-hidden="true" />
<Link Contact
href="/services/virus-removal" </Link>
className={`link ${ </Button>
pathname === "/services/virus-removal" ? styles.active : "" </SheetClose>
}`} </nav>
> </SheetContent>
Virus Removal </Sheet>
</Link>
</li>
</ul>
</li>
<li>
<Link
href="/#faq"
>
FAQ
</Link>
</li>
<li className="bg-sky-900 text-white lg:ml-4">
<Link
href="/contact"
className={`link ${pathname === "/contact" ? styles.active : ""}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M2 3.5A1.5 1.5 0 0 1 3.5 2h1.148a1.5 1.5 0 0 1 1.465 1.175l.716 3.223a1.5 1.5 0 0 1-1.052 1.767l-.933.267c-.41.117-.643.555-.48.95a11.542 11.542 0 0 0 6.254 6.254c.395.163.833-.07.95-.48l.267-.933a1.5 1.5 0 0 1 1.767-1.052l3.223.716A1.5 1.5 0 0 1 18 15.352V16.5a1.5 1.5 0 0 1-1.5 1.5H15c-1.149 0-2.263-.15-3.326-.43A13.022 13.022 0 0 1 2.43 8.326 13.019 13.019 0 0 1 2 5V3.5Z"
clipRule="evenodd"
/>
</svg>
Contact Us
</Link>
</li>
</ul>
</div> </div>
</nav> </header>
);
}
function MobileList({ links, isActive }) {
return (
<ul className="grid gap-1">
{links.map(({ href, label }) => (
<li key={href}>
<SheetClose asChild>
<Link
href={href}
className={cn(
"block rounded-md px-2 py-2 hover:bg-accent",
isActive(href) && "font-semibold text-primary"
)}
>
{label}
</Link>
</SheetClose>
</li>
))}
</ul>
); );
} }
-219
View File
@@ -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;
}
+18
View File
@@ -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>
);
}
+176
View File
@@ -0,0 +1,176 @@
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 Footnote from "./Footnote";
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}
{service.fixesNoteFootnote && <Footnote />}
</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&apos;t fix it, you don&apos;t pay it.
Once I know what&apos;s wrong, I&apos;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&apos;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">
&ldquo;{testimonial.quote}&rdquo;
</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
View File
@@ -1,11 +1,81 @@
import Link from "next/link";
import { Clock, Mail, MapPin } from "lucide-react";
import HeaderSm from "../components/HeaderSm"; import HeaderSm from "../components/HeaderSm";
import ContactButtons from "../components/ContactButtons";
import ContactForm from "../components/ContactForm"; 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 ( return (
<div> <div>
<HeaderSm text="Contact" /> <HeaderSm text="Contact & book a repair" />
<section className="container mx-auto mb-64">
<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&apos;m{" "}
{business.ownerName}, and I&apos;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 /> <ContactForm />
</section> </section>
</div> </div>
+12 -4
View File
@@ -30,14 +30,15 @@
---break--- ---break---
*/ */
:root { :root {
--radius: 0.625rem; --radius: 0.5rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695); --foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695); --card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695); --popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755); /* Brand blue (Tailwind sky-800), matching the existing buttons and bands. */
--primary: oklch(0.443 0.11 240.79);
--primary-foreground: oklch(0.984 0.003 247.858); --primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896); --secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755); --secondary-foreground: oklch(0.208 0.042 265.755);
@@ -48,7 +49,7 @@
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508); --border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508); --input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788); --ring: oklch(0.588 0.158 241.966);
--chart-1: oklch(0.646 0.222 41.116); --chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704); --chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392); --chart-3: oklch(0.398 0.07 227.392);
@@ -103,6 +104,8 @@
---break--- ---break---
*/ */
@theme inline { @theme inline {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius); --radius-lg: var(--radius);
@@ -147,6 +150,11 @@
@apply border-border outline-ring/50; @apply border-border outline-ring/50;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground antialiased;
}
h1,
h2,
h3 {
@apply tracking-tight text-balance;
} }
} }
+15 -28
View File
@@ -1,14 +1,12 @@
import localFont from "next/font/local"; import localFont from "next/font/local";
import Script from "next/script";
import "./globals.css"; import "./globals.css";
import Navbar from "./components/Navbar"; import Navbar from "./components/Navbar";
import Footer from "./components/Footer"; import Footer from "./components/Footer";
import ChatwootWidget from "./components/ChatwootWidget"; import ChatwootWidget from "./components/ChatwootWidget";
import JsonLd from "./components/JsonLd";
// Add spyware import AnalyticsEvents from "./components/AnalyticsEvents";
import { Analytics } from "@vercel/analytics/react"; import { SITE_URL, business, localBusinessSchema } from "@/lib/site";
import { GoogleAnalytics } from "@next/third-parties/google";
import { SpeedInsights } from "@vercel/speed-insights/next";
import Script from "next/script";
const geistSans = localFont({ const geistSans = localFont({
src: "./fonts/GeistVF.woff", src: "./fonts/GeistVF.woff",
@@ -21,37 +19,26 @@ const geistMono = localFont({
weight: "100 900", weight: "100 900",
}); });
// Pages set their own title, description and canonical via pageMetadata().
export const metadata = { export const metadata = {
title: "Te Atatu Computer Repair & Web Design", metadataBase: new URL(SITE_URL),
description: title: business.name,
"Your Local Experts for PCs, Laptops, Web Design, IT Support & More.",
}; };
export default function RootLayout({ children }) { export default function RootLayout({ children }) {
return ( return (
<html lang="en"> <html lang="en-NZ" className={`${geistSans.variable} ${geistMono.variable}`}>
<body className={`${geistSans.variable} ${geistMono.variable}`}> <body>
<JsonLd data={localBusinessSchema()} />
<ChatwootWidget /> <ChatwootWidget />
<Navbar /> <Navbar />
{children} {children}
<Footer /> <Footer />
<Analytics /> <AnalyticsEvents />
<SpeedInsights /> <Script
<GoogleAnalytics gaId="G-TN7ZL1Y4CC" /> src="https://analytics.cloudrite.co.nz/api/script.js?siteId=362da09a6cea"
<Script id="facebook-pixel"> strategy="afterInteractive"
{` />
!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>
</body> </body>
</html> </html>
); );
+319 -316
View File
@@ -1,16 +1,113 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
import styles from "./page.module.css"; import styles from "./page.module.css";
import bgimage from "./assets/header-bg.jpg"; import bgimage from "./assets/header-bg.jpg";
import Button from "./components/Button"; import ContactButtons from "./components/ContactButtons";
import Link from "next/link"; import MapEmbed from "./components/MapEmbed";
import { GoogleMapsEmbed } from "@next/third-parties/google"; import { Card, CardContent } from "@/components/ui/card";
import ContactForm from "./components/ContactForm"; 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 { import {
Accordion, ArrowRight,
AccordionContent, Briefcase,
AccordionItem, Cpu,
AccordionTrigger, FolderInput,
} from "@/components/ui/accordion"; 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() { export default function Home() {
return ( return (
@@ -20,9 +117,8 @@ export default function Home() {
<div className={styles.header}> <div className={styles.header}>
<Image <Image
src={bgimage} src={bgimage}
alt="Header Background" alt=""
placeholder="blur" placeholder="blur"
quality={100}
fill fill
priority priority
sizes="100vw" sizes="100vw"
@@ -33,329 +129,236 @@ export default function Home() {
<div className={styles.textbox}> <div className={styles.textbox}>
<div className="container mx-auto"> <div className="container mx-auto">
<div className={styles.textboxInner}> <div className={styles.textboxInner}>
<h1 className="mb-4 text-6xl font-bold"> <h1 className="mb-4 text-4xl md:text-6xl font-bold">
Delivering Exceptional Service. Laptop & PC repairs in Te Atatū
</h1> </h1>
<p className={styles.p}> <p className={styles.p}>
<strong> <strong>No fix, no fee.</strong>{" "}A $35 diagnosis, then I&apos;ll
Your Local Experts for PCs, Laptops, Web Design, IT tell you what it costs before I start. Local, honest help
Support & More. for West Auckland.
</strong> </p>
<br /> <ContactButtons onDark className="mb-6" />
From hardware fixes to software solutions, we&apos;re here <p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
to help. Friendly, affordable service right in your <Star
neighborhood! aria-hidden="true"
className="size-4 fill-yellow-400 text-yellow-400"
/>
<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> </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"
/>
</svg>
Get a Quote Now
</Button>
</Link>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section className="container mx-auto mb-32"> <section className="container mx-auto mb-24">
<h1 className="text-center text-3xl font-bold mb-4"> <h2 className="text-center text-3xl font-bold mb-2">
Services we Offer Common fixes & prices
</h1> </h2>
<div className="text-center mb-4 flex flex-row flex-wrap justify-center"> <p className="text-center mb-8 text-muted-foreground">
<div className="max-w-xs p-4 text-center justify-center"> Labour prices. Parts are extra, and I&apos;ll always quote before
<svg starting.
xmlns="http://www.w3.org/2000/svg" </p>
fill="none" <ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
viewBox="0 0 24 24" {commonFixes.map(({ icon: Icon, title, price, href }) => (
strokeWidth={1.5} <li key={title}>
stroke="currentColor" <Link href={href} className="group block h-full rounded-xl">
width="6rem" <Card className="h-full flex-row items-center gap-4 p-4 shadow-none transition-colors group-hover:border-primary">
className="mx-auto mb-4" <Icon
> aria-hidden="true"
<path className="size-7 shrink-0 text-primary"
strokeLinecap="round" strokeWidth={1.5}
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" <span>
/> <span className="block font-semibold">{title}</span>
</svg> <span className="block text-sm text-muted-foreground">
<h2 className="text-xl font-bold mb-2"> {price}
Computer Repair/Upgrades </span>
</h2> </span>
<p> </Card>
We can fix anything from a broken screen to a software issue, or </Link>
upgrade your computer&apos;s parts and optimise Windows to get </li>
your PC running like new again. ))}
</p> </ul>
</div> <SectionCta />
<div className="max-w-xs p-4 text-center justify-center"> <Link
<svg href="/windows-10"
xmlns="http://www.w3.org/2000/svg" className="mt-8 flex items-center justify-between gap-4 rounded-lg bg-sky-900 p-4 text-white transition-colors hover:bg-primary"
fill="none" >
viewBox="0 0 24 24" <span>
strokeWidth={1.5} <span className="block font-bold">Still on Windows 10?</span>
stroke="currentColor" <span className="block text-sm">
width="6rem" See your options, with a free Windows 11 compatibility check.
className="mx-auto mb-4" </span>
> </span>
<path <ArrowRight aria-hidden="true" className="size-5 shrink-0" />
strokeLinecap="round" </Link>
strokeLinejoin="round" </section>
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> <section className="bg-slate-100 py-16 mb-24">
<p> <div className="container mx-auto">
We&apos;ll help you pick the parts and build a custom gaming or <h2 className="text-center text-3xl font-bold mb-8">
workstation PC for you. How it works
</p> </h2>
</div> <ol className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="max-w-xs p-4 text-center justify-center"> {steps.map(({ title, text }, i) => (
<svg <li key={title} className="border-t border-slate-300 pt-4">
xmlns="http://www.w3.org/2000/svg" <span className="mb-3 flex size-8 items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground">
fill="none" {i + 1}
viewBox="0 0 24 24" </span>
strokeWidth={1.5} <h3 className="text-xl font-bold mb-2">{title}</h3>
stroke="currentColor" <p>{text}</p>
width="6rem" </li>
className="mx-auto mb-4" ))}
> </ol>
<path <SectionCta />
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>
<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>
</div> </div>
</section> </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&apos;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&apos;re talking to the person who will actually fix your
computer. There&apos;s no call centre and no being passed around.
</p>
<p className="text-lg">
I&apos;ll explain what&apos;s wrong in plain language and tell you
what it will cost before I start. If I can&apos;t fix it, you
don&apos;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">&ldquo;{quote}&rdquo;</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"> <div className="w-full">
<GoogleMapsEmbed <h2 className="text-3xl font-bold mb-4">Areas I serve</h2>
apiKey="AIzaSyDbgQY55TQY5CefsCUwKbc8RfoOUe8U5gY" <p className="mb-4 flex items-start gap-2">
width="100%" <MapPin aria-hidden="true" className="size-5 shrink-0 mt-0.5" />
height="600px" <span>
mode="place" Based at {business.address.street}, {business.address.suburb}.
q="place_id:ChIJz0ZoDYhADW0R0TDgWoWMq0M" 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&apos;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> </div>
</section> </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"> <section className="container mx-auto mb-24">
Web Design and Development <Card className="flex-col gap-6 p-6 shadow-none md:flex-row md:items-center md:p-8">
</h3> <Briefcase
<Accordion> aria-hidden="true"
<AccordionItem value="item-1"> className="size-12 shrink-0 text-primary"
<AccordionTrigger> strokeWidth={1.5}
What platforms do you build websites with? />
</AccordionTrigger> <div className="flex-1">
<AccordionContent> <h2 className="text-2xl font-bold mb-2">For local businesses</h2>
We specialize in modern web development using WordPress for <p>
content-driven sites and React/Next.js for high-performance, IT support for small businesses: Wi-Fi and networks, email,
custom web applications. We also offer support for almost any backups, security and new PC setup. Onsite or remote, $35 an
website builder, whether it&apos;s Wix, Oddo, Squarespace, or hour, or $65 a month for 2 hours of support.
anything else. Whether you need a blog, business site, </p>
portfolio, or a fully custom web app—we&apos;ve got you covered. </div>
</AccordionContent> <Link
</AccordionItem> 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"> <section
<AccordionTrigger> className="container mx-auto max-w-3xl mb-24 scroll-mt-20"
How do I choose between WordPress and React/Next.js? id="faq"
</AccordionTrigger> >
<AccordionContent> <h2 className="text-center text-3xl font-bold mb-8">
It depends on your needs: Frequently asked questions
<ul className="list-disc pl-6 my-2"> </h2>
<li> <FaqList faqs={homeFaqs} />
<strong>WordPress</strong> is best for content-heavy <SectionCta />
websites like blogs, portfolios, and marketing sites. </section>
</li>
<li> <CtaBand quoteHref="#quote" />
<strong>React/Next.js</strong> is ideal for fast, scalable, <section
custom web applications or interactive user interfaces. id="quote"
</li> className="container mx-auto max-w-2xl py-16 scroll-mt-20"
</ul> >
We’ll help you decide what works best based on your goals and <ContactForm />
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> </section>
</main> </main>
</div> </div>
+1 -1
View File
@@ -1,6 +1,6 @@
.header { .header {
position: relative; position: relative;
height: 50em; height: 32em;
margin-bottom: 3.5em; margin-bottom: 3.5em;
} }
+76
View File
@@ -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&apos;ll tell you
what&apos;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&apos;t fix it, you don&apos;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&apos;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&apos;t accept EFTPOS.</p>
</section>
<CtaBand />
</div>
);
}
+6
View File
@@ -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
View File
@@ -1,10 +1,11 @@
import { SITE_URL } from "@/lib/site";
export default function robots() { export default function robots() {
return { return {
rules: { rules: {
userAgent: "*", userAgent: "*",
allow: "/", allow: "/",
disallow: "/private/",
}, },
sitemap: "https://www.teatatucomputers.com/sitemap.xml", sitemap: `${SITE_URL}/sitemap.xml`,
}; };
} }
+5 -31
View File
@@ -1,33 +1,7 @@
import HeaderSm from "../../components/HeaderSm"; import ServicePage, { serviceMetadata } from "../../components/ServicePage";
export default function Home() { export const metadata = serviceMetadata("computer-repair");
return (
<div style={{ marginBottom: "30em" }}> export default function Page() {
<HeaderSm text="Computer & Laptop Repairs" /> return <ServicePage slug="computer-repair" />;
<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&apos;re dealing with a cracked screen, a faulty keyboard, or software
issues, we excel in diagnosing and resolving all types of laptop
challenges. We&apos;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>
);
} }
+5 -25
View File
@@ -1,27 +1,7 @@
import HeaderSm from "../../components/HeaderSm"; import ServicePage, { serviceMetadata } from "../../components/ServicePage";
export default function Home() { export const metadata = serviceMetadata("computer-upgrades");
return (
<div style={{ marginBottom: "30em" }}> export default function Page() {
<HeaderSm text="Computer & Laptop Upgrades/Speedup" /> return <ServicePage slug="computer-upgrades" />;
<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&apos;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&apos;s responsiveness. These upgrades are cost-effective
solutions to breathe new life into your device and improve your
productivity.
</p>
</section>
</div>
);
} }
+5 -31
View File
@@ -1,33 +1,7 @@
import HeaderSm from "../../components/HeaderSm"; import ServicePage, { serviceMetadata } from "../../components/ServicePage";
export default function Home() { export const metadata = serviceMetadata("it-support");
return (
<div style={{ marginBottom: "30em" }}> export default function Page() {
<HeaderSm text="IT Support" /> return <ServicePage slug="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&apos;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&apos;s digital landscape. With Te Atatu
Computers, you get more than just IT support—you gain a trusted
technology partner.
</p>
</section>
</div>
);
} }
+5 -23
View File
@@ -1,25 +1,7 @@
import HeaderSm from "../../components/HeaderSm"; import ServicePage, { serviceMetadata } from "../../components/ServicePage";
export default function Home() { export const metadata = serviceMetadata("virus-removal");
return (
<div style={{ marginBottom: "30em" }}> export default function Page() {
<HeaderSm text="Virus Removal" /> return <ServicePage slug="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&apos;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>
);
} }
-37
View File
@@ -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&apos;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&apos;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>
);
}
+13 -20
View File
@@ -1,22 +1,15 @@
import { SITE_URL, routes } from "@/lib/site";
import { areas } from "@/lib/areas";
export default function sitemap() { export default function sitemap() {
return [ const areaRoutes = areas.map(({ slug }) => ({
{ path: `/areas/${slug}`,
url: 'https://www.teatatucomputers.com/', priority: 0.6,
lastModified: new Date(), }));
changeFrequency: 'monthly', return [...routes, ...areaRoutes].map(({ path, priority }) => ({
priority: 1, url: `${SITE_URL}${path}`,
}, lastModified: new Date(),
{ changeFrequency: "monthly",
url: 'https://www.teatatucomputers.com/services', priority,
lastModified: new Date(), }));
changeFrequency: 'monthly',
priority: 0.5,
},
{
url: 'https://www.teatatucomputers.com/contact',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5,
},
]
} }
+100
View File
@@ -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&apos;t need to rush out and
buy a new computer. Here are your options, and I&apos;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&apos;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>
);
}
+7 -4
View File
@@ -1,10 +1,10 @@
"use client" "use client"
import * as React from "react" import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion" import { Accordion as AccordionPrimitive } from "radix-ui"
import { ChevronDownIcon } from "lucide-react" import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "cn"
function Accordion({ function Accordion({
...props ...props
@@ -34,7 +34,7 @@ function AccordionTrigger({
<AccordionPrimitive.Trigger <AccordionPrimitive.Trigger
data-slot="accordion-trigger" data-slot="accordion-trigger"
className={cn( className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180", "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-base font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className className
)} )}
{...props}> {...props}>
@@ -52,9 +52,12 @@ function AccordionContent({
...props ...props
}) { }) {
return ( return (
// forceMount keeps closed answers in the server-rendered HTML (needed for
// FAQPage schema); data-[state=closed]:hidden keeps them visually collapsed.
<AccordionPrimitive.Content <AccordionPrimitive.Content
data-slot="accordion-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}> {...props}>
<div className={cn("pt-0 pb-4", className)}>{children}</div> <div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
+68
View File
@@ -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 }
+60
View File
@@ -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 }
+112
View File
@@ -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,
}
+24
View File
@@ -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 }
+23
View File
@@ -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 }
+65
View File
@@ -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 }
+165
View File
@@ -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,
}
+44
View File
@@ -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 }
+27
View File
@@ -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 }
+147
View File
@@ -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,
}
+136
View File
@@ -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,
}
+20
View File
@@ -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 }
+18
View File
@@ -0,0 +1,18 @@
// Sends a custom event to Rybbit once its script has loaded; otherwise does nothing.
// Events: click_call, click_text, click_email, submit_quote_form, click_google_reviews.
export function trackEvent(name) {
if (typeof window !== "undefined" && window.rybbit) {
window.rybbit.event(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;
}
+61
View File
@@ -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, Macs 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);
}
+51
View File
@@ -0,0 +1,51 @@
// 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, MacBooks and iMacs, Chromebooks, custom-built PCs, and PlayStation and Xbox consoles. I don't repair phones or tablets.",
footnote: true,
},
{
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.",
},
];
+223
View File
@@ -0,0 +1,223 @@
// 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, Macs, Chromebooks and PlayStation and Xbox consoles 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",
"PlayStation and Xbox console repairs",
],
fixesNote:
"I repair MacBooks, iMacs and PlayStation and Xbox consoles, but not phones or tablets. I don't repair console motherboard faults.",
fixesNoteFootnote: true,
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: "PlayStation or Xbox repair", price: "from $35 + parts" },
{ item: "Other repairs (labour)", price: "$35 an hour" },
{ item: "Windows reinstall", price: "from $45" },
],
faqs: [
{
question: "Do you repair MacBooks and iMacs?",
answer:
"Yes. I repair MacBooks and iMacs as well as Windows laptops and PCs, Chromebooks and custom-built computers. I don't repair phones or tablets.",
footnote: true,
},
{
question: "Do you repair PlayStation and Xbox consoles?",
answer:
"Yes. I fix common PlayStation and Xbox problems, at the same prices as computer repairs. I don't repair console motherboard faults.",
footnote: true,
},
{
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 and Macs, 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:
"Yes. I clean up malware, adware and scam pop-ups on Macs as well as 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"],
},
};
+239
View File
@@ -0,0 +1,239 @@
// 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",
};
// Shown once in the footer. Mark Mac and console mentions with <Footnote />
// (or `footnote: true` on a FAQ or service) instead of repeating it.
export const brandDisclaimer =
"Te Atatu Computers is an independent repair service and is not affiliated with, endorsed by or authorised by Apple, Sony or Microsoft. The use of genuine manufacturer parts and the continuation of any manufacturer warranty or AppleCare coverage cannot be guaranteed.";
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: "PlayStation or Xbox repair", 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": "LocalBusiness",
"@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,
},
})),
};
}
-6
View File
@@ -1,6 +0,0 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs));
}