feat: add plans, about, blog and contact pages

Add the /plans, /about, /blog and /contact routes, a Company dropdown in
the navbar, and a shared 3D about-bus tile. Group components into
ui, layout, home, plans, about, blog and contact folders.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
vargha1
2026-10-03 15:07:44 +03:30
parent 5198432136
commit b9cc3c6349
45 changed files with 1461 additions and 290 deletions

BIN
public/FAQCube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
public/mission.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
public/vision.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

147
src/app/about/page.tsx Normal file
View File

@@ -0,0 +1,147 @@
import type { Metadata } from "next";
import { AboutBus } from "@/components/about/about-bus";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { Breadcrumb } from "@/components/ui/breadcrumb";
import { ButtonLink } from "@/components/ui/button";
import Image from "next/image";
import { Reveal } from "@/components/ui/reveal";
export const metadata: Metadata = {
title: "About us — VOYAGER",
description: "Voyager is an AI-powered business operating platform for tour operators, transportation companies and rental businesses.",
};
const principles = [
{ title: "Simple", text: "Powerful technology shouldn’t feel complicated." },
{ title: "Connected", text: "Your systems should work together." },
{ title: "Scalable", text: "Your software should grow with your business." },
{ title: "Intelligence", text: "AI should help your team, not replace the people who know your business." },
{ title: "Affordable", text: "Enterprise-level capability shouldn’t require enterprise-level complexity." },
];
const stats = [
{ value: "37mil", label: "Yearly Tickets" },
{ value: "360", label: "Operation" },
{ value: "280", label: "Clients" },
{ value: "50", label: "Countries" },
{ value: "102", label: "Employees" },
];
const aims = [
{
title: "Vision",
image: "/vision.png",
text: "To give every tour and transportation business the tools of a global enterprise, in one connected platform that grows with them.",
},
{
title: "Mission",
image: "/mission.png",
text: "To simplify how tourism businesses book, manage and grow by bringing ticketing, operations, distribution, payments and AI together.",
},
];
export default function AboutPage() {
return (
<>
<Navbar />
<main className="flex-1 overflow-x-clip">
<section className="pb-12 pt-4 lg:pb-20">
<div className="mx-auto flex max-w-6xl flex-col gap-6 px-4 sm:px-6">
<Breadcrumb current="About us" />
<div className="grid items-start gap-10 lg:grid-cols-[6fr_4fr]">
<div className="flex flex-col gap-6">
<h1 className="text-4xl font-medium leading-tight tracking-tight text-[#11192d] sm:text-5xl">
Voyager<span className="text-icon">,</span> the ticketing solution for tour operators
</h1>
<div className="flex flex-col gap-1">
<h2 className="text-xs font-bold text-[#11192d]">Technology That Makes Tourism Businesses Simpler</h2>
<p className="text-xs leading-relaxed text-[#11192d]/70">
Voyager is an AI-powered business operating platform for tour operators, transportation companies, rental businesses and tourism
enterprises. Voyager was created around a simple idea: tour operators shouldn&rsquo;t have to build their business around their
software. The software should adapt to the business. Voyager brings tour booking, ticketing, operations, fleet management, CRM,
payments, communication, distribution, marketing and AI together in one connected platform.
</p>
</div>
<div className="flex flex-col gap-3">
<h2 className="text-lg font-medium text-[#11192d]">Our Principles</h2>
<ul className="grid gap-3 sm:grid-cols-2">
{principles.map((p, i) => (
<li key={p.title} className={i === principles.length - 1 && principles.length % 2 === 1 ? "sm:col-span-2" : ""}>
<Reveal delay={(i % 2) * 90} className="h-full">
<div className="flex h-full flex-col gap-1 rounded-2xl bg-[#0160d4]/4 p-4">
<h3 className="text-base font-medium text-[#11192d]">{p.title}</h3>
<p className="max-w-56 text-xs text-[#11192d]/60 sm:max-w-none">{p.text}</p>
</div>
</Reveal>
</li>
))}
</ul>
</div>
</div>
<div className="mx-auto w-72 sm:w-80 lg:mx-0 lg:w-full lg:max-w-sm lg:justify-self-center">
<AboutBus priority />
</div>
</div>
</div>
</section>
<section className="py-12 lg:py-16">
<div className="mx-auto grid max-w-6xl gap-6 px-4 pt-20 sm:px-6 md:grid-cols-2">
{aims.map((a, i) => (
<Reveal key={a.title} delay={i * 90}>
<div className="relative flex h-full min-h-[22rem] flex-col justify-end rounded-3xl bg-[#0160d4]/4 p-8">
<div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden rounded-3xl">
<div className="absolute -left-10 top-1/3 h-24 w-[130%] -rotate-6 rounded-full border-[14px] border-[#0160d4]/8" />
</div>
<Image src={a.image} alt="" width={460} height={370} sizes="(min-width: 768px) 24rem, 80vw" className="pointer-events-none absolute inset-x-0 -top-16 mx-auto h-auto w-[75%] max-w-sm" />
<h2 className="relative text-lg font-bold text-[#11192d]">{a.title}</h2>
<p className="relative mt-2 text-xs leading-relaxed text-[#11192d]/70">{a.text}</p>
</div>
</Reveal>
))}
</div>
</section>
<section className="py-12 lg:py-16">
<div className="mx-auto flex max-w-6xl flex-col gap-4 px-4 sm:px-6">
<h2 className="flex flex-wrap items-end gap-x-4 leading-none">
<span className="bg-linear-to-r from-[#0160d4] to-[#0a8bff]/10 bg-clip-text text-[7rem] font-black leading-[0.85] text-transparent sm:text-[9rem]">
WHY
</span>
<span className="pb-2 text-3xl font-medium text-[#11192d] sm:text-4xl">
Choose Voyager<span className="text-accent">?</span>
</span>
</h2>
<ul className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
{stats.map((s) => (
<li key={s.label} className="flex flex-col items-center gap-1 rounded-2xl bg-[#0160d4]/4 px-4 py-4 text-center">
<span className="text-3xl font-medium text-[#0160d4]">{s.value}</span>
<span className="text-xs text-[#11192d]/70">{s.label}</span>
</li>
))}
</ul>
<div className="flex flex-col items-start justify-between gap-4 rounded-2xl border border-[#0160d4]/12 bg-white p-3 pl-5 sm:flex-row sm:items-center">
<p className="flex items-center gap-2 text-lg font-medium text-[#11192d]">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="size-5 text-icon">
<path d="m5 3 14 7-6 2-2 6L5 3Z" />
</svg>
Wanna try?
</p>
<ButtonLink href="/#demo" className="h-9 rounded-lg px-4 text-xs">
Become a Voyager Customer
</ButtonLink>
</div>
</div>
</section>
</main>
<Footer />
</>
);
}

30
src/app/blog/page.tsx Normal file
View File

@@ -0,0 +1,30 @@
import type { Metadata } from "next";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { Breadcrumb } from "@/components/ui/breadcrumb";
import { BlogList } from "@/components/blog/blog-list";
import { BlogAbout } from "@/components/blog/blog-about";
export const metadata: Metadata = {
title: "Weblog — VOYAGER",
description: "Guides, industry insights, customer stories and product updates for tour operators.",
};
export default function BlogPage() {
return (
<>
<Navbar />
<main className="flex-1 overflow-x-clip">
<section className="pb-12 pt-4 lg:pb-16">
<div className="mx-auto flex max-w-6xl flex-col gap-6 px-4 sm:px-6">
<Breadcrumb current="Weblog" />
<h1 className="text-4xl font-medium tracking-tight text-[#11192d] sm:text-5xl">Weblog</h1>
<BlogList />
</div>
</section>
<BlogAbout />
</main>
<Footer />
</>
);
}

90
src/app/contact/page.tsx Normal file
View File

@@ -0,0 +1,90 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import { Navbar } from "@/components/layout/navbar";
import { Footer } from "@/components/layout/footer";
import { Breadcrumb } from "@/components/ui/breadcrumb";
import { ContactForm } from "@/components/contact/contact-form";
export const metadata: Metadata = {
title: "Contact us — VOYAGER",
description: "Get in touch with the Voyager sales team or find our office details.",
};
const icons = {
pin: (
<>
<path d="M12 21s7-6 7-11a7 7 0 0 0-14 0c0 5 7 11 7 11Z" />
<circle cx="12" cy="10" r="2.5" />
</>
),
phone: <path d="M5 4h4l2 5-2.500 1.500a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A15 15 0 0 1 3 6a2 2 0 0 1 2-2Z" />,
mail: (
<>
<rect x="3" y="5" width="18" height="14" rx="3" />
<path d="m4 8 8 6 8-6" />
</>
),
};
function InfoCard({ icon, label, value, href, children }: { icon: ReactNode; label: string; value: string; href?: string; children?: ReactNode }) {
return (
<div className="flex items-center gap-4 rounded-3xl bg-[#0160d4]/4 px-5 py-4">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="size-6 shrink-0 text-icon">
{icon}
</svg>
<div className="flex min-w-0 grow flex-col">
<span className="text-[10px] text-[#11192d]/60">{label}</span>
{href ? (
<a href={href} className="text-sm font-medium text-[#11192d] transition-colors hover:text-accent">
{value}
</a>
) : (
<span className="text-sm font-medium text-[#11192d]">{value}</span>
)}
</div>
{children}
</div>
);
}
export default function ContactPage() {
return (
<>
<Navbar />
<main className="flex-1 overflow-x-clip">
<section className="pb-16 pt-4 lg:pb-24">
<div className="mx-auto flex max-w-6xl flex-col gap-6 px-4 sm:px-6">
<Breadcrumb current="Contact us" />
<h1 className="text-4xl font-medium tracking-tight text-[#11192d] sm:text-5xl">Contact us</h1>
<div className="grid items-start gap-6 lg:grid-cols-[5fr_5fr]">
<div className="flex flex-col gap-4">
<InfoCard icon={icons.pin} label="Central Office" value="full address replaced here">
<a
href="https://maps.google.com"
target="_blank"
rel="noopener noreferrer"
className="flex shrink-0 items-center gap-2 rounded-xl bg-white px-4 py-2.5 text-xs font-medium text-[#11192d] transition-colors hover:text-accent"
>
<svg viewBox="0 0 24 24" aria-hidden className="size-4">
<path d="M12 2a7 7 0 0 0-7 7c0 5 7 13 7 13s7-8 7-13a7 7 0 0 0-7-7Z" fill="#34a853" />
<path d="M12 2a7 7 0 0 0-5 2.100L12 10Z" fill="#ea4335" />
<circle cx="12" cy="9" r="2.500" fill="#fff" />
</svg>
Google map
</a>
</InfoCard>
<InfoCard icon={icons.phone} label="Sales Unit" value="+1 234 5432" href="tel:+12345432" />
<InfoCard icon={icons.phone} label="CEO Unit" value="+1 234 5432" href="tel:+12345432" />
<InfoCard icon={icons.mail} label="Mail" value="info@voyager.com" href="mailto:info@voyager.com" />
</div>
<ContactForm />
</div>
</div>
</section>
</main>
<Footer />
</>
);
}

View File

@@ -123,3 +123,22 @@ body {
scroll-behavior: auto !important;
}
}
.revenue-slider::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
border-radius: 9999px;
background: #0160d4;
border: 3px solid #fff;
box-shadow: 0 0 0 1px #0160d4;
}
.revenue-slider::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 9999px;
background: #0160d4;
border: 3px solid #fff;
box-shadow: 0 0 0 1px #0160d4;
}

View File

@@ -1,22 +1,22 @@
import { Navbar } from "@/components/navbar";
import { Hero } from "@/components/hero";
import { Partners } from "@/components/partners";
import { Platform } from "@/components/platform";
import { VoyagerDifference } from "@/components/voyager-difference";
import { TourBooking } from "@/components/tour-booking";
import { VoyagerAI } from "@/components/voyager-ai";
import { TourOperations } from "@/components/tour-operations";
import { CRM } from "@/components/crm";
import { OTADistribution } from "@/components/ota-distribution";
import { RentalManagement } from "@/components/rental-management";
import { PaymentsRevenue } from "@/components/payments-revenue";
import { MarketingGrowth } from "@/components/marketing-growth";
import { Solutions } from "@/components/solutions";
import { Pricing } from "@/components/pricing";
import { Integrations } from "@/components/integrations";
import { CustomerStories } from "@/components/customer-stories";
import { About } from "@/components/about";
import { Footer } from "@/components/footer";
import { Navbar } from "@/components/layout/navbar";
import { Hero } from "@/components/home/hero";
import { Partners } from "@/components/home/partners";
import { Platform } from "@/components/home/platform";
import { VoyagerDifference } from "@/components/home/voyager-difference";
import { TourBooking } from "@/components/home/tour-booking";
import { VoyagerAI } from "@/components/home/voyager-ai";
import { TourOperations } from "@/components/home/tour-operations";
import { CRM } from "@/components/home/crm";
import { OTADistribution } from "@/components/home/ota-distribution";
import { RentalManagement } from "@/components/home/rental-management";
import { PaymentsRevenue } from "@/components/home/payments-revenue";
import { MarketingGrowth } from "@/components/home/marketing-growth";
import { Solutions } from "@/components/home/solutions";
import { Pricing } from "@/components/home/pricing";
import { Integrations } from "@/components/home/integrations";
import { CustomerStories } from "@/components/home/customer-stories";
import { About } from "@/components/home/about";
import { Footer } from "@/components/layout/footer";
export default function Home() {
return (

37
src/app/plans/page.tsx Normal file
View File

@@ -0,0 +1,37 @@
import type { Metadata } from "next";
import { Navbar } from "@/components/layout/navbar";
import { Breadcrumb } from "@/components/ui/breadcrumb";
import { Footer } from "@/components/layout/footer";
import { PlanCalculator } from "@/components/plans/plan-calculator";
import { PlanIncludes } from "@/components/plans/plan-includes";
import { PlanComparison } from "@/components/plans/plan-comparison";
import { PlanFAQ } from "@/components/plans/plan-faq";
export const metadata: Metadata = {
title: "Our Plans — VOYAGER",
description: "Simple pricing built around your business. Compare Core, Grow, Scale and Enterprise plans.",
};
export default function PlansPage() {
return (
<>
<Navbar />
<main className="flex-1 overflow-x-clip">
<section className="pb-12 pt-4 lg:pb-16">
<div className="mx-auto flex max-w-6xl flex-col gap-6 px-4 sm:px-6">
<Breadcrumb current="Our Plans" />
<div className="flex flex-col gap-3">
<h1 className="text-4xl font-medium tracking-tight text-[#11192d] sm:text-5xl">Our Plans</h1>
<p className="text-lg text-[#11192d]">Your annual revenue</p>
</div>
<PlanCalculator />
</div>
</section>
<PlanIncludes />
<PlanComparison />
<PlanFAQ />
</main>
<Footer />
</>
);
}

View File

@@ -0,0 +1,34 @@
import Image from "next/image";
/** Blue tile with the bus overflowing the top. Layered shading gives the tile depth instead of a flat fill. */
export function AboutBus({ priority = false }: { priority?: boolean }) {
return (
<div className="relative aspect-4/5">
{/* Reflection fading out below the tile */}
<div aria-hidden className="absolute inset-x-[11%] bottom-0 top-[76%] bg-linear-to-b from-[#0160d4]/30 via-[#0160d4]/10 to-transparent" />
{/* Tall tile tilted back in perspective (top edge recedes), lit from the top-left, with a glossy top rim */}
<div
aria-hidden
className="absolute inset-x-[11%] top-[3%] h-[76%] origin-bottom overflow-hidden rounded-[32px] [transform:perspective(700px)_rotateX(10deg)] bg-linear-to-br from-[#1a7bf0] via-[#0160d4] to-[#0a4fc0] shadow-[inset_0_2px_0_rgba(255,255,255,0.35),inset_0_-40px_56px_-28px_rgba(0,16,84,0.45),0_28px_40px_-22px_rgba(1,70,180,0.7)]"
>
<div className="absolute -left-1/4 -top-1/4 size-3/4 rounded-full bg-white/15 blur-3xl" />
<div className="absolute inset-y-0 right-0 w-1/4 bg-linear-to-l from-[#001a66]/20 to-transparent" />
</div>
{/* Bus shadow cast onto the tile, plus contact shadow on the floor */}
<div aria-hidden className="absolute left-[32%] top-[12%] h-[62%] w-[48%] rounded-[28px] bg-[#001555]/45 blur-xl" />
<div aria-hidden className="absolute bottom-[20%] left-[18%] h-[4%] w-[60%] rounded-full bg-[#001555]/40 blur-lg" />
<Image
src="/aboutBus.png"
alt="A Voyager fleet vehicle"
width={290}
height={500}
sizes="288px"
priority={priority}
className="absolute left-4 top-0 h-auto w-[68%] drop-shadow-[10px_18px_14px_rgba(0,20,90,0.35)]"
/>
</div>
);
}

View File

@@ -0,0 +1,32 @@
"use client";
import { useState } from "react";
const paragraphs = [
"The Voyager blog brings together practical advice for tour operators, transportation companies and rental businesses. Our articles cover booking and ticketing, fleet and staff operations, online distribution, payments, marketing and the way AI is changing day-to-day work in tourism.",
"Whether you are choosing booking software for the first time or looking for ways to increase direct bookings, you will find guides, industry insights, customer stories and product updates written to help you run a simpler, more connected business.",
];
export function BlogAbout() {
const [expanded, setExpanded] = useState(false);
return (
<section className="py-12 lg:py-16">
<div className="mx-auto flex max-w-6xl flex-col items-center gap-6 px-4 sm:px-6">
<h2 className="self-start text-3xl font-medium tracking-tight text-[#11192d] sm:text-4xl">About Articles</h2>
<div className="flex flex-col gap-4 text-xs leading-relaxed text-[#11192d]/70">
<p>{paragraphs[0]}</p>
{expanded && <p>{paragraphs[1]}</p>}
</div>
<button
type="button"
aria-expanded={expanded}
onClick={() => setExpanded((v) => !v)}
className="h-10 rounded-lg border border-[#0160d4]/12 bg-white px-10 text-xs font-medium text-[#11192d] transition-colors hover:bg-outline-hover"
>
{expanded ? "Read less" : "Read more"}
</button>
</div>
</section>
);
}

View File

@@ -0,0 +1,207 @@
"use client";
import Link from "next/link";
import { useMemo, useState } from "react";
import { Select } from "@/components/ui/select";
import { Telescope, Target } from "@/components/ui/illustrations";
import { Reveal } from "@/components/ui/reveal";
import { categories, posts, type Post } from "@/data/posts";
const PAGE_SIZE = 8;
const YEARS = [2026, 2025, 2024];
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
type DateParts = { y: string; m: string; d: string };
const emptyDate: DateParts = { y: "", m: "", d: "" };
/** Start (or end) of the range described by partially filled year/month/day selects. */
function boundary({ y, m, d }: DateParts, end: boolean) {
if (!y) return null;
const year = Number(y);
if (d) return Date.UTC(year, Number(m || 1) - 1, Number(d));
if (m) return end ? Date.UTC(year, Number(m), 0) : Date.UTC(year, Number(m) - 1, 1);
return end ? Date.UTC(year, 12, 0) : Date.UTC(year, 0, 1);
}
const dateFormat = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
const themes = {
navy: "bg-[#11192d]",
blue: "bg-linear-to-br from-[#0160d4] to-[#0a8bff]",
orange: "bg-linear-to-br from-[#f3610c] to-[#ffb020]",
};
function CalendarIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="size-4 text-icon">
<rect x="4" y="5" width="16" height="15" rx="3" />
<path d="M8 3v4m8-4v4M4 11h16" />
</svg>
);
}
function PostCard({ post }: { post: Post }) {
const Art = post.art === "telescope" ? Telescope : Target;
return (
<article className="flex flex-col gap-3">
<div className={`relative aspect-[16/9] overflow-hidden rounded-2xl ${themes[post.theme]}`}>
<div aria-hidden className="absolute -right-10 top-6 size-56 rounded-full border-20 border-white/10" />
<Art className="absolute -left-6 top-0 h-[115%] w-auto drop-shadow-xl" />
</div>
<div className="flex flex-col gap-1 px-2">
<h3 className="text-sm font-bold text-[#11192d]">{post.title}</h3>
<p className="line-clamp-2 text-xs text-[#11192d]/60">{post.excerpt}</p>
</div>
<div className="flex items-center justify-between rounded-xl bg-[#0160d4]/4 p-2 pl-3">
<span className="flex items-center gap-2 text-[10px] text-[#11192d]/70">
<CalendarIcon />
<time dateTime={post.date}>{dateFormat.format(new Date(post.date))}</time>
</span>
<Link href="#" className="rounded-lg bg-white px-4 py-1.5 text-[10px] font-medium text-[#11192d] transition-colors hover:text-accent">
Read more
</Link>
</div>
</article>
);
}
function DateFilter({ label, value, onChange }: { label: string; value: DateParts; onChange: (v: DateParts) => void }) {
const set = (key: keyof DateParts) => (e: React.ChangeEvent<HTMLSelectElement>) => onChange({ ...value, [key]: e.target.value });
return (
<fieldset className="flex flex-col gap-1">
<legend className="mb-1 text-[10px] capitalize text-[#11192d]/50">{label}</legend>
<div className="grid grid-cols-3 gap-2">
<Select compact aria-label={`${label} year`} value={value.y} onChange={set("y")} className="h-10 min-w-0">
<option value="">Year</option>
{YEARS.map((y) => (
<option key={y}>{y}</option>
))}
</Select>
<Select compact aria-label={`${label} month`} value={value.m} onChange={set("m")} className="h-10 min-w-0">
<option value="">Month</option>
{MONTHS.map((m, i) => (
<option key={m} value={i + 1}>
{m}
</option>
))}
</Select>
<Select compact aria-label={`${label} day`} value={value.d} onChange={set("d")} className="h-10 min-w-0">
<option value="">Day</option>
{Array.from({ length: 31 }, (_, i) => (
<option key={i}>{i + 1}</option>
))}
</Select>
</div>
</fieldset>
);
}
export function BlogList() {
const [query, setQuery] = useState("");
const [category, setCategory] = useState("");
const [sort, setSort] = useState("latest");
const [from, setFrom] = useState(emptyDate);
const [to, setTo] = useState(emptyDate);
const [visible, setVisible] = useState(PAGE_SIZE);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
const start = boundary(from, false);
const end = boundary(to, true);
return posts
.filter((p) => {
const t = new Date(p.date).getTime();
return (
(!q || p.title.toLowerCase().includes(q) || p.excerpt.toLowerCase().includes(q)) &&
(!category || p.category === category) &&
(start === null || t >= start) &&
(end === null || t <= end)
);
})
.sort((a, b) => (sort === "latest" ? b.date.localeCompare(a.date) : a.date.localeCompare(b.date)));
}, [query, category, sort, from, to]);
// Any filter change returns to the first page.
const reset = <T,>(setter: (v: T) => void) => (v: T) => {
setter(v);
setVisible(PAGE_SIZE);
};
const labelClass = "text-sm font-medium text-[#11192d]";
return (
<div className="grid items-start gap-8 lg:grid-cols-[17rem_1fr]">
<aside className="flex flex-col gap-4 rounded-2xl border border-[#0160d4]/12 bg-white p-4 lg:sticky lg:top-4">
<h2 className="text-lg font-medium text-[#11192d]">Filters</h2>
<div className="relative">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-icon">
<circle cx="11" cy="11" r="6" />
<path d="m20 20-4-4" />
</svg>
<input
type="search"
aria-label="Search articles"
placeholder="Search ..."
value={query}
onChange={(e) => reset(setQuery)(e.target.value)}
className="h-11 w-full rounded-lg border border-[#0160d4]/15 bg-white pl-9 pr-3 text-sm outline-none transition-colors placeholder:text-[#11192d]/50 focus-visible:border-icon focus-visible:ring-2 focus-visible:ring-icon/20"
/>
</div>
<div className="flex flex-col gap-2 border-t border-divider pt-4">
<label htmlFor="blog-category" className={labelClass}>
Category
</label>
<Select id="blog-category" value={category} onChange={(e) => reset(setCategory)(e.target.value)} className="h-11">
<option value="">Select Category</option>
{categories.map((c) => (
<option key={c}>{c}</option>
))}
</Select>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="blog-sort" className={labelClass}>
Sort by
</label>
<Select id="blog-sort" value={sort} onChange={(e) => reset(setSort)(e.target.value)} className="h-11">
<option value="latest">Latest</option>
<option value="oldest">Oldest</option>
</Select>
</div>
<div className="flex flex-col gap-2">
<span className={labelClass}>Date Filter</span>
<DateFilter label="from" value={from} onChange={reset(setFrom)} />
<DateFilter label="to" value={to} onChange={reset(setTo)} />
</div>
</aside>
<div className="flex flex-col items-center gap-8">
{filtered.length === 0 ? (
<p className="py-16 text-center text-sm text-[#11192d]/60">No articles match your filters.</p>
) : (
<ul className="grid w-full gap-x-4 gap-y-8 sm:grid-cols-2">
{filtered.slice(0, visible).map((post, i) => (
<li key={post.slug}>
<Reveal delay={(i % 2) * 90}>
<PostCard post={post} />
</Reveal>
</li>
))}
</ul>
)}
{visible < filtered.length && (
<button
type="button"
onClick={() => setVisible((v) => v + PAGE_SIZE)}
className="h-10 rounded-lg border border-[#0160d4]/12 bg-white px-12 text-xs font-medium text-[#11192d] transition-colors hover:bg-outline-hover"
>
Load more
</button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,160 @@
"use client";
import Link from "next/link";
import { useState, type ComponentProps, type FormEvent, type ReactNode } from "react";
import { Select } from "@/components/ui/select";
const countries = [
"Australia", "Austria", "Belgium", "Brazil", "Canada", "Croatia", "Czech Republic", "Denmark", "Egypt", "Finland",
"France", "Germany", "Greece", "Hungary", "Iceland", "India", "Ireland", "Italy", "Japan", "Jordan", "Mexico",
"Morocco", "Netherlands", "New Zealand", "Norway", "Poland", "Portugal", "Saudi Arabia", "Singapore", "South Africa",
"Spain", "Sweden", "Switzerland", "Thailand", "Turkey", "United Arab Emirates", "United Kingdom", "United States",
];
const dialCodes = ["+1", "+7", "+20", "+27", "+30", "+31", "+32", "+33", "+34", "+39", "+41", "+43", "+44", "+45", "+46", "+47", "+49", "+61", "+64", "+65", "+66", "+81", "+90", "+91", "+971"];
const currencies = ["USD", "EUR", "GBP", "AUD", "CAD", "AED", "CHF", "JPY"];
const subjects = ["Book a demo", "Talk to sales", "Support", "Partnerships", "Something else"];
const fieldClass =
"h-11 w-full rounded-lg border border-[#0160d4]/15 bg-white px-3 text-sm text-[#11192d] outline-none transition-colors placeholder:text-[#11192d]/40 focus-visible:border-icon focus-visible:ring-2 focus-visible:ring-icon/20";
function Field({ label, required, children, className = "" }: { label: string; required?: boolean; children: ReactNode; className?: string }) {
return (
<label className={`flex flex-col gap-2 text-sm font-medium text-[#11192d] ${className}`}>
<span>
{label}
{required && <span className="text-accent"> *</span>}
</span>
{children}
</label>
);
}
function Input(props: ComponentProps<"input">) {
return <input {...props} className={fieldClass} />;
}
export function ContactForm() {
const [sent, setSent] = useState(false);
function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
// TODO: wire up to the sales/CRM endpoint once it exists.
setSent(true);
}
if (sent) {
return (
<div role="status" className="flex min-h-96 flex-col items-center justify-center gap-3 rounded-3xl border border-[#0160d4]/12 bg-white p-8 text-center">
<h2 className="text-2xl font-medium text-[#11192d]">Thank you!</h2>
<p className="max-w-sm text-sm text-[#11192d]/70">Your message has been received. Our team will get back to you shortly.</p>
<button type="button" onClick={() => setSent(false)} className="mt-2 text-sm font-medium text-icon hover:underline">
Send another message
</button>
</div>
);
}
return (
<form onSubmit={onSubmit} className="flex flex-col gap-4 rounded-3xl border border-[#0160d4]/12 bg-white p-4 sm:p-5">
<h2 className="text-2xl font-medium text-[#11192d]">Contact Sales</h2>
<Field label="Country of operation" required>
<Select name="country" required defaultValue="" className="h-11">
<option value="" disabled>
Select Country
</option>
{countries.map((c) => (
<option key={c}>{c}</option>
))}
</Select>
</Field>
<div className="grid gap-4 border-b border-divider pb-4 sm:grid-cols-2">
<Field label="Company name" required>
<Input name="company" required placeholder="company name" autoComplete="organization" />
</Field>
<Field label="Company website" required>
<Input name="website" type="url" required placeholder="www.example.com" autoComplete="url" />
</Field>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<Field label="First name">
<Input name="firstName" placeholder="first name" autoComplete="given-name" />
</Field>
<Field label="Last name">
<Input name="lastName" placeholder="last name" autoComplete="family-name" />
</Field>
</div>
<Field label="Job title">
<Input name="jobTitle" placeholder="job title" autoComplete="organization-title" />
</Field>
<div className="grid gap-4 border-b border-divider pb-4 sm:grid-cols-2">
<Field label="Email" required>
<Input name="email" type="email" required placeholder="email" autoComplete="email" />
</Field>
<div className="flex flex-col gap-2 text-sm font-medium text-[#11192d]">
<label htmlFor="phone">
Phone number<span className="text-accent"> *</span>
</label>
<div className="flex gap-2">
<Select aria-label="Country code" name="dialCode" defaultValue="+1" className="h-11 w-24 shrink-0">
{dialCodes.map((c) => (
<option key={c}>{c}</option>
))}
</Select>
<input id="phone" name="phone" type="tel" required placeholder="123456677" autoComplete="tel-national" className={fieldClass} />
</div>
</div>
</div>
<Field label="Subject" required>
<Select name="subject" required defaultValue="Book a demo" className="h-11">
{subjects.map((s) => (
<option key={s}>{s}</option>
))}
</Select>
</Field>
<div className="flex flex-col gap-2 text-sm font-medium text-[#11192d]">
<label htmlFor="revenue-amount">
What is your company&rsquo;s approximate annual revenue?<span className="text-accent"> *</span>
</label>
<div className="grid grid-cols-2 gap-2">
<Select aria-label="Currency" name="currency" required defaultValue="" className="h-11">
<option value="" disabled>
Select Currency
</option>
{currencies.map((c) => (
<option key={c}>{c}</option>
))}
</Select>
<input id="revenue-amount" name="revenue" type="number" min={0} required placeholder="Ex : 3,000,000" className={fieldClass} />
</div>
</div>
<Field label="What pain points are you experiencing with your current system?" required>
<textarea name="painPoints" required rows={5} placeholder="Tell us about your current setup" className={`${fieldClass} h-auto resize-y py-3`} />
</Field>
<p className="px-1 text-justify text-[10px] leading-snug text-[#11192d]/70">
Voyager needs the contact information you provide to us to contact you about our products and services. You may unsubscribe from these
communications at anytime. For information on how to unsubscribe, as well as our privacy practices and commitment to protecting your privacy,
check out our{" "}
<Link href="#" className="text-icon hover:underline">
Privacy Policy
</Link>
.
</p>
<button
type="submit"
className="h-11 rounded-lg bg-primary text-sm font-medium text-primary-foreground transition-all duration-200 hover:-translate-y-0.5 hover:bg-primary-hover hover:shadow-lg active:translate-y-0 active:shadow-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
>
Submit
</button>
</form>
);
}

View File

@@ -1,5 +1,5 @@
import Image from "next/image";
import { Reveal } from "@/components/reveal";
import { AboutBus } from "@/components/about/about-bus";
import { Reveal } from "@/components/ui/reveal";
const principles = [
{ title: "Simple", description: "Powerful technology shouldn’t feel complicated." },
@@ -50,22 +50,7 @@ export function About() {
</div>
<Reveal delay={100} className="relative mx-auto w-64 sm:w-72 lg:mx-0">
{/* Blue tile with the bus (and its baked-in shadow) overflowing the top, plus a fading reflection */}
<div className="relative aspect-4/5">
<div
aria-hidden
className="absolute inset-x-[8%] bottom-0 top-[40%] bg-linear-to-b from-[#0160d4]/35 to-transparent"
/>
<div aria-hidden className="absolute inset-x-[10%] top-[3%] h-[62%] rounded-[32px] bg-[#0160d4]" />
<Image
src="/aboutBus.png"
alt="A Voyager fleet vehicle"
width={290}
height={500}
sizes="288px"
className="absolute left-4 top-0 h-auto w-[68%]"
/>
</div>
<AboutBus />
</Reveal>
</div>
</section>

View File

@@ -1,6 +1,6 @@
import type { ReactNode } from "react";
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
type Feature = { title: string; description: string; icon: ReactNode };

View File

@@ -2,8 +2,8 @@
import { useEffect, useRef, useState, type PointerEvent } from "react";
import Image from "next/image";
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
const stories = Array.from({ length: 5 }, (_, i) => i);
const quote = "Voyager helped us bring bookings, operations and customer communication into one platform.";

View File

@@ -1,5 +1,5 @@
import Image from "next/image";
import { ButtonLink } from "@/components/button";
import { ButtonLink } from "@/components/ui/button";
export function Hero() {
return (

View File

@@ -1,5 +1,5 @@
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
const columns = [
{ title: "Distribution", items: ["GetYourGuide", "Viator", "TourCMS", "Other OTA Channels"] },

View File

@@ -1,7 +1,7 @@
import Image from "next/image";
import type { ReactNode } from "react";
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
type Item = { title: string; description: string; icon: ReactNode };

View File

@@ -1,5 +1,5 @@
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
const channels = ["GetYourGuide", "Viator", "TourCMS", "Your Website", "Affiliates", "Travel Agents"];
const pricingTargets = ["OTAs", "Hotels", "Affiliates", "Travel Agents", "Corporate Clients", "Direct Customers"];

View File

@@ -1,4 +1,4 @@
import { Reveal } from "@/components/reveal";
import { Reveal } from "@/components/ui/reveal";
const partners = Array.from({ length: 6 }, (_, i) => ({
id: i,

View File

@@ -1,6 +1,6 @@
import Image from "next/image";
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
const features = [
{

View File

@@ -1,7 +1,7 @@
import Image from "next/image";
import type { ReactNode } from "react";
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
type Feature = { title: string; description: string; icon: ReactNode };

View File

@@ -0,0 +1,154 @@
import type { ReactNode } from "react";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
type Plan = {
name: string;
tagline: ReactNode;
blurb: string;
heading: string;
items: string[];
cta: string;
popular?: boolean;
};
const plus = <span className="text-[#f3610c]">+</span>;
const plans: Plan[] = [
{
name: "Core",
tagline: "Book",
blurb: "For operators getting started.",
heading: "Includes :",
items: ["Tour management", "Online bookings", "Availability", "Fixed pricing", "Customer management", "Payments", "Basic reporting"],
cta: "Get Started",
},
{
name: "Grow",
tagline: <>Book {plus} Manage</>,
blurb: "For growing operators.",
heading: "Everything in Core plus :",
items: ["CRM", "Scheduling", "Fleet", "Staff", "Dynamic pricing", "Communication", "OTA connectivity", "Advanced reporting"],
cta: "Book a Demo",
popular: true,
},
{
name: "Scale",
tagline: <>Book {plus} Manage {plus} Grow</>,
blurb: "For established operators.",
heading: "Everything in Grow, plus :",
items: ["AI", "Advanced CRM", "Rentals", "Contracts", "Marketing", "Affiliate management", "Incentives", "Advanced analytics", "API integrations", "Multi-location capabilities"],
cta: "Talk to Sales",
},
];
const enterpriseItems = [
"Enterprise integrations",
"Custom workflows",
"Multiple locations",
"Advanced permissions",
"Dedicated support",
"Custom API requirements",
"Data migration",
"Implementation services",
];
export function PricingCards() {
return (
<div className="grid w-full gap-4 sm:grid-cols-2 lg:grid-cols-4">
{plans.map((p, i) => (
<Reveal key={p.name} delay={i * 90}>
<div className="relative flex h-full flex-col gap-4 rounded-3xl border border-[#0160d4]/12 bg-white p-4">
{p.popular && (
<span className="absolute right-4 top-5 inline-flex h-4.5 items-center rounded-xl bg-[#f3610c] px-3 text-xs font-medium text-white">
Most Popular
</span>
)}
<div className="flex flex-col gap-2">
<h3 className="text-2xl font-bold text-[#11192d]">{p.name}</h3>
<p className="text-base font-bold text-[#11192d]">{p.tagline}</p>
<p className="text-base text-[#11192d]/70">{p.blurb}</p>
</div>
<div className="flex grow flex-col gap-4 rounded-2xl bg-[#0160d4]/3 p-4">
<p className="text-base font-bold text-[#0160d4]">{p.heading}</p>
<ul className="flex flex-col gap-2 text-xs font-medium text-[#11192d]">
{p.items.map((item) => (
<li key={item} className="flex gap-2">
<span aria-hidden>•</span>
{item}
</li>
))}
</ul>
</div>
<ButtonLink href="#demo" variant="outline" size="lg" className="h-12 w-full rounded-xl border-[#0160d4]/12">
{p.cta}
</ButtonLink>
</div>
</Reveal>
))}
<Reveal delay={plans.length * 90}>
<div className="relative flex h-full flex-col gap-4 overflow-hidden rounded-3xl bg-[#f3610c] p-4 text-white">
<div aria-hidden className="pointer-events-none absolute -right-20 -top-24 size-72 rounded-full bg-white/25 blur-3xl" />
<div aria-hidden className="pointer-events-none absolute -left-24 top-40 size-64 rounded-full border-30 border-white/10" />
<div className="relative flex grow flex-col gap-4">
<div className="flex flex-col gap-2">
<h3 className="text-2xl font-bold">Enterprise</h3>
<p className="text-base">
<span className="font-bold">Your </span>
<span className="italic">Business</span>
<span className="font-bold">, your </span>
<span className="italic">Platform</span>
</p>
<p className="text-xs font-medium">Custom architecture for larger operations.</p>
</div>
<div className="flex flex-col gap-4 rounded-2xl bg-[#11192d]/6 p-4">
<p className="text-base font-bold">Includes :</p>
<ul className="flex flex-col gap-2 text-xs font-bold">
{enterpriseItems.map((item) => (
<li key={item} className="flex gap-2">
<span aria-hidden>•</span>
{item}
</li>
))}
</ul>
</div>
<p className="px-4 text-xs font-medium">
Enterprise booking, ticketing and tourism management platform for multi-location operators and larger
transportation businesses.
</p>
</div>
<ButtonLink
href="#demo"
size="lg"
className="relative h-12 w-full rounded-xl border-white bg-white text-[#11192d]! hover:border-white hover:bg-white/90"
>
Contact Sales
</ButtonLink>
</div>
</Reveal>
</div>
);
}
export function Pricing() {
return (
<section id="plans" className="relative scroll-mt-4 py-20 lg:py-28">
<div className="mx-auto flex max-w-6xl flex-col items-center gap-8 px-4 sm:px-6">
<div className="flex flex-col items-center gap-6 text-center">
<h2 className="text-4xl font-medium tracking-tight text-[#11192d] sm:text-5xl">Tour Operator Software Pricing</h2>
<p className="rounded-2xl bg-[#0160d4]/10 px-4 py-3 text-xl text-[#0160d4] sm:text-2xl">
<span className="font-bold">Simple Pricing. Built Around Your </span>
<span className="italic">Business</span>
<span className="font-bold">.</span>
</p>
<p className="text-base text-[#11192d]/70">
Start with the capabilities you need. Add more as your business grows.
</p>
</div>
<PricingCards />
</div>
</section>
);
}

View File

@@ -1,7 +1,7 @@
import Image from "next/image";
import type { ReactNode } from "react";
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
type Feature = { title: string; description: string; icon: ReactNode };

View File

@@ -1,5 +1,5 @@
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
type Solution = { letter: string; title: string; description: string; cta: string; tone: "blue" | "orange" };

View File

@@ -1,6 +1,6 @@
import type { ReactNode } from "react";
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
type Feature = { title: string; description: string; icon: ReactNode; full?: boolean };

View File

@@ -1,7 +1,7 @@
import Image from "next/image";
import type { ReactNode } from "react";
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
type Feature = { title: string; description: string; icon: ReactNode };

View File

@@ -1,6 +1,6 @@
import Image from "next/image";
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
import { ButtonLink } from "@/components/ui/button";
import { Reveal } from "@/components/ui/reveal";
type Pillar = { title: string; description: string };

View File

@@ -1,5 +1,5 @@
import Image from "next/image";
import { Reveal } from "@/components/reveal";
import { Reveal } from "@/components/ui/reveal";
type DiffCard = {
title: string;

View File

@@ -0,0 +1,114 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { ButtonLink } from "@/components/ui/button";
import { Logo } from "@/components/layout/logo";
type NavLink = { href: string; label: string; children?: { href: string; label: string }[] };
const links: NavLink[] = [
{ href: "/", label: "Home" },
{ href: "/#features", label: "Features" },
{ href: "/#solutions", label: "Solutions" },
{
href: "/about",
label: "Company",
children: [
{ href: "/about", label: "About us" },
{ href: "/blog", label: "Weblog" },
{ href: "/contact", label: "Contact us" },
],
},
];
function Chevron({ className = "" }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden className={`size-3.5 ${className}`}>
<path d="m6 9 6 6 6-6" />
</svg>
);
}
export function Navbar() {
const [open, setOpen] = useState(false);
return (
<header className="relative z-20">
<div className="mx-auto flex h-20 max-w-6xl items-center justify-between gap-6 px-4 sm:px-6 lg:h-24">
<Logo />
<nav aria-label="Primary" className="hidden items-center gap-12 lg:flex">
{links.map((link) =>
link.children ? (
<div key={link.label} className="group relative">
<button type="button" aria-haspopup="menu" className="flex items-center gap-1 text-sm text-foreground transition-colors hover:text-accent group-focus-within:text-accent">
{link.label}
<Chevron className="transition-transform duration-200 group-hover:rotate-180 group-focus-within:rotate-180" />
</button>
<div className="invisible absolute left-1/2 top-full z-30 -translate-x-1/2 pt-3 opacity-0 transition-all duration-200 group-focus-within:visible group-focus-within:opacity-100 group-hover:visible group-hover:opacity-100">
<div className="flex min-w-40 flex-col rounded-xl border border-outline bg-background p-1.5 shadow-lg">
{link.children.map((child) => (
<Link key={child.href} href={child.href} className="rounded-lg px-3 py-2 text-sm text-foreground transition-colors hover:bg-card hover:text-accent">
{child.label}
</Link>
))}
</div>
</div>
</div>
) : (
<Link
key={link.label}
href={link.href}
className="group relative text-sm text-foreground transition-colors hover:text-accent"
>
{link.label}
<span className="absolute -bottom-1 left-0 h-px w-0 bg-accent transition-all duration-300 group-hover:w-full" />
</Link>
),
)}
</nav>
<div className="flex items-center gap-3">
<div className="hidden items-center gap-3 lg:flex">
<ButtonLink href="/plans" variant="outline">Our Plans</ButtonLink>
<ButtonLink href="/#demo">Book a Demo</ButtonLink>
</div>
<button
type="button"
aria-label={open ? "Close menu" : "Open menu"}
aria-expanded={open}
aria-controls="mobile-menu"
onClick={() => setOpen((v) => !v)}
className="grid size-10 place-items-center rounded-lg border-2 border-outline bg-outline-bg text-foreground transition-colors hover:bg-outline-hover lg:hidden"
>
<svg className="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
{open ? <path d="M6 6l12 12M18 6L6 18" /> : <path d="M4 7h16M4 12h16M4 17h16" />}
</svg>
</button>
</div>
</div>
{open && (
<div id="mobile-menu" className="absolute inset-x-0 top-full border-y border-outline bg-background px-4 pb-6 pt-2 shadow-lg sm:px-6 lg:hidden">
<nav aria-label="Mobile" className="flex flex-col">
{links.flatMap((link) => (link.children ?? [link]).map((l) => (
<Link
key={l.href + l.label}
href={l.href}
onClick={() => setOpen(false)}
className="border-b border-outline py-3 text-base text-foreground"
>
{l.label}
</Link>
)))}
</nav>
<div className="mt-5 grid grid-cols-2 gap-3">
<ButtonLink href="/plans" variant="outline" onClick={() => setOpen(false)}>Our Plans</ButtonLink>
<ButtonLink href="/#demo" onClick={() => setOpen(false)}>Book a Demo</ButtonLink>
</div>
</div>
)}
</header>
);
}

View File

@@ -1,78 +0,0 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { ButtonLink } from "@/components/button";
import { Logo } from "@/components/logo";
const links = [
{ href: "/", label: "Home" },
{ href: "#features", label: "Features" },
{ href: "#solutions", label: "Solutions" },
{ href: "#others", label: "Others" },
];
export function Navbar() {
const [open, setOpen] = useState(false);
return (
<header className="relative z-20">
<div className="mx-auto flex h-20 max-w-6xl items-center justify-between gap-6 px-4 sm:px-6 lg:h-24">
<Logo />
<nav aria-label="Primary" className="hidden items-center gap-12 lg:flex">
{links.map((link) => (
<Link
key={link.label}
href={link.href}
className="group relative text-sm text-foreground transition-colors hover:text-accent"
>
{link.label}
<span className="absolute -bottom-1 left-0 h-px w-0 bg-accent transition-all duration-300 group-hover:w-full" />
</Link>
))}
</nav>
<div className="flex items-center gap-3">
<div className="hidden items-center gap-3 lg:flex">
<ButtonLink href="#plans" variant="outline">Our Plans</ButtonLink>
<ButtonLink href="#demo">Book a Demo</ButtonLink>
</div>
<button
type="button"
aria-label={open ? "Close menu" : "Open menu"}
aria-expanded={open}
aria-controls="mobile-menu"
onClick={() => setOpen((v) => !v)}
className="grid size-10 place-items-center rounded-lg border-2 border-outline bg-outline-bg text-foreground transition-colors hover:bg-outline-hover lg:hidden"
>
<svg className="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
{open ? <path d="M6 6l12 12M18 6L6 18" /> : <path d="M4 7h16M4 12h16M4 17h16" />}
</svg>
</button>
</div>
</div>
{open && (
<div id="mobile-menu" className="absolute inset-x-0 top-full border-y border-outline bg-background px-4 pb-6 pt-2 shadow-lg sm:px-6 lg:hidden">
<nav aria-label="Mobile" className="flex flex-col">
{links.map((link) => (
<Link
key={link.label}
href={link.href}
onClick={() => setOpen(false)}
className="border-b border-outline py-3 text-base text-foreground"
>
{link.label}
</Link>
))}
</nav>
<div className="mt-5 grid grid-cols-2 gap-3">
<ButtonLink href="#plans" variant="outline" onClick={() => setOpen(false)}>Our Plans</ButtonLink>
<ButtonLink href="#demo" onClick={() => setOpen(false)}>Book a Demo</ButtonLink>
</div>
</div>
)}
</header>
);
}

View File

@@ -0,0 +1,155 @@
"use client";
import { useState } from "react";
import { ButtonLink } from "@/components/ui/button";
const MIN = 1_000_000;
const MAX = 16_000_000;
const STEP = 100_000;
const REFERENCE = 3_500_000;
const TRANSACTION_RATE = 0.0093;
const currencies = {
USD: { symbol: "$", rate: 1 },
EUR: { symbol: "€", rate: 0.92 },
GBP: { symbol: "£", rate: 0.79 },
} as const;
type CurrencyCode = keyof typeof currencies;
// Monthly price at the reference revenue; scales with revenue.
const tiers = [
{ name: "Core", base: 600, upTo: 2_000_000 },
{ name: "Grow", base: 2320, upTo: 6_000_000 },
{ name: "Scale", base: 3600, upTo: 11_000_000 },
{ name: "Enterprise", base: 5850, upTo: Infinity },
];
function monthlyPrice(base: number, revenue: number) {
return Math.round((base * (0.4 + 0.6 * (revenue / REFERENCE))) / 10) * 10;
}
function compact(value: number) {
if (value >= 1_000_000) return `${+(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `${+(value / 1_000).toFixed(1)}k`;
return `${Math.round(value)}`;
}
export function PlanCalculator() {
const [revenue, setRevenue] = useState(REFERENCE);
const [currency, setCurrency] = useState<CurrencyCode>("USD");
const { symbol, rate } = currencies[currency];
const money = (usd: number) => `${symbol}${Math.round(usd * rate).toLocaleString("en-US")}`;
const short = (usd: number) => `${symbol}${compact(usd * rate)}`;
const recommended = tiers.findIndex((t) => revenue < t.upTo);
const transactionFee = revenue * TRANSACTION_RATE;
const planFee = monthlyPrice(tiers[recommended].base, revenue) * 12;
const progress = ((revenue - MIN) / (MAX - MIN)) * 100;
return (
<div className="flex w-full flex-col gap-6">
<div className="relative pt-14">
<div
className="absolute top-0 -translate-x-1/2 transition-[left] duration-75"
style={{ left: `calc(${progress}% + ${8 - progress * 0.16}px)` }}
>
<div className="flex items-center gap-2 rounded-xl border-2 border-[#11192d] bg-[#11192d] pl-3 text-white">
<output className="text-base font-bold tabular-nums" aria-live="polite">
{symbol}
{compact(revenue * rate)}
</output>
<label className="sr-only" htmlFor="currency">
Currency
</label>
<select
id="currency"
value={currency}
onChange={(e) => setCurrency(e.target.value as CurrencyCode)}
className="h-9 cursor-pointer rounded-lg bg-white px-2 text-xs font-medium text-[#11192d] outline-none focus-visible:outline-2 focus-visible:outline-accent"
>
{Object.keys(currencies).map((code) => (
<option key={code}>{code}</option>
))}
</select>
</div>
</div>
<label className="sr-only" htmlFor="revenue">
Annual revenue
</label>
<input
id="revenue"
type="range"
min={MIN}
max={MAX}
step={STEP}
value={revenue}
onChange={(e) => setRevenue(Number(e.target.value))}
style={{ background: `linear-gradient(to right, #0160d4 ${progress}%, rgba(1, 96, 212, 0.15) ${progress}%)` }}
className="revenue-slider h-1 w-full cursor-pointer appearance-none rounded-full"
/>
<div className="mt-3 flex justify-between text-[10px] font-medium text-[#11192d]/70">
<span>{short(MIN)}</span>
<span>{short(MAX)}+</span>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
{tiers.map((t, i) => {
const active = i === recommended;
return (
<div
key={t.name}
className={`relative flex flex-col gap-3 rounded-3xl p-4 pb-0 transition-colors duration-300 ${
active
? "bg-linear-to-br from-[#0160d4] to-[#0a8bff] text-white"
: "border border-[#0160d4]/12 bg-white text-[#11192d]"
}`}
>
{active && (
<span className="absolute inset-x-0 top-0 mx-auto w-fit -translate-y-1/2 rounded-xl bg-[#f3610c] px-3 py-0.5 text-[10px] font-medium text-white">
Recommended
</span>
)}
<h3 className="text-sm font-bold">{t.name}</h3>
<div>
<p className="text-2xl font-bold tabular-nums">{money(monthlyPrice(t.base, revenue))}</p>
<p className={`text-xs ${active ? "text-white/80" : "text-[#11192d]/60"}`}>/ Month</p>
</div>
<div className={`mt-4 rounded-xl p-3 ${active ? "bg-white/15" : "bg-[#0160d4]/4"}`}>
<p className="text-sm font-bold tabular-nums">{short(transactionFee)}</p>
<p className={`text-[10px] ${active ? "text-white/80" : "text-[#11192d]/60"}`}>Transaction fee</p>
</div>
</div>
);
})}
<div className="flex flex-col gap-3 rounded-3xl bg-[#11192d] p-4 text-white sm:col-span-2 lg:col-span-1">
<h3 className="text-sm font-bold">Annual Total</h3>
<p className="flex items-baseline gap-1">
<span className="text-2xl font-bold tabular-nums">{short(planFee + transactionFee)}</span>
<span className="text-xs text-white/70">/ year</span>
</p>
<div className="grid grid-cols-2 gap-2 rounded-xl bg-white/10 p-3">
<div>
<p className="text-sm font-bold tabular-nums">{short(planFee)}</p>
<p className="text-[10px] text-white/70">Plan fee</p>
</div>
<div>
<p className="text-sm font-bold tabular-nums">{short(transactionFee)}</p>
<p className="text-[10px] text-white/70">Transaction fee</p>
</div>
</div>
<ButtonLink
href="/#demo"
className="mt-auto h-10 w-full rounded-lg border-white bg-white text-[#11192d]! hover:border-white hover:bg-white/90"
>
Contact Sales
</ButtonLink>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { PricingCards } from "@/components/home/pricing";
export function PlanComparison() {
return (
<section className="py-12 lg:py-16">
<div className="mx-auto flex max-w-6xl flex-col gap-8 px-4 sm:px-6">
<div className="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<h2 className="text-3xl font-medium tracking-tight text-[#11192d] sm:text-4xl">Comparison</h2>
<p className="rounded-xl bg-[#0160d4]/10 px-4 py-2 text-base text-[#0160d4] sm:text-lg">
<span className="font-bold">What&rsquo;s </span>
<span className="italic">different</span>
<span className="font-bold"> between each </span>
<span className="italic">plan?</span>
</p>
</div>
<PricingCards />
</div>
</section>
);
}

View File

@@ -0,0 +1,72 @@
import Image from "next/image";
import { Reveal } from "@/components/ui/reveal";
const faqs = [
{
q: "How can I book a plan?",
a: "Pick the plan that fits your business and use its button to get started or book a demo. Our team will walk you through setup and onboarding.",
},
{
q: "How is the plan price calculated?",
a: "Pricing is based on your annual revenue. Move the slider at the top of this page to see the monthly plan fee, the transaction fee and your estimated annual total.",
},
{
q: "Can I switch plans later?",
a: "Yes. You can move up to a larger plan whenever your business grows, and your data and settings carry over.",
},
{
q: "What does the transaction fee cover?",
a: "It covers payment processing on bookings taken through Voyager, so there are no separate payment gateway contracts to manage.",
},
{
q: "Do you offer custom plans for larger operators?",
a: "The Enterprise plan is built around your business, with custom workflows, integrations, data migration and dedicated support. Contact sales to discuss your needs.",
},
];
function Cube() {
return (
<div aria-hidden className="relative mx-auto aspect-[7/7] w-full max-w-md">
<div className="absolute inset-x-0 bottom-[10%] h-[48%] rounded-3xl bg-linear-to-b from-[#0160d4] to-[#0a8bff]" />
<div className="absolute inset-x-0 bottom-[10%] h-[24%] rounded-b-3xl bg-linear-to-b from-transparent to-white" />
<Image src="/FAQCube.png" alt="" fill sizes="(min-width: 1024px) 28rem, 90vw" className="object-contain scale-130 left-18!" />
</div>
);
}
export function PlanFAQ() {
return (
<section className="py-12 lg:py-20">
<div className="mx-auto grid max-w-6xl items-start gap-10 px-4 sm:px-6 lg:grid-cols-[4fr_7fr] lg:gap-16">
<div className="lg:pt-24">
<Cube />
</div>
<div className="flex flex-col gap-6 lg:border-l lg:border-divider lg:pl-12">
<div className="flex flex-col gap-3">
<h2 className="text-3xl font-medium tracking-tight text-[#11192d] sm:text-4xl">FAQ&rsquo;s</h2>
<p className="text-sm text-[#11192d]/70">
Answers to the most common questions about choosing a plan, pricing and getting started with Voyager.
</p>
</div>
<div className="flex flex-col gap-3">
{faqs.map((f, i) => (
<Reveal key={f.q} delay={i * 60}>
<details name="plan-faq" open={i === 0} className="group rounded-xl border border-[#0160d4]/12 bg-white px-5 py-4">
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 text-xs font-medium text-[#11192d] [&::-webkit-details-marker]:hidden">
{f.q}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="size-4 shrink-0 transition-transform duration-200 group-open:rotate-180">
<path d="m6 9 6 6 6-6" />
</svg>
</summary>
<p className="mt-3 text-xs leading-relaxed text-[#11192d]/70">{f.a}</p>
</details>
</Reveal>
))}
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,36 @@
import type { ReactNode } from "react";
import { Reveal } from "@/components/ui/reveal";
const includes: { label: string; icon: ReactNode }[] = [
{ label: "Tour Management", icon: <path d="M4 16V7a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v9M4 16h16M4 16v2m16-2v2M8 20v-2m8 2v-2M4 11h16" /> },
{ label: "Online Booking", icon: <><circle cx="12" cy="12" r="8" /><path d="m9 12 2 2 4-4" /></> },
{ label: "Availability", icon: <><rect x="4" y="5" width="16" height="15" rx="3" /><path d="M8 3v4m8-4v4M4 11h16" /></> },
{ label: "Fixed Pricing", icon: <><path d="M3 12V4h8l9 9-8 8-9-9Z" /><circle cx="7.5" cy="8.5" r="1" /></> },
{ label: "Customer Management", icon: <><circle cx="9" cy="8" r="3" /><path d="M3 20a6 6 0 0 1 12 0M16 5.5a3 3 0 0 1 0 5.500M18 20a5 5 0 0 0-2-4" /></> },
{ label: "Payments", icon: <><rect x="3" y="6" width="18" height="12" rx="3" /><path d="M3 10h18M7 15h3" /></> },
{ label: "Basic Reporting", icon: <><rect x="4" y="4" width="16" height="16" rx="3" /><path d="M8 15v-3m4 3V9m4 6v-5" /></> },
];
export function PlanIncludes() {
return (
<section className="py-12 lg:py-16">
<div className="mx-auto max-w-6xl px-4 sm:px-6">
<h2 className="text-3xl font-medium tracking-tight text-[#11192d] sm:text-4xl">All Plans Include</h2>
<ul className="mt-8 grid gap-4 sm:grid-cols-2">
{includes.map((item, i) => (
<li key={item.label} className={i === includes.length - 1 && includes.length % 2 === 1 ? "sm:col-span-2" : ""}>
<Reveal delay={(i % 2) * 90}>
<div className="flex h-20 items-center gap-4 rounded-2xl bg-[#0160d4]/4 px-5 sm:px-6">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="size-6 shrink-0 text-icon">
{item.icon}
</svg>
<span className="text-lg font-medium text-[#11192d] sm:text-xl">{item.label}</span>
</div>
</Reveal>
</li>
))}
</ul>
</div>
</section>
);
}

View File

@@ -1,148 +0,0 @@
import type { ReactNode } from "react";
import { ButtonLink } from "@/components/button";
import { Reveal } from "@/components/reveal";
type Plan = {
name: string;
tagline: ReactNode;
blurb: string;
heading: string;
items: string[];
cta: string;
popular?: boolean;
};
const plus = <span className="text-[#f3610c]">+</span>;
const plans: Plan[] = [
{
name: "Core",
tagline: "Book",
blurb: "For operators getting started.",
heading: "Includes :",
items: ["Tour management", "Online bookings", "Availability", "Fixed pricing", "Customer management", "Payments", "Basic reporting"],
cta: "Get Started",
},
{
name: "Grow",
tagline: <>Book {plus} Manage</>,
blurb: "For growing operators.",
heading: "Everything in Core plus :",
items: ["CRM", "Scheduling", "Fleet", "Staff", "Dynamic pricing", "Communication", "OTA connectivity", "Advanced reporting"],
cta: "Book a Demo",
popular: true,
},
{
name: "Scale",
tagline: <>Book {plus} Manage {plus} Grow</>,
blurb: "For established operators.",
heading: "Everything in Grow, plus :",
items: ["AI", "Advanced CRM", "Rentals", "Contracts", "Marketing", "Affiliate management", "Incentives", "Advanced analytics", "API integrations", "Multi-location capabilities"],
cta: "Talk to Sales",
},
];
const enterpriseItems = [
"Enterprise integrations",
"Custom workflows",
"Multiple locations",
"Advanced permissions",
"Dedicated support",
"Custom API requirements",
"Data migration",
"Implementation services",
];
export function Pricing() {
return (
<section id="plans" className="relative scroll-mt-4 py-20 lg:py-28">
<div className="mx-auto flex max-w-6xl flex-col items-center gap-8 px-4 sm:px-6">
<div className="flex flex-col items-center gap-6 text-center">
<h2 className="text-4xl font-medium tracking-tight text-[#11192d] sm:text-5xl">Tour Operator Software Pricing</h2>
<p className="rounded-2xl bg-[#0160d4]/10 px-4 py-3 text-xl text-[#0160d4] sm:text-2xl">
<span className="font-bold">Simple Pricing. Built Around Your </span>
<span className="italic">Business</span>
<span className="font-bold">.</span>
</p>
<p className="text-base text-[#11192d]/70">
Start with the capabilities you need. Add more as your business grows.
</p>
</div>
<div className="grid w-full gap-4 sm:grid-cols-2 lg:grid-cols-4">
{plans.map((p, i) => (
<Reveal key={p.name} delay={i * 90}>
<div className="relative flex h-full flex-col gap-4 rounded-3xl border border-[#0160d4]/12 bg-white p-4">
{p.popular && (
<span className="absolute right-4 top-5 inline-flex h-4.5 items-center rounded-xl bg-[#f3610c] px-3 text-xs font-medium text-white">
Most Popular
</span>
)}
<div className="flex flex-col gap-2">
<h3 className="text-2xl font-bold text-[#11192d]">{p.name}</h3>
<p className="text-base font-bold text-[#11192d]">{p.tagline}</p>
<p className="text-base text-[#11192d]/70">{p.blurb}</p>
</div>
<div className="flex grow flex-col gap-4 rounded-2xl bg-[#0160d4]/3 p-4">
<p className="text-base font-bold text-[#0160d4]">{p.heading}</p>
<ul className="flex flex-col gap-2 text-xs font-medium text-[#11192d]">
{p.items.map((item) => (
<li key={item} className="flex gap-2">
<span aria-hidden>•</span>
{item}
</li>
))}
</ul>
</div>
<ButtonLink href="#demo" variant="outline" size="lg" className="h-12 w-full rounded-xl border-[#0160d4]/12">
{p.cta}
</ButtonLink>
</div>
</Reveal>
))}
<Reveal delay={plans.length * 90}>
<div className="relative flex h-full flex-col gap-4 overflow-hidden rounded-3xl bg-[#f3610c] p-4 text-white">
<div aria-hidden className="pointer-events-none absolute -right-20 -top-24 size-72 rounded-full bg-white/25 blur-3xl" />
<div aria-hidden className="pointer-events-none absolute -left-24 top-40 size-64 rounded-full border-30 border-white/10" />
<div className="relative flex grow flex-col gap-4">
<div className="flex flex-col gap-2">
<h3 className="text-2xl font-bold">Enterprise</h3>
<p className="text-base">
<span className="font-bold">Your </span>
<span className="italic">Business</span>
<span className="font-bold">, your </span>
<span className="italic">Platform</span>
</p>
<p className="text-xs font-medium">Custom architecture for larger operations.</p>
</div>
<div className="flex flex-col gap-4 rounded-2xl bg-[#11192d]/6 p-4">
<p className="text-base font-bold">Includes :</p>
<ul className="flex flex-col gap-2 text-xs font-bold">
{enterpriseItems.map((item) => (
<li key={item} className="flex gap-2">
<span aria-hidden>•</span>
{item}
</li>
))}
</ul>
</div>
<p className="px-4 text-xs font-medium">
Enterprise booking, ticketing and tourism management platform for multi-location operators and larger
transportation businesses.
</p>
</div>
<ButtonLink
href="#demo"
size="lg"
className="relative h-12 w-full rounded-xl border-white bg-white text-[#11192d]! hover:border-white hover:bg-white/90"
>
Contact Sales
</ButtonLink>
</div>
</Reveal>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,15 @@
import Link from "next/link";
export function Breadcrumb({ current }: { current: string }) {
return (
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-[10px] text-[#11192d]/70">
<Link href="/" className="transition-colors hover:text-accent">
Home
</Link>
<span aria-hidden>/</span>
<span className="text-[#11192d]" aria-current="page">
{current}
</span>
</nav>
);
}

View File

@@ -0,0 +1,45 @@
/** Placeholder 3D-style illustrations. Swap for the real artwork when available. */
function Defs() {
return (
<defs>
<linearGradient id="ill-body" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stopColor="#ffffff" />
<stop offset="1" stopColor="#c9d0dd" />
</linearGradient>
</defs>
);
}
export function Telescope({ className = "" }: { className?: string }) {
return (
<svg viewBox="0 0 200 200" aria-hidden className={className}>
<Defs />
<ellipse cx="100" cy="182" rx="38" ry="7" fill="#000" opacity="0.12" />
<path d="M90 112 70 178h60l-20-62Z" fill="url(#ill-body)" />
<circle cx="100" cy="112" r="9" fill="#e6e9ef" />
<g transform="rotate(-35 100 100)">
<rect x="44" y="82" width="96" height="38" rx="16" fill="url(#ill-body)" />
<rect x="126" y="76" width="22" height="50" rx="9" fill="#f4f6fa" />
<ellipse cx="146" cy="101" rx="8" ry="19" fill="#aeb6c6" />
<rect x="60" y="68" width="40" height="12" rx="6" fill="url(#ill-body)" />
</g>
</svg>
);
}
export function Target({ className = "" }: { className?: string }) {
return (
<svg viewBox="0 0 200 200" aria-hidden className={className}>
<Defs />
<ellipse cx="100" cy="176" rx="52" ry="8" fill="#000" opacity="0.12" />
<circle cx="96" cy="108" r="62" fill="url(#ill-body)" />
<circle cx="96" cy="108" r="46" fill="#dfe3ec" />
<circle cx="96" cy="108" r="31" fill="url(#ill-body)" />
<circle cx="96" cy="108" r="15" fill="#c2cadb" />
<path d="M96 108 168 38" stroke="#d5dbe7" strokeWidth="6" strokeLinecap="round" />
<path d="m150 38 22-6-6 22-10-4-6-12Z" fill="url(#ill-body)" />
<path d="m132 54 12 4-18 18-6-10 12-12Z" fill="#f4f6fa" />
</svg>
);
}

View File

@@ -0,0 +1,18 @@
import type { ComponentProps } from "react";
/** Native select with a custom chevron. Pass sizing/spacing via `className` (applied to the wrapper). */
export function Select({ className = "", compact = false, children, ...props }: ComponentProps<"select"> & { compact?: boolean }) {
return (
<div className={`relative ${className}`}>
<select
{...props}
className={`h-full min-h-10 w-full cursor-pointer appearance-none rounded-lg border border-[#0160d4]/15 bg-white ${compact ? "pl-2 pr-5 text-xs" : "pl-3 pr-8 text-sm"} text-[#11192d] outline-none transition-colors focus-visible:border-icon focus-visible:ring-2 focus-visible:ring-icon/20 invalid:text-[#11192d]/50`}
>
{children}
</select>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden className={`pointer-events-none absolute ${compact ? "right-1.5" : "right-3"} top-1/2 size-3.5 -translate-y-1/2 text-[#11192d]`}>
<path d="m6 9 6 6 6-6" />
</svg>
</div>
);
}

27
src/data/posts.ts Normal file
View File

@@ -0,0 +1,27 @@
export type Post = {
slug: string;
title: string;
excerpt: string;
category: string;
/** ISO date (YYYY-MM-DD) */
date: string;
theme: "navy" | "blue" | "orange";
art: "telescope" | "target";
};
export const categories = ["Industry Insights", "Guides", "Product Updates", "Customer Stories"];
export const posts: Post[] = [
{ slug: "choosing-tour-booking-software", title: "How to choose the right tour booking software", excerpt: "A practical checklist for comparing booking platforms for tours, transfers and rentals.", category: "Guides", date: "2026-09-30", theme: "navy", art: "target" },
{ slug: "ai-in-tour-operations", title: "How AI is changing tour operations", excerpt: "From dynamic pricing to automated customer replies, where AI already saves operators time.", category: "Industry Insights", date: "2026-09-27", theme: "blue", art: "telescope" },
{ slug: "ota-connectivity-explained", title: "OTA connectivity explained", excerpt: "How to distribute inventory to marketplaces without double bookings or manual updates.", category: "Guides", date: "2026-09-21", theme: "orange", art: "telescope" },
{ slug: "voyager-fleet-management", title: "New: fleet and staff scheduling", excerpt: "Assign vehicles and drivers to departures in a few clicks, with conflicts flagged for you.", category: "Product Updates", date: "2026-09-14", theme: "navy", art: "target" },
{ slug: "direct-bookings-playbook", title: "A playbook for more direct bookings", excerpt: "Lower your OTA commissions by turning your own website into your best sales channel.", category: "Industry Insights", date: "2026-08-29", theme: "navy", art: "target" },
{ slug: "city-tours-case-study", title: "How a city tour operator doubled online sales", excerpt: "A look at the workflows and tools behind a year of steady growth.", category: "Customer Stories", date: "2026-08-17", theme: "blue", art: "telescope" },
{ slug: "pricing-your-tours", title: "Fixed vs. dynamic pricing for tours", excerpt: "When to keep prices simple and when demand-based pricing pays off.", category: "Guides", date: "2026-07-22", theme: "orange", art: "telescope" },
{ slug: "payments-update", title: "Faster payouts and simpler reconciliation", excerpt: "What changed in payments, and how it affects your month-end reporting.", category: "Product Updates", date: "2026-07-03", theme: "navy", art: "target" },
{ slug: "rental-business-growth", title: "Adding rentals to a tour business", excerpt: "Why many operators expand into vehicle and equipment rentals, and how to start.", category: "Industry Insights", date: "2026-06-12", theme: "navy", art: "target" },
{ slug: "transfer-company-story", title: "A transfer company goes paperless", excerpt: "Replacing spreadsheets and phone calls with one connected booking system.", category: "Customer Stories", date: "2026-05-20", theme: "blue", art: "telescope" },
{ slug: "crm-for-operators", title: "Why tour operators need a CRM", excerpt: "Keep every guest, enquiry and repeat booking in one place.", category: "Guides", date: "2026-04-08", theme: "orange", art: "telescope" },
{ slug: "api-and-integrations", title: "API and integrations: what is new", excerpt: "New endpoints and partner integrations for connecting Voyager to your stack.", category: "Product Updates", date: "2026-03-15", theme: "navy", art: "target" },
];