Modernizing + /contact rewire
This commit is contained in:
parent
9094a4d0e4
commit
b8c514013c
|
|
@ -1,150 +1,264 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { site } from "@/lib/site";
|
||||
import { useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type Status = { state: "idle" | "submitting" | "success" | "error"; message?: string };
|
||||
export default function RequestPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const prefillType = searchParams.get("type") as RequestType | null;
|
||||
|
||||
export default function ContactPage() {
|
||||
const [startedAt, setStartedAt] = useState<string>("");
|
||||
const [status, setStatus] = useState<Status>({ state: "idle" });
|
||||
const [step, setStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
type: prefillType || "",
|
||||
name: "",
|
||||
email: "",
|
||||
domain: "",
|
||||
company: "",
|
||||
message: "",
|
||||
hosting: "",
|
||||
concern: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setStartedAt(String(Date.now()));
|
||||
}, []);
|
||||
type RequestType =
|
||||
| "audit"
|
||||
| "consultation"
|
||||
| "support"
|
||||
| "tool"
|
||||
| "partnership";
|
||||
|
||||
const disabledEarly = useMemo(() => {
|
||||
if (!startedAt) return true;
|
||||
const min = Number(process.env.NEXT_PUBLIC_CONTACT_MIN_SUBMIT_SECONDS ?? 3) || 3;
|
||||
return Date.now() - Number(startedAt) < min * 1000;
|
||||
}, [startedAt]);
|
||||
const requestTypes: { id: RequestType; label: string; desc: string; icon: string }[] = [
|
||||
{
|
||||
id: "audit",
|
||||
label: "Free Audit",
|
||||
desc: "Request a free website, DNS or performance check.",
|
||||
icon: "🧠",
|
||||
},
|
||||
{
|
||||
id: "consultation",
|
||||
label: "Consultation / Quote",
|
||||
desc: "Discuss a project, hosting setup, or optimization.",
|
||||
icon: "⚙️",
|
||||
},
|
||||
{
|
||||
id: "support",
|
||||
label: "Technical Support",
|
||||
desc: "Report an issue or request hands-on help.",
|
||||
icon: "🛠️",
|
||||
},
|
||||
{
|
||||
id: "tool",
|
||||
label: "Tool Follow-Up",
|
||||
desc: "Continue from one of our free tools or reports.",
|
||||
icon: "📊",
|
||||
},
|
||||
{
|
||||
id: "partnership",
|
||||
label: "Partnership / Collaboration",
|
||||
desc: "Discuss a potential collaboration or integration.",
|
||||
icon: "🤝",
|
||||
},
|
||||
];
|
||||
|
||||
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setForm((f) => ({ ...f, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus({ state: "submitting" });
|
||||
|
||||
const form = e.currentTarget;
|
||||
const formData = new FormData(form);
|
||||
const payload = Object.fromEntries(formData.entries());
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/contact", {
|
||||
const res = await fetch("/api/request", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
body: JSON.stringify(form),
|
||||
});
|
||||
const json = (await res.json()) as { ok: boolean; error?: string };
|
||||
if (!res.ok || !json.ok) throw new Error(json.error || "Failed");
|
||||
setStatus({ state: "success", message: "Thanks! We’ll get back to you shortly." });
|
||||
form.reset();
|
||||
setStartedAt(String(Date.now())); // reset time trap
|
||||
} catch {
|
||||
setStatus({ state: "error", message: "Could not send. Please try again." });
|
||||
|
||||
if (!res.ok) throw new Error("Submission failed");
|
||||
|
||||
toast.success("Request submitted successfully!");
|
||||
router.push("/request/success");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Something went wrong. Please try again.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="container py-12">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Contact</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Tell us what you want fixed or managed. We’ll confirm scope and start fast.
|
||||
</p>
|
||||
|
||||
<form onSubmit={onSubmit} className="mt-8 space-y-5">
|
||||
{/* Honeypot (hidden from users & screen readers) */}
|
||||
<div aria-hidden className="hidden">
|
||||
<label htmlFor="website">Website</label>
|
||||
<input id="website" name="website" type="text" tabIndex={-1} autoComplete="off" />
|
||||
</div>
|
||||
|
||||
{/* Time trap */}
|
||||
<input type="hidden" name="startedAt" value={startedAt} readOnly />
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="name" className="text-sm font-medium">
|
||||
Name<span className="text-red-600"> *</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
required
|
||||
className="rounded-xl border bg-white px-3 py-2 outline-none ring-2 ring-transparent focus:ring-primary"
|
||||
placeholder="Jane Doe"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="email" className="text-sm font-medium">
|
||||
Email<span className="text-red-600"> *</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
className="rounded-xl border bg-white px-3 py-2 outline-none ring-2 ring-transparent focus:ring-primary"
|
||||
placeholder="jane@company.com"
|
||||
inputMode="email"
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="company" className="text-sm font-medium">
|
||||
Company
|
||||
</label>
|
||||
<input
|
||||
id="company"
|
||||
name="company"
|
||||
className="rounded-xl border bg-white px-3 py-2 outline-none ring-2 ring-transparent focus:ring-primary"
|
||||
placeholder="Company B.V."
|
||||
autoComplete="organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="message" className="text-sm font-medium">
|
||||
What do you need?<span className="text-red-600"> *</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
required
|
||||
rows={6}
|
||||
className="rounded-xl border bg-white px-3 py-2 outline-none ring-2 ring-transparent focus:ring-primary"
|
||||
placeholder="Briefly describe the problem or goal. Links are optional."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
By submitting, you agree we can contact you about your request. We don’t sell data.
|
||||
<main className="relative isolate min-h-screen bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
<section className="border-b border-neutral-200 dark:border-neutral-800">
|
||||
<div className="container mx-auto max-w-4xl px-6 py-16 text-center">
|
||||
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
|
||||
Start a Request
|
||||
</h1>
|
||||
<p className="mt-4 text-neutral-700 dark:text-neutral-300 text-lg">
|
||||
Choose what you’d like to do — audits, consultations, or support.
|
||||
We’ll guide you through the right steps and get back within one business day.
|
||||
</p>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status.state === "submitting" || disabledEarly}
|
||||
className="inline-flex min-w-[9rem] items-center justify-center rounded-xl bg-primary px-4 py-2 text-primary-foreground transition-opacity disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
{status.state === "submitting" ? "Sending..." : "Send"}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{status.state === "success" && (
|
||||
<div role="status" className="rounded-xl border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700">
|
||||
{status.message}
|
||||
</div>
|
||||
)}
|
||||
{status.state === "error" && (
|
||||
<div role="alert" className="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
||||
{status.message}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
<section className="container mx-auto max-w-3xl px-6 py-12">
|
||||
{/* Step 1 – Select Request Type */}
|
||||
<AnimatePresence mode="wait">
|
||||
{step === 1 && (
|
||||
<motion.div
|
||||
key="step1"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<h2 className="text-2xl font-semibold text-neutral-900 dark:text-white mb-6 text-center">
|
||||
What kind of request do you have?
|
||||
</h2>
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
{requestTypes.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => {
|
||||
setForm((f) => ({ ...f, type: t.id }));
|
||||
setStep(2);
|
||||
}}
|
||||
className={`rounded-xl border border-neutral-200 dark:border-neutral-800 p-6 text-left hover:shadow-md transition-all duration-200 ${
|
||||
form.type === t.id
|
||||
? "bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white"
|
||||
: "bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white"
|
||||
}`}
|
||||
>
|
||||
<div className="text-3xl mb-2">{t.icon}</div>
|
||||
<h3 className="font-semibold text-lg mb-1">{t.label}</h3>
|
||||
<p className="text-sm opacity-80">{t.desc}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<div className="mt-10 text-sm text-muted-foreground">
|
||||
Prefer email? Reach us at <a className="underline" href={`mailto:${site.contact.email}`}>{site.contact.email}</a>.
|
||||
</div>
|
||||
</section>
|
||||
{/* Step 2 – Request Form */}
|
||||
{step === 2 && (
|
||||
<motion.form
|
||||
key="step2"
|
||||
onSubmit={handleSubmit}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="mt-4 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl shadow-sm p-8"
|
||||
>
|
||||
<h2 className="text-xl font-semibold mb-6 text-neutral-900 dark:text-white text-center">
|
||||
{requestTypes.find((t) => t.id === form.type)?.label}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={form.name}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Show domain and technical fields only for audits / consultations */}
|
||||
{(form.type === "audit" || form.type === "consultation") && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
|
||||
Website or Domain
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="domain"
|
||||
placeholder="example.com"
|
||||
value={form.domain}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
|
||||
Main Concern
|
||||
</label>
|
||||
<select
|
||||
name="concern"
|
||||
value={form.concern}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Select one</option>
|
||||
<option value="speed">Speed / Performance</option>
|
||||
<option value="security">Security</option>
|
||||
<option value="dns">DNS / Email Issues</option>
|
||||
<option value="hosting">Hosting Migration</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
|
||||
Message / Details
|
||||
</label>
|
||||
<textarea
|
||||
name="message"
|
||||
rows={4}
|
||||
value={form.message}
|
||||
onChange={handleChange}
|
||||
placeholder="Describe your request..."
|
||||
className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex justify-between items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(1)}
|
||||
className="text-sm text-neutral-600 dark:text-neutral-400 hover:underline"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white font-medium px-6 py-2 hover:opacity-90 transition"
|
||||
>
|
||||
{loading ? "Submitting..." : "Submit Request"}
|
||||
</button>
|
||||
</div>
|
||||
</motion.form>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,42 @@
|
|||
"use client"; export default function GlobalError({error,reset}:{error:Error&{digest?:string},reset:()=>void}){return (<section className="container py-16 text-center"><h1 className="text-3xl font-semibold tracking-tight">Something went wrong</h1><p className="mt-2 text-muted-foreground">An unexpected error occurred. Please try again.</p><div className="mt-6 flex items-center justify-center gap-3"><button onClick={()=>reset()} className="btn-primary">Retry</button></div></section>);}
|
||||
"use client";
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
return (
|
||||
<main className="relative isolate min-h-screen flex items-center justify-center bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
<section className="container mx-auto max-w-lg px-6 py-16 text-center animate-[fadeInUp_0.6s_ease_forwards]">
|
||||
<div className="mx-auto inline-flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-r from-red-500 via-pink-600 to-purple-600 text-white text-3xl shadow-lg">
|
||||
⚠️
|
||||
</div>
|
||||
|
||||
<h1 className="mt-6 text-4xl font-bold tracking-tight bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
|
||||
Something went wrong
|
||||
</h1>
|
||||
|
||||
<p className="mt-4 text-neutral-700 dark:text-neutral-300">
|
||||
An unexpected error occurred. Please try again.
|
||||
</p>
|
||||
|
||||
{error?.digest && (
|
||||
<p className="mt-2 text-xs text-neutral-500 dark:text-neutral-500">
|
||||
Error ID: <code>{error.digest}</code>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex items-center justify-center">
|
||||
<button
|
||||
onClick={() => reset()}
|
||||
className="inline-flex items-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white px-6 py-2.5 text-sm font-medium hover:opacity-90 transition"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,254 @@
|
|||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
export const metadata: Metadata = { title: "Free tools", description: "Quick checks for email, edge security headers and DNS health.", robots: { index: false, follow: true }, alternates: { canonical: "/free" } };
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Free Tools & Audits — Van Hunen IT",
|
||||
description:
|
||||
"Run free reliability, DNS, and performance checks or request quick IT audits. Diagnose, optimize, and secure your website in minutes.",
|
||||
robots: { index: true, follow: true },
|
||||
alternates: { canonical: "/free" },
|
||||
};
|
||||
|
||||
export default function FreeIndex() {
|
||||
const tools = [
|
||||
{ title:"Email Auth Check (DMARC/SPF/DKIM)", excerpt:"See if DMARC, SPF and common DKIM selectors are configured.", href:"/free/email-check" },
|
||||
{ title:"Edge Security Headers Check", excerpt:"Inspect HSTS, CSP, X-Frame-Options, Referrer-Policy and more.", href:"/free/edge-check" },
|
||||
{ title:"DNS Health Snapshot", excerpt:"Look up A/AAAA, MX, NS and CAA records at a glance.", href:"/free/dns-health" }
|
||||
{
|
||||
title: "Web Reliability Check",
|
||||
excerpt:
|
||||
"Test your website’s uptime, SSL, and DNS performance — get an instant reliability grade (A–F).",
|
||||
href: "/free/web-reliability-check",
|
||||
},
|
||||
{
|
||||
title: "DNS Health Analyzer",
|
||||
excerpt:
|
||||
"Check your domain’s DNS setup for missing SPF, DMARC, or CAA records and other critical issues.",
|
||||
href: "/free/dns-health-analyzer",
|
||||
},
|
||||
{
|
||||
title: "Website Speed Test (Lite)",
|
||||
excerpt:
|
||||
"Run a Lighthouse-based performance test and see how your site scores for speed, SEO, and accessibility.",
|
||||
href: "/free/website-speed-test",
|
||||
},
|
||||
{
|
||||
title: "Server Header Inspector",
|
||||
excerpt:
|
||||
"Check your site’s security headers like HSTS, CSP, and Referrer-Policy to spot missing protections.",
|
||||
href: "/free/server-header-inspector",
|
||||
},
|
||||
{
|
||||
title: "Cloudflare Optimization Checker",
|
||||
excerpt:
|
||||
"Analyze if your Cloudflare settings are optimized for caching, SSL, and performance.",
|
||||
href: "/free/cloudflare-checker",
|
||||
},
|
||||
{
|
||||
title: "Uptime Monitor (Free Tier)",
|
||||
excerpt:
|
||||
"Set up free uptime monitoring for your website and receive downtime alerts instantly.",
|
||||
href: "/free/uptime-monitor",
|
||||
},
|
||||
{
|
||||
title: "SSL/TLS Expiry Tracker",
|
||||
excerpt:
|
||||
"Track when your SSL certificate expires and receive an automated renewal reminder.",
|
||||
href: "/free/ssl-expiry-tracker",
|
||||
},
|
||||
{
|
||||
title: "Website Backup Readiness Test",
|
||||
excerpt:
|
||||
"Check if your website is protected by regular automated backups and detect missing layers.",
|
||||
href: "/free/backup-readiness-test",
|
||||
},
|
||||
{
|
||||
title: "Email Deliverability Tester",
|
||||
excerpt:
|
||||
"Send a test email and verify SPF, DKIM, and DMARC for maximum inbox reliability.",
|
||||
href: "/free/email-deliverability-tester",
|
||||
},
|
||||
{
|
||||
title: "Docker Deployment Readiness Tool",
|
||||
excerpt:
|
||||
"Scan your public repository for Docker configuration quality and best practices.",
|
||||
href: "/free/docker-readiness",
|
||||
},
|
||||
{
|
||||
title: "Kubernetes Cost Estimator",
|
||||
excerpt:
|
||||
"Estimate infrastructure costs for your Kubernetes or k3s setup based on your resources.",
|
||||
href: "/free/kubernetes-cost-estimator",
|
||||
},
|
||||
{
|
||||
title: "Minecraft Server Performance Checker",
|
||||
excerpt:
|
||||
"Test TPS, latency, and plugin performance of your Minecraft server in seconds.",
|
||||
href: "/free/minecraft-checker",
|
||||
},
|
||||
{
|
||||
title: "Site Downtime Simulation Demo",
|
||||
excerpt:
|
||||
"Simulate downtime to visualize business impact and recovery time objectives.",
|
||||
href: "/free/downtime-simulator",
|
||||
},
|
||||
{
|
||||
title: "Automation ROI Calculator",
|
||||
excerpt:
|
||||
"See how much time and cost you could save by automating repetitive IT or admin tasks.",
|
||||
href: "/free/automation-roi-calculator",
|
||||
},
|
||||
{
|
||||
title: "VPS Performance Benchmark Tool",
|
||||
excerpt:
|
||||
"Run a quick benchmark on your VPS or server to test CPU, I/O, and disk performance.",
|
||||
href: "/free/vps-benchmark",
|
||||
},
|
||||
];
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: "20-Minute Tech Health Check",
|
||||
excerpt:
|
||||
"A quick diagnostic session covering your hosting, DNS, SSL, and uptime setup.",
|
||||
href: "/free/tech-health-check",
|
||||
},
|
||||
{
|
||||
title: "DNS & Mail Deliverability Audit",
|
||||
excerpt:
|
||||
"Get a personalized DNS and email configuration audit to fix SPF, DKIM, and DMARC issues.",
|
||||
href: "/free/dns-mail-audit",
|
||||
},
|
||||
{
|
||||
title: "Website Speed & Optimization Preview",
|
||||
excerpt:
|
||||
"Receive a short performance report and improvement roadmap for your website.",
|
||||
href: "/free/website-optimization-preview",
|
||||
},
|
||||
{
|
||||
title: "Cloudflare Setup Review",
|
||||
excerpt:
|
||||
"Let us analyze your Cloudflare configuration for speed, caching, and SSL performance.",
|
||||
href: "/free/cloudflare-review",
|
||||
},
|
||||
{
|
||||
title: "Security Headers Checkup",
|
||||
excerpt:
|
||||
"We’ll scan your site headers for vulnerabilities and send a one-page hardening report.",
|
||||
href: "/free/security-checkup",
|
||||
},
|
||||
{
|
||||
title: "Backup & Recovery Simulation",
|
||||
excerpt:
|
||||
"Test your current backup setup and identify gaps in your disaster recovery plan.",
|
||||
href: "/free/backup-simulation",
|
||||
},
|
||||
{
|
||||
title: "Uptime Reliability Snapshot",
|
||||
excerpt:
|
||||
"We’ll monitor your domain for 24 hours and share an uptime report with insights.",
|
||||
href: "/free/reliability-snapshot",
|
||||
},
|
||||
{
|
||||
title: "Automation Opportunity Call",
|
||||
excerpt:
|
||||
"Tell us your weekly routines — we’ll identify tasks that could be automated instantly.",
|
||||
href: "/free/automation-call",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="container py-12">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Free tools</h1>
|
||||
<p className="mt-2 text-muted-foreground">Helpful checks you can run in seconds. No sign-up needed.</p>
|
||||
<div className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{tools.map((t)=>(<article key={t.href} className="card"><h2 className="text-lg font-semibold">{t.title}</h2><p className="mt-2 text-sm text-muted-foreground">{t.excerpt}</p><Link href={t.href} className="btn-primary mt-4 no-underline w-full text-center">Open</Link></article>))}
|
||||
</div>
|
||||
</section>
|
||||
<main className="relative isolate min-h-screen bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
{/* --- Header --- */}
|
||||
<section className="relative overflow-hidden border-b border-neutral-200 dark:border-neutral-800">
|
||||
<div className="container mx-auto max-w-5xl px-6 py-20 text-center">
|
||||
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
|
||||
Free Tools & Quick Audits
|
||||
</h1>
|
||||
<p className="mt-4 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
|
||||
Diagnose, benchmark, and optimize your IT setup with a collection of
|
||||
free utilities and expert mini-audits.
|
||||
No sign-up required, privacy-friendly, and built by{" "}
|
||||
<span className="font-semibold">Van Hunen IT</span>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Automated Tools --- */}
|
||||
<section className="container mx-auto max-w-6xl px-6 py-16">
|
||||
<h2 className="text-2xl font-bold mb-10 text-neutral-900 dark:text-white text-center">
|
||||
Automated Tools
|
||||
</h2>
|
||||
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{tools.map((t, i) => (
|
||||
<article
|
||||
key={t.href}
|
||||
className="group relative overflow-hidden rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-8 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur animate-[fadeInUp_0.6s_ease_forwards]"
|
||||
style={{ animationDelay: `${i * 60}ms` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/0 via-transparent to-purple-100/0 dark:from-blue-900/10 dark:to-purple-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
{t.title}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">
|
||||
{t.excerpt}
|
||||
</p>
|
||||
<Link
|
||||
href={t.href}
|
||||
className="mt-6 inline-block w-full rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white text-center py-2.5 text-sm font-medium hover:opacity-90 transition"
|
||||
>
|
||||
Open Tool
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Free Audits & Services --- */}
|
||||
<section className="container mx-auto max-w-6xl px-6 py-16 border-t border-neutral-200 dark:border-neutral-800">
|
||||
<h2 className="text-2xl font-bold mb-10 text-neutral-900 dark:text-white text-center">
|
||||
Free Audits & Quick Services
|
||||
</h2>
|
||||
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{services.map((s, i) => (
|
||||
<article
|
||||
key={s.href}
|
||||
className="group relative overflow-hidden rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-8 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur animate-[fadeInUp_0.6s_ease_forwards]"
|
||||
style={{ animationDelay: `${i * 80}ms` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/0 via-transparent to-purple-100/0 dark:from-blue-900/10 dark:to-purple-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-white">
|
||||
{s.title}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">
|
||||
{s.excerpt}
|
||||
</p>
|
||||
<Link
|
||||
href={s.href}
|
||||
className="mt-6 inline-block w-full rounded-lg bg-gradient-to-r from-purple-600 via-blue-600 to-purple-600 text-white text-center py-2.5 text-sm font-medium hover:opacity-90 transition"
|
||||
>
|
||||
Request
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- CTA Banner --- */}
|
||||
<section className="border-t border-neutral-200 dark:border-neutral-800 bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white py-16 mt-12 text-center">
|
||||
<div className="container mx-auto max-w-4xl px-6">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold mb-4">
|
||||
Not Sure Where to Start?
|
||||
</h2>
|
||||
<p className="text-base sm:text-lg text-white/90 mb-6">
|
||||
Book a free 15-minute consultation and we’ll help you pick the right
|
||||
checks or services for your setup.
|
||||
</p>
|
||||
<Link
|
||||
href="/contact?topic=consultation"
|
||||
className="inline-block rounded-lg bg-white text-blue-700 font-semibold py-3 px-6 shadow hover:shadow-md transition"
|
||||
>
|
||||
Book Free Call
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,3 +10,14 @@ a{@apply underline-offset-2} .btn-primary{@apply inline-flex items-center justif
|
|||
@media (prefers-reduced-motion: reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}
|
||||
.skip-link{position:absolute;left:-9999px;top:0;z-index:9999;padding:.5rem .75rem;background:hsl(var(--primary));color:hsl(var(--primary-foreground));border-radius:.75rem}
|
||||
.skip-link:focus-visible{left:.5rem;top:.5rem;outline:0}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,36 @@
|
|||
import Link from 'next/link'; export default function NotFound(){return (<section className='container py-16 text-center'><h1 className='text-3xl font-semibold tracking-tight'>Page not found</h1><p className='mt-2 text-muted-foreground'>The page you’re looking for doesn’t exist.</p><div className='mt-6 flex items-center justify-center gap-3'><Link href='/' className='btn-primary no-underline'>Go home</Link><Link href='/services' className='no-underline rounded-xl border px-5 py-3'>Browse services</Link></div></section>);}
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="relative isolate min-h-screen flex items-center justify-center bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
<section className="container mx-auto max-w-lg px-6 py-16 text-center animate-[fadeInUp_0.6s_ease_forwards]">
|
||||
<div className="mx-auto inline-flex h-20 w-20 items-center justify-center rounded-full bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white text-3xl shadow-lg">
|
||||
404
|
||||
</div>
|
||||
|
||||
<h1 className="mt-6 text-4xl font-bold tracking-tight bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
|
||||
Page not found
|
||||
</h1>
|
||||
|
||||
<p className="mt-4 text-neutral-700 dark:text-neutral-300">
|
||||
The page you’re looking for doesn’t exist or has been moved.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-wrap items-center justify-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white px-6 py-2.5 text-sm font-medium hover:opacity-90 transition no-underline"
|
||||
>
|
||||
Go home
|
||||
</Link>
|
||||
<Link
|
||||
href="/services"
|
||||
className="inline-flex items-center rounded-lg border border-neutral-300 dark:border-neutral-700 px-6 py-2.5 text-sm font-medium text-neutral-800 dark:text-neutral-200 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition no-underline"
|
||||
>
|
||||
Browse services
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
159
web/app/page.tsx
159
web/app/page.tsx
|
|
@ -1,15 +1,15 @@
|
|||
import type { Metadata } from "next";
|
||||
import Hero from "@/components/Hero";
|
||||
import ServiceCards from "@/components/ServiceCards";
|
||||
import Process from "@/components/Process";
|
||||
import Pricing from "@/components/Pricing";
|
||||
import Testimonials from "@/components/Testimonials";
|
||||
import FAQ from "@/components/FAQ";
|
||||
import CTA from "@/components/CTA";
|
||||
import { services } from "@/lib/services";
|
||||
import { SERVICE_CATEGORIES } from "@/lib/services";
|
||||
import { plans } from "@/lib/pricing";
|
||||
import { site } from "@/lib/site";
|
||||
import JsonLd from "@/components/JsonLd";
|
||||
import Link from "next/link";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Productized IT for SMBs",
|
||||
|
|
@ -23,7 +23,7 @@ export default function HomePage() {
|
|||
name: site.name,
|
||||
url: site.url,
|
||||
logo: site.org.logo,
|
||||
sameAs: site.org.sameAs
|
||||
sameAs: site.org.sameAs,
|
||||
};
|
||||
|
||||
const faqLd = {
|
||||
|
|
@ -33,36 +33,157 @@ export default function HomePage() {
|
|||
{
|
||||
"@type": "Question",
|
||||
name: "How fast can you start?",
|
||||
acceptedAnswer: { "@type": "Answer", text: "Most sprints start within 2–3 business days after scope confirmation." }
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Most sprints start within 2–3 business days after scope confirmation.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Do you work under NDA?",
|
||||
acceptedAnswer: { "@type": "Answer", text: "Yes—mutual NDA available on request; we keep credentials least-privilege." }
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Yes—mutual NDA available on request; we keep credentials least-privilege.",
|
||||
},
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
name: "Can we switch providers later?",
|
||||
acceptedAnswer: { "@type": "Answer", text: "Yes. Everything is documented; you own the accounts and artifacts." }
|
||||
}
|
||||
]
|
||||
acceptedAnswer: {
|
||||
"@type": "Answer",
|
||||
text: "Yes. Everything is documented; you own the accounts and artifacts.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Service category summaries
|
||||
const categoryDescriptions: Record<string, string> = {
|
||||
"infrastructure-devops":
|
||||
"Reliable servers, containers, and automation—Docker, Kubernetes, Cloudflare, and backups that just work.",
|
||||
"web-performance":
|
||||
"Make your website fast, secure, and consistently available. Core Web Vitals, deliverability, and uptime care.",
|
||||
"dev-platforms":
|
||||
"Private Git hosting, CI/CD, staging environments, and secrets management—developer efficiency built in.",
|
||||
"migrations":
|
||||
"Move from legacy hosting or apps to modern, containerized infrastructure with zero-to-minimal downtime.",
|
||||
"minecraft":
|
||||
"Managed Minecraft servers, plugin development, and performance tuning for creators and communities.",
|
||||
"web-dev":
|
||||
"Modern websites and headless CMS setups designed for speed, SEO, and maintainability.",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="relative isolate min-h-screen bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
<Hero />
|
||||
<ServiceCards services={services} />
|
||||
|
||||
{/* --- Service Areas --- */}
|
||||
<section className="relative overflow-hidden border-b border-neutral-200 dark:border-neutral-800">
|
||||
<div className="container mx-auto max-w-6xl px-4 py-20 text-center">
|
||||
<h2 className="text-4xl font-bold tracking-tight sm:text-5xl bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
|
||||
Service Areas
|
||||
</h2>
|
||||
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
|
||||
Fixed-scope sprints and managed plans for infrastructure, performance, and development.
|
||||
Each category leads to specialized services tailored to small and mid-sized teams.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto max-w-6xl px-4 pb-20">
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{Object.entries(SERVICE_CATEGORIES).map(([id, cat], i) => (
|
||||
<article
|
||||
key={id}
|
||||
className="group relative overflow-hidden rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-8 shadow-sm hover:shadow-lg transition-all duration-300 backdrop-blur"
|
||||
style={{ animation: `fadeInUp 0.6s ease forwards`, animationDelay: `${i * 80}ms` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/0 via-transparent to-purple-100/0 dark:from-blue-900/10 dark:to-purple-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<h3 className="text-xl font-semibold tracking-tight text-neutral-900 dark:text-white">
|
||||
{cat.label}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">
|
||||
{categoryDescriptions[id] ?? ""}
|
||||
</p>
|
||||
<Link
|
||||
href={`/services#${cat.anchor}`}
|
||||
className="mt-5 inline-flex items-center text-sm font-medium text-blue-600 hover:underline"
|
||||
>
|
||||
Explore services →
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-14 text-center">
|
||||
<Link
|
||||
href="/services"
|
||||
className="inline-flex items-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white px-6 py-3 text-sm font-medium hover:opacity-90 transition"
|
||||
>
|
||||
View all services
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Process --- */}
|
||||
<Process />
|
||||
<Pricing plans={plans} />
|
||||
<Testimonials />
|
||||
<FAQ items={[
|
||||
{ q: "How fast can you start?", a: "Most sprints start within 2–3 business days after scope confirmation." },
|
||||
{ q: "Do you work under NDA?", a: "Yes—mutual NDA available on request; we keep credentials least-privilege." },
|
||||
{ q: "What’s your guarantee?", a: "We show proof of outcomes. If scope isn’t met, we make it right or refund the sprint fee." }
|
||||
]} />
|
||||
<CTA />
|
||||
|
||||
{/* --- Pricing --- */}
|
||||
<section className="relative py-24 bg-gradient-to-br from-blue-50/40 via-white to-purple-50/40 dark:from-blue-950/10 dark:to-purple-950/10">
|
||||
<div className="container mx-auto max-w-6xl px-4">
|
||||
<Pricing plans={plans} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Testimonials --- */}
|
||||
<section className="relative py-24 border-t border-neutral-200 dark:border-neutral-800">
|
||||
<div className="container mx-auto max-w-6xl px-4">
|
||||
<Testimonials />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- FAQ --- */}
|
||||
<section className="relative py-24 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
<div className="container mx-auto max-w-4xl px-4">
|
||||
<FAQ
|
||||
items={[
|
||||
{
|
||||
q: "How fast can you start?",
|
||||
a: "Most sprints start within 2–3 business days after scope confirmation.",
|
||||
},
|
||||
{
|
||||
q: "Do you work under NDA?",
|
||||
a: "Yes—mutual NDA available on request; we keep credentials least-privilege.",
|
||||
},
|
||||
{
|
||||
q: "What’s your guarantee?",
|
||||
a: "We show proof of outcomes. If scope isn’t met, we make it right or refund the sprint fee.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Call to Action --- */}
|
||||
<section className="relative py-24 text-center bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white">
|
||||
<div className="container mx-auto max-w-4xl px-6">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">
|
||||
Ready to make your IT infrastructure reliable and scalable?
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-blue-100">
|
||||
Talk with Van Hunen IT — get a fixed-scope sprint or managed plan that fits your business.
|
||||
</p>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="mt-8 inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition"
|
||||
>
|
||||
Contact Us
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<JsonLd data={orgLd} />
|
||||
<JsonLd data={faqLd} />
|
||||
</>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,40 +1,119 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Metadata } from "next";
|
||||
import { getAllServices, SERVICE_CATEGORIES, type ServiceCategoryId } from "@/lib/services";
|
||||
import ServiceCard from "@/components/ServiceCard";
|
||||
import Link from "next/link";
|
||||
import { services } from "@/lib/services";
|
||||
import JsonLd from "@/components/JsonLd";
|
||||
import { site } from "@/lib/site";
|
||||
|
||||
export const revalidate = 86400;
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Services",
|
||||
description: "Fixed-scope sprints and managed plans for email, Cloudflare, web and ops.",
|
||||
alternates: { canonical: "/services" }
|
||||
title: "Services — Van Hunen IT",
|
||||
description:
|
||||
"Explore Van Hunen IT's fixed-scope sprints and managed solutions — from VPS hardening and Docker orchestration to Kubernetes, Cloudflare, Core Web Vitals, and Minecraft operations.",
|
||||
alternates: { canonical: "/services" },
|
||||
};
|
||||
|
||||
export default function ServicesIndex() {
|
||||
const collectionLd = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "CollectionPage",
|
||||
name: "Services",
|
||||
url: `${site.url}/services`,
|
||||
hasPart: services.map((s) => ({ "@type": "Service", name: s.title, description: s.excerpt, url: `${site.url}/services/${s.slug}` }))
|
||||
};
|
||||
export default function ServicesPage() {
|
||||
const services = getAllServices();
|
||||
const categories: ServiceCategoryId[] = Object.keys(SERVICE_CATEGORIES) as ServiceCategoryId[];
|
||||
|
||||
return (
|
||||
<section className="container py-12">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Services</h1>
|
||||
<div className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{services.map(s => (
|
||||
<article key={s.slug} className="card">
|
||||
<div className="text-xs uppercase tracking-wide text-accent">{s.category}</div>
|
||||
<h3 className="mt-1 text-lg font-semibold">{s.title}</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{s.excerpt}</p>
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<span className="text-sm font-semibold">{s.price}</span>
|
||||
<Link className="no-underline rounded-xl border px-3 py-2 text-sm" href={`/services/${s.slug}`}>View</Link>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
<main className="relative isolate min-h-screen bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
{/* --- Hero Header --- */}
|
||||
<section className="relative overflow-hidden border-b border-neutral-200 dark:border-neutral-800">
|
||||
<div className="container mx-auto max-w-6xl px-4 py-20 text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
|
||||
Professional IT Services
|
||||
</h1>
|
||||
<p className="mt-5 text-lg text-neutral-700 dark:text-neutral-300 max-w-2xl mx-auto">
|
||||
Fixed-scope sprints and managed plans for{" "}
|
||||
<strong>VPS</strong>, <strong>Docker</strong>, <strong>Kubernetes</strong>,{" "}
|
||||
<strong>Cloudflare</strong>, <strong>Core Web Vitals</strong>, and{" "}
|
||||
<strong>Minecraft</strong>. Clear outcomes, flat pricing, and proof you can keep.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
||||
{categories.map((id) => (
|
||||
<a
|
||||
key={id}
|
||||
href={`#${SERVICE_CATEGORIES[id].anchor}`}
|
||||
className="rounded-full border border-neutral-300 dark:border-neutral-700 px-4 py-1.5 text-sm font-medium text-neutral-700 dark:text-neutral-300 hover:border-blue-500 hover:text-blue-600 dark:hover:text-blue-400 transition"
|
||||
>
|
||||
{SERVICE_CATEGORIES[id].label}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Service Categories --- */}
|
||||
<div className="container mx-auto max-w-6xl px-4 py-16">
|
||||
{categories.map((id, index) => {
|
||||
const list = services.filter((s) => s.category === id);
|
||||
if (!list.length) return null;
|
||||
|
||||
return (
|
||||
<section
|
||||
key={id}
|
||||
id={SERVICE_CATEGORIES[id].anchor}
|
||||
aria-labelledby={`${id}-heading`}
|
||||
className={`relative py-16 scroll-mt-24 ${
|
||||
index % 2 === 0
|
||||
? "bg-white dark:bg-neutral-900/50"
|
||||
: "bg-neutral-50 dark:bg-neutral-900/30"
|
||||
} rounded-2xl mb-12 shadow-sm`}
|
||||
>
|
||||
<div className="absolute inset-0 -z-10 bg-gradient-to-br from-blue-50/10 via-transparent to-purple-50/10 dark:from-blue-950/10 dark:to-purple-950/10 rounded-2xl" />
|
||||
|
||||
<div className="px-4 sm:px-8">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
||||
<h2
|
||||
id={`${id}-heading`}
|
||||
className="text-2xl sm:text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent"
|
||||
>
|
||||
{SERVICE_CATEGORIES[id].label}
|
||||
</h2>
|
||||
|
||||
<Link
|
||||
href={`/services#${SERVICE_CATEGORIES[id].anchor}`}
|
||||
className="mt-3 sm:mt-0 inline-flex items-center text-sm font-medium text-blue-600 hover:underline dark:text-blue-400"
|
||||
>
|
||||
Jump to top ↑
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{list.map((svc, i) => (
|
||||
<div
|
||||
key={svc.slug}
|
||||
className="animate-[fadeInUp_0.6s_ease_forwards]"
|
||||
style={{ animationDelay: `${i * 80}ms` }}
|
||||
>
|
||||
<ServiceCard svc={svc} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<JsonLd data={collectionLd} />
|
||||
</section>
|
||||
|
||||
{/* --- CTA footer --- */}
|
||||
<section className="relative py-24 text-center bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white">
|
||||
<div className="container mx-auto max-w-4xl px-6">
|
||||
<h2 className="text-3xl sm:text-4xl font-bold tracking-tight">
|
||||
Ready to optimize your IT infrastructure?
|
||||
</h2>
|
||||
<p className="mt-4 text-lg text-blue-100">
|
||||
Get in touch to discuss your goals — we’ll help you choose the right sprint or managed plan.
|
||||
</p>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="mt-8 inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition"
|
||||
>
|
||||
Contact Us
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,25 @@ import Link from "next/link";
|
|||
|
||||
export default function CTA() {
|
||||
return (
|
||||
<section className="container py-14">
|
||||
<div className="card flex flex-col items-start gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold">Ready to ship a fix or start care?</h3>
|
||||
<p className="text-sm text-muted-foreground">Book a free 20-minute check. Clear scope, then we execute.</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Link href="/contact" className="btn-primary no-underline">Book free check</Link>
|
||||
<Link href="/services" className="no-underline rounded-xl border px-5 py-3">Browse services</Link>
|
||||
<section className="relative py-24 text-center bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white">
|
||||
<div className="container mx-auto max-w-3xl px-6">
|
||||
<h3 className="text-3xl font-bold tracking-tight">Ready to ship a fix or start care?</h3>
|
||||
<p className="mt-3 text-lg text-blue-100">
|
||||
Book a free 20-minute check. Clear scope, then we execute.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap justify-center gap-4">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center rounded-lg bg-white text-blue-700 font-semibold px-6 py-3 hover:bg-blue-50 transition"
|
||||
>
|
||||
Book free check
|
||||
</Link>
|
||||
<Link
|
||||
href="/services"
|
||||
className="inline-flex items-center rounded-lg border border-white/70 px-6 py-3 text-white font-medium hover:bg-white/10 transition"
|
||||
>
|
||||
Browse services
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,25 @@ type QA = { q: string; a: string };
|
|||
|
||||
export default function FAQ({ items }: { items: QA[] }) {
|
||||
return (
|
||||
<section className="container py-14">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">FAQ</h2>
|
||||
<div className="mt-6 grid gap-4">
|
||||
{items.map((x, i) => (
|
||||
<details key={i} className="rounded-2xl border p-5">
|
||||
<summary className="cursor-pointer text-base font-medium">{x.q}</summary>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{x.a}</p>
|
||||
</details>
|
||||
))}
|
||||
<section className="relative py-24 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
<div className="container mx-auto max-w-4xl px-4">
|
||||
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent text-center">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<div className="mt-10 space-y-4">
|
||||
{items.map((x, i) => (
|
||||
<details
|
||||
key={i}
|
||||
className="group rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-5 shadow-sm hover:shadow-md transition"
|
||||
>
|
||||
<summary className="cursor-pointer text-lg font-medium text-neutral-900 dark:text-white flex items-center justify-between">
|
||||
{x.q}
|
||||
<span className="transition-transform group-open:rotate-180">⌄</span>
|
||||
</summary>
|
||||
<p className="mt-3 text-sm text-neutral-700 dark:text-neutral-300">{x.a}</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,27 +1,52 @@
|
|||
import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="border-t">
|
||||
<div className="container grid gap-6 py-10 sm:grid-cols-3">
|
||||
<footer className="relative border-t border-neutral-200 dark:border-neutral-800 bg-gradient-to-b from-white via-neutral-50 to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
<div className="container grid gap-8 py-16 sm:grid-cols-3">
|
||||
<div>
|
||||
<div className="text-lg font-semibold">Van Hunen IT</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Fixes in days. Uptime for months.</p>
|
||||
<div className="text-xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
|
||||
Van Hunen IT
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Fixes in days. Uptime for months.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm font-semibold">Links</div>
|
||||
<ul className="mt-2 space-y-1 text-sm">
|
||||
<li><Link href="/services" className="no-underline hover:underline">Services</Link></li>
|
||||
<li><Link href="/free" className="no-underline hover:underline">Free tools</Link></li>
|
||||
<li><Link href="/contact" className="no-underline hover:underline">Contact</Link></li>
|
||||
<li><Link href="/privacy" className="no-underline hover:underline">Privacy</Link></li>
|
||||
<li><Link href="/terms" className="no-underline hover:underline">Terms</Link></li>
|
||||
<li><Link href="/cookie" className="no-underline hover:underline">Cookie</Link></li>
|
||||
<div className="text-sm font-semibold text-neutral-900 dark:text-white">Links</div>
|
||||
<ul className="mt-3 space-y-1 text-sm">
|
||||
{[
|
||||
{ href: "/services", label: "Services" },
|
||||
{ href: "/free", label: "Free tools" },
|
||||
{ href: "/contact", label: "Contact" },
|
||||
{ href: "/privacy", label: "Privacy" },
|
||||
{ href: "/terms", label: "Terms" },
|
||||
{ href: "/cookie", label: "Cookie" },
|
||||
].map((l) => (
|
||||
<li key={l.href}>
|
||||
<Link
|
||||
href={l.href}
|
||||
className="no-underline text-neutral-700 dark:text-neutral-300 hover:underline"
|
||||
>
|
||||
{l.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm font-semibold">Contact</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">Email: <a href="mailto:hello@vanhunen.it" className="underline">hello@vanhunen.it</a></p>
|
||||
<p className="mt-2 text-xs text-muted-foreground">© {new Date().getFullYear()} Van Hunen IT. All rights reserved.</p>
|
||||
<div className="text-sm font-semibold text-neutral-900 dark:text-white">Contact</div>
|
||||
<p className="mt-3 text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Email:{" "}
|
||||
<a href="mailto:hello@vanhunen.it" className="underline">
|
||||
hello@vanhunen.it
|
||||
</a>
|
||||
</p>
|
||||
<p className="mt-3 text-xs text-neutral-500 dark:text-neutral-500">
|
||||
© {new Date().getFullYear()} Van Hunen IT. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,35 @@
|
|||
import Link from "next/link";
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 z-40 w-full border-b bg-background/80 backdrop-blur">
|
||||
<header className="sticky top-0 z-50 w-full border-b border-neutral-200 dark:border-neutral-800 bg-white/80 dark:bg-neutral-950/70 backdrop-blur-md">
|
||||
<div className="container flex items-center justify-between py-4">
|
||||
<Link href="/" className="flex items-center gap-2 no-underline">
|
||||
<span className="text-lg font-semibold">Van Hunen IT</span>
|
||||
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
|
||||
Van Hunen IT
|
||||
</span>
|
||||
</Link>
|
||||
<nav aria-label="Main" className="flex items-center gap-2 text-sm">
|
||||
{[{ href: "/services", label: "Services" },{ href: "/free", label: "Free tools" },{ href: "/contact", label: "Contact" },{ href: "/privacy", label: "Privacy" },{ href: "/terms", label: "Terms" }].map((item) => (
|
||||
<Link key={item.href} href={item.href} className="no-underline rounded-lg px-3 py-2 hover:bg-muted focus-visible:bg-muted transition">{item.label}</Link>
|
||||
<nav aria-label="Main" className="flex items-center gap-1 text-sm">
|
||||
{[
|
||||
{ href: "/services", label: "Services" },
|
||||
{ href: "/free", label: "Free tools" },
|
||||
{ href: "/contact", label: "Contact" },
|
||||
].map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="no-underline rounded-lg px-3 py-2 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 transition"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<Link href="/contact" className="btn-primary no-underline">Start now</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="hidden sm:inline-flex items-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white px-4 py-2 text-sm font-medium hover:opacity-90 transition"
|
||||
>
|
||||
Start now
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,29 +2,40 @@ import Link from "next/link";
|
|||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section className="container py-14 sm:py-20">
|
||||
<div className="grid items-center gap-8 sm:grid-cols-2">
|
||||
<section className="relative overflow-hidden border-b border-neutral-200 dark:border-neutral-800 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
<div className="container mx-auto grid items-center gap-12 py-24 sm:grid-cols-2 px-6">
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold tracking-tight sm:text-5xl">
|
||||
Fixes in days. <span className="text-primary">Uptime</span> for months.
|
||||
<h1 className="text-5xl sm:text-6xl font-extrabold tracking-tight leading-tight bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
|
||||
Fixes in days. <br /> <span className="text-blue-600 dark:text-blue-400">Uptime</span> for months.
|
||||
</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
Productized IT for small businesses: inbox-ready email, secure Cloudflare/DNS,
|
||||
and website care—clear scope and flat pricing.
|
||||
<p className="mt-6 text-lg text-neutral-700 dark:text-neutral-300 max-w-lg">
|
||||
Productized IT for small businesses: inbox-ready email, secure Cloudflare & DNS, and website care—
|
||||
clear scope, flat pricing, and measurable proof.
|
||||
</p>
|
||||
<div className="mt-6 flex gap-3">
|
||||
<Link href="/contact" className="btn-primary no-underline">Start now</Link>
|
||||
<Link href="/services" className="no-underline rounded-xl border px-5 py-3">See services</Link>
|
||||
<div className="mt-8 flex flex-wrap gap-4">
|
||||
<Link
|
||||
href="/contact"
|
||||
className="inline-flex items-center rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white px-6 py-3 text-sm font-medium hover:opacity-90 transition"
|
||||
>
|
||||
Start now
|
||||
</Link>
|
||||
<Link
|
||||
href="/services"
|
||||
className="inline-flex items-center rounded-lg border border-neutral-300 dark:border-neutral-700 px-6 py-3 text-sm font-medium hover:bg-neutral-50 dark:hover:bg-neutral-800 transition"
|
||||
>
|
||||
See services
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<ul className="space-y-3 text-sm">
|
||||
|
||||
<div className="rounded-2xl bg-white/70 dark:bg-neutral-900/70 backdrop-blur p-8 shadow-lg border border-neutral-200 dark:border-neutral-800 animate-[fadeInUp_0.8s_ease_forwards]">
|
||||
<ul className="space-y-3 text-sm text-neutral-800 dark:text-neutral-200">
|
||||
<li>✅ DMARC aligned, spoofing blocked</li>
|
||||
<li>✅ Cloudflare WAF & HTTP/3, faster TTFB</li>
|
||||
<li>✅ Daily backups with restore tests</li>
|
||||
<li>✅ Uptime watch + incident notes</li>
|
||||
</ul>
|
||||
<div className="mt-5 rounded-xl bg-muted p-4 text-sm text-muted-foreground">
|
||||
<div className="mt-6 rounded-xl bg-neutral-100 dark:bg-neutral-800 p-4 text-sm text-neutral-600 dark:text-neutral-300">
|
||||
We agree scope up front and show proof: before/after reports and restore tests.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,24 +3,45 @@ import type { Plan } from "@/lib/pricing";
|
|||
|
||||
export default function Pricing({ plans }: { plans: Plan[] }) {
|
||||
return (
|
||||
<section className="container py-14">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Simple, flat pricing</h2>
|
||||
<div className="mt-6 grid gap-6 sm:grid-cols-3">
|
||||
{plans.map((p) => (
|
||||
<div key={p.id} className={`card ${p.popular ? "ring-2 ring-primary" : ""}`}>
|
||||
{p.popular && <div className="mb-2 inline-block rounded-full bg-primary px-3 py-1 text-xs text-primary-foreground">Most popular</div>}
|
||||
<div className="text-lg font-semibold">{p.name}</div>
|
||||
<div className="mt-2 flex items-baseline gap-1">
|
||||
<div className="text-3xl font-bold">{p.price}</div>
|
||||
{p.periodicity && <div className="text-sm text-muted-foreground">{p.periodicity}</div>}
|
||||
<section className="relative py-24 border-t border-neutral-200 dark:border-neutral-800 bg-gradient-to-b from-white via-neutral-50 to-white dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
|
||||
<div className="container mx-auto max-w-6xl px-4 text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
Simple, flat pricing
|
||||
</h2>
|
||||
<div className="mt-12 grid gap-8 sm:grid-cols-3">
|
||||
{plans.map((p, i) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className={`rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-8 shadow-sm hover:shadow-lg transition backdrop-blur animate-[fadeInUp_0.6s_ease_forwards]`}
|
||||
style={{ animationDelay: `${i * 100}ms` }}
|
||||
>
|
||||
{p.popular && (
|
||||
<div className="mb-3 inline-block rounded-full bg-gradient-to-r from-blue-600 to-purple-600 px-3 py-1 text-xs text-white">
|
||||
Most popular
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xl font-semibold">{p.name}</div>
|
||||
<div className="mt-3 flex items-baseline justify-center gap-1">
|
||||
<div className="text-4xl font-bold text-neutral-900 dark:text-white">{p.price}</div>
|
||||
{p.periodicity && (
|
||||
<div className="text-sm text-neutral-500 dark:text-neutral-400">{p.periodicity}</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-neutral-600 dark:text-neutral-300">{p.description}</p>
|
||||
<ul className="mt-5 space-y-2 text-sm text-neutral-700 dark:text-neutral-300 text-left">
|
||||
{p.features.map((f, idx) => (
|
||||
<li key={idx}>• {f}</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link
|
||||
href={p.cta.href}
|
||||
className="mt-8 inline-block w-full rounded-lg bg-gradient-to-r from-blue-600 to-purple-600 text-white py-3 text-sm font-medium hover:opacity-90 transition"
|
||||
>
|
||||
{p.cta.label}
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{p.description}</p>
|
||||
<ul className="mt-4 space-y-2 text-sm">
|
||||
{p.features.map((f, i) => <li key={i}>• {f}</li>)}
|
||||
</ul>
|
||||
<Link href={p.cta.href} className="btn-primary mt-6 no-underline w-full text-center">{p.cta.label}</Link>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,31 @@
|
|||
export default function Process() {
|
||||
const steps = [
|
||||
{ t: "Free check", d: "We assess scope, risks and impact in 20–30 minutes." },
|
||||
{ t: "Sprint or managed", d: "Pick a fixed sprint or a managed plan with clear outcomes." },
|
||||
{ t: "Sprint or managed", d: "Pick a fixed sprint or managed plan with clear outcomes." },
|
||||
{ t: "Proof", d: "We deliver and prove it: reports, tests, and notes you keep." },
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="container py-10">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">How we work</h2>
|
||||
<div className="mt-6 grid gap-6 sm:grid-cols-3">
|
||||
{steps.map((s, i) => (
|
||||
<div key={i} className="card">
|
||||
<div className="h-10 w-10 rounded-xl bg-primary text-primary-foreground grid place-items-center font-semibold">{i+1}</div>
|
||||
<div className="mt-3 text-lg font-semibold">{s.t}</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">{s.d}</p>
|
||||
</div>
|
||||
))}
|
||||
<section className="relative py-24 bg-gradient-to-br from-blue-50/30 via-white to-purple-50/30 dark:from-blue-950/10 dark:to-purple-950/10">
|
||||
<div className="container mx-auto max-w-6xl px-4 text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
How we work
|
||||
</h2>
|
||||
<div className="mt-12 grid gap-8 sm:grid-cols-3">
|
||||
{steps.map((s, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-2xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/60 p-8 shadow-sm hover:shadow-md transition backdrop-blur animate-[fadeInUp_0.6s_ease_forwards]"
|
||||
style={{ animationDelay: `${i * 120}ms` }}
|
||||
>
|
||||
<div className="mx-auto h-10 w-10 rounded-xl bg-gradient-to-r from-blue-600 to-purple-600 text-white grid place-items-center font-semibold">
|
||||
{i + 1}
|
||||
</div>
|
||||
<div className="mt-4 text-lg font-semibold text-neutral-900 dark:text-white">{s.t}</div>
|
||||
<p className="mt-2 text-sm text-neutral-600 dark:text-neutral-300">{s.d}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function ServiceCard({ svc }: { svc: Service }) {
|
|||
</Link>
|
||||
<Link
|
||||
href={`/contact?topic=${encodeURIComponent(svc.slug)}`}
|
||||
className="inline-flex items-center rounded-md bg-black text-white px-3 py-1.5 text-sm hover:opacity-90"
|
||||
className="inline-flex items-center rounded-md bg-blue-600 text-white px-3 py-1.5 text-sm hover:opacity-90"
|
||||
>
|
||||
Start now
|
||||
</Link>
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
// path: components/Testimonials.tsx
|
||||
export default function Testimonials() {
|
||||
return (
|
||||
<section className="py-16 text-center">
|
||||
<h2 className="text-2xl font-semibold mb-4">What clients say</h2>
|
||||
<p className="text-gray-600">Testimonials coming soon.</p>
|
||||
<section className="relative py-24 bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950 text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||
What clients say
|
||||
</h2>
|
||||
<p className="mt-6 text-neutral-600 dark:text-neutral-300">
|
||||
Testimonials coming soon.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,8 @@ export const site = {
|
|||
description:
|
||||
"Inbox-ready email, Cloudflare edge security, and website care for small businesses—clear scope, flat pricing, and proof of outcomes.",
|
||||
contact: { email: "hello@vanhunen.it" },
|
||||
org: {
|
||||
logo: "/og.png",
|
||||
sameAs: ["https://www.linkedin.com/company/van-hunen-it"],
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@
|
|||
"node": ">=20 <23"
|
||||
},
|
||||
"dependencies": {
|
||||
"framer-motion": "^12.23.24",
|
||||
"next": "14.2.13",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
"react-dom": "18.3.1",
|
||||
"sonner": "^2.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.9.1",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ importers:
|
|||
|
||||
.:
|
||||
dependencies:
|
||||
framer-motion:
|
||||
specifier: ^12.23.24
|
||||
version: 12.23.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
next:
|
||||
specifier: 14.2.13
|
||||
version: 14.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
|
@ -17,6 +20,9 @@ importers:
|
|||
react-dom:
|
||||
specifier: 18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
sonner:
|
||||
specifier: ^2.0.7
|
||||
version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: 24.9.1
|
||||
|
|
@ -844,6 +850,20 @@ packages:
|
|||
fraction.js@4.3.7:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
|
||||
framer-motion@12.23.24:
|
||||
resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
|
|
@ -1199,6 +1219,12 @@ packages:
|
|||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
motion-dom@12.23.23:
|
||||
resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==}
|
||||
|
||||
motion-utils@12.23.6:
|
||||
resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
|
|
@ -1529,6 +1555,12 @@ packages:
|
|||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
sonner@2.0.7:
|
||||
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -2709,6 +2741,15 @@ snapshots:
|
|||
|
||||
fraction.js@4.3.7: {}
|
||||
|
||||
framer-motion@12.23.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
motion-dom: 12.23.23
|
||||
motion-utils: 12.23.6
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
|
|
@ -3076,6 +3117,12 @@ snapshots:
|
|||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
motion-dom@12.23.23:
|
||||
dependencies:
|
||||
motion-utils: 12.23.6
|
||||
|
||||
motion-utils@12.23.6: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
mz@2.7.0:
|
||||
|
|
@ -3429,6 +3476,11 @@ snapshots:
|
|||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
stable-hash@0.0.5: {}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user