A complete SaaS landing page from free GetUrUI blocks: header, hero, logos, features, numbers, pricing, FAQ, call to action and footer.
"use client";
import * as React from "react";
import { CalendarClock, ChartColumn, Check, Keyboard, ListChecks, Menu, Route, X } from "lucide-react";
import { HeroSection } from "@/components/blocks/hero-section";
import { LogoCloud, type Logo } from "@/components/blocks/logo-cloud";
import { FeatureGrid, type Feature } from "@/components/blocks/feature-grid";
import { StatsSection, type Stat } from "@/components/blocks/stats-section";
import { PricingSection, type PricingPlan, type SeatTier } from "@/components/blocks/pricing-section";
import { TestimonialsWall, type Testimonial } from "@/components/blocks/testimonials-wall";
import { FaqSection, type FaqContact } from "@/components/blocks/faq-section";
import { CtaBanner } from "@/components/blocks/cta-banner";
import { FooterBlock, type FooterColumn, type FooterLink } from "@/components/blocks/footer-block";
/*
* SaaS landing (lite) — a GetUrUI recipe.
* Rewrite the constants below for your product; the JSX underneath only wires them into the sections.
* Section ids (see NAV.links) are the anchors the header links to.
*/
// ─── Content ────────────────────────────────────────────────────────────────
const BRAND = {
name: "Taskwell",
/** One line used in the footer. */
tagline: "One calm queue for every request your team gets.",
};
const NAV = {
links: [
{ label: "Features", href: "#features" },
{ label: "Pricing", href: "#pricing" },
{ label: "FAQ", href: "#faq" },
],
secondary: { label: "Sign in", href: "/sign-in" },
cta: { label: "Start free", href: "#pricing" },
};
const HERO = {
eyebrow: "New: rules that assign work for you",
eyebrowBadge: "New",
eyebrowHref: "#features",
headline: "Every request, routed to the right person",
mark: "routed",
subcopy:
"Taskwell collects requests from forms, email and chat into one shared queue, assigns an owner and keeps everyone posted until the work is done.",
primaryAction: { label: "Start free", href: "#pricing" },
secondaryAction: { label: "See how it works", href: "#features" },
};
const LOGOS = {
heading: "Teams that keep their requests in Taskwell",
logos: [
{ name: "Northpeak", style: "geometric", glyph: "triangle" },
{ name: "OSTRA", style: "heavy", glyph: "square" },
{ name: "fernbrook", style: "light", glyph: "bars" },
{ name: "Halden", style: "serif", glyph: "dot" },
{ name: "kestrel", style: "rounded", glyph: "ring" },
{ name: "Lumen & Co", style: "mono", glyph: "slash" },
] satisfies Logo[],
};
const FEATURES_COPY = {
eyebrow: "Why Taskwell",
heading: "Less chasing, more finishing",
subheading: "The parts of team work that usually live in someone's head, written down and running on their own.",
};
/** Small strings shown inside the custom feature illustrations. */
const FEATURE_ART = {
queue: [
{ title: "New laptop for Sam", meta: "IT · today", done: true },
{ title: "Update the pricing page", meta: "Web · Fri", done: false },
{ title: "Renew the office lease", meta: "Ops · in 3 days", done: false },
],
rule: { when: "Form is “Access request”", then: "Assign to IT on call" },
due: { label: "Due in 2 hours", detail: "Reminder sent to the owner" },
};
const FEATURES: Feature[] = [
{
title: "One queue for every request",
description: "Forms, email and chat land in the same list, already tagged by team and urgency.",
icon: <ListChecks className="size-4" />,
media: <QueueArt items={FEATURE_ART.queue} />,
},
{
title: "Keyboard first",
description: "Triage, assign and close without touching the mouse.",
icon: <Keyboard className="size-4" />,
visual: "focus",
},
{
title: "Rules that route work",
description: "Write a rule once and new requests find their owner on their own.",
icon: <Route className="size-4" />,
media: <RuleArt when={FEATURE_ART.rule.when} then={FEATURE_ART.rule.then} />,
},
{
title: "Nothing slips",
description: "Due dates and gentle reminders keep work moving without a status meeting.",
icon: <CalendarClock className="size-4" />,
media: <DueArt label={FEATURE_ART.due.label} detail={FEATURE_ART.due.detail} />,
},
{
title: "Workload at a glance",
description: "See who is overloaded before it turns into a missed deadline.",
icon: <ChartColumn className="size-4" />,
visual: "bars",
},
];
const STATS = {
eyebrow: "By the numbers",
heading: "What teams see in the first month",
subheading: "Example figures. Replace them with numbers you can stand behind.",
stats: [
{ label: "Requests routed", value: 12480, trend: [3100, 5200, 7900, 10200, 12480], delta: "+22% MoM" },
{ label: "Median minutes to owner", value: 4, trend: [38, 21, 12, 7, 4], delta: "89% faster", deltaDirection: "up" },
{ label: "On-time completion", value: 96.4, suffix: "%", decimals: 1, trend: [81, 87, 91, 94, 96.4], delta: "+15 pts", deltaDirection: "up" },
{ label: "Teams onboarded", value: 340, trend: [80, 150, 220, 290, 340], delta: "+50 this month", deltaDirection: "up" },
] satisfies Stat[],
};
const PRICING = {
eyebrow: "Pricing",
heading: "Simple per-seat pricing",
subheading: "Start free with your team. Upgrade when the queue grows.",
yearlyNote: "Save 20%",
currency: "$",
defaultBilling: "yearly" as const,
minSeats: 1,
maxSeats: 100,
defaultSeats: 10,
tiers: [
{ from: 1, discount: 0 },
{ from: 25, discount: 0.1 },
{ from: 60, discount: 0.2 },
] satisfies SeatTier[],
plans: [
{
id: "starter",
name: "Starter",
description: "For small teams getting requests out of chat.",
monthly: 0,
yearly: 0,
cta: "Start free",
features: [
{ label: "Shared request queue", included: true },
{ label: "Request forms", included: "2 forms" },
{ label: "Routing rules", included: false },
{ label: "Workload reports", included: false },
],
},
{
id: "team",
name: "Team",
description: "For teams that want work to route itself.",
monthly: 12,
yearly: 10,
cta: "Start a 14-day trial",
featured: true,
badge: "Most popular",
features: [
{ label: "Shared request queue", included: true },
{ label: "Request forms", included: "Unlimited" },
{ label: "Routing rules", included: true },
{ label: "Workload reports", included: true },
],
},
{
id: "business",
name: "Business",
description: "For several teams with approvals and SSO.",
monthly: 24,
yearly: 20,
cta: "Talk to sales",
features: [
{ label: "Everything in Team", included: true },
{ label: "Approval steps", included: true },
{ label: "SAML single sign-on", included: true },
{ label: "Audit log", included: "1 year" },
],
},
] satisfies PricingPlan[],
};
/** Placeholders on purpose: never ship invented quotes. Replace with real ones, or delete the section. */
const TESTIMONIALS = {
eyebrow: "Customer stories",
heading: "Your customers, in their words",
subheading: "Placeholder cards. Add real quotes with permission, or remove this section.",
testimonials: [
{ quote: "Replace with a real quote from a customer.", name: "Customer name", handle: "@handle", role: "Role, Company" },
{ quote: "Replace with a real quote about the problem your product solved.", name: "Customer name 2", handle: "@handle", role: "Role, Company" },
{ quote: "Replace with a real quote about a result, ideally with a number in it.", name: "Customer name 3", handle: "@handle", role: "Role, Company" },
{ quote: "Replace with a real quote about what changed for their team.", name: "Customer name 4", handle: "@handle", role: "Role, Company" },
{ quote: "Replace with a real quote about what onboarding felt like.", name: "Customer name 5", handle: "@handle", role: "Role, Company" },
{ quote: "Replace with a real quote about why they would recommend it.", name: "Customer name 6", handle: "@handle", role: "Role, Company" },
] satisfies Testimonial[],
};
const FAQ = {
eyebrow: "FAQ",
heading: "Questions, answered",
subheading: "Everything people ask before they move their team's requests over.",
items: [
{ q: "How long does setup take?", a: "Most teams create their first form and routing rule in under fifteen minutes. You can import open requests from a CSV." },
{ q: "Can people outside my workspace send requests?", a: "Yes. Forms have a public link and email requests work from any address, so requesters never need an account." },
{ q: "What happens when the free plan runs out?", a: "Nothing is deleted. You can keep working in the queue and upgrade when you need more forms or routing rules." },
{ q: "Can I cancel any time?", a: "Yes. Paid plans stay active until the end of the billing period and you can export everything first." },
{ q: "Do you offer discounts for non-profits?", a: "Yes. Write to us from your organisation's address and we will set it up." },
],
contact: {
title: "Still have questions?",
description: "Talk to a person on our team. We usually reply within a day.",
actionLabel: "Contact us",
href: "mailto:hello@example.com",
team: ["Alex Kim", "Sam Ortiz", "Mira Lund"],
responseTime: "Replies in about 4 hours",
} satisfies FaqContact,
};
const CTA = {
eyebrow: "Free for small teams",
headline: "Give every request an owner today",
subcopy: "Set up your first form in minutes. No card needed.",
primaryAction: { label: "Start free", href: "#pricing" },
secondaryAction: { label: "Book a demo", href: "mailto:hello@example.com" },
};
const FOOTER = {
columns: [
{ title: "Product", links: [{ label: "Features", href: "#features" }, { label: "Pricing", href: "#pricing" }, { label: "Changelog", href: "/changelog", badge: "New" }] },
{ title: "Company", links: [{ label: "About", href: "/about" }, { label: "Careers", href: "/careers" }, { label: "Contact", href: "mailto:hello@example.com" }] },
{ title: "Resources", links: [{ label: "Help center", href: "/help" }, { label: "Templates", href: "/templates" }, { label: "FAQ", href: "#faq" }] },
] satisfies FooterColumn[],
newsletter: { title: "Product updates", description: "One short email a month. No spam.", placeholder: "you@company.com" },
copyright: "© 2026 Taskwell, Inc.",
legal: [{ label: "Privacy", href: "/privacy" }, { label: "Terms", href: "/terms" }] satisfies FooterLink[],
status: "All systems normal" as string | null,
};
/** Wire these to your app: checkout, newsletter API. */
function onSelectPlan({ planId, billing, seats }: { planId: string; billing: string; seats: number }) {
window.location.assign(new URL(`/sign-up?plan=${planId}&billing=${billing}&seats=${seats}`, window.location.origin));
}
async function onSubscribe(email: string) {
void email; // e.g. await fetch("/api/subscribe", { method: "POST", body: JSON.stringify({ email }) })
}
// ─── Page ───────────────────────────────────────────────────────────────────
const SECTION_Y = "py-16 sm:py-20";
/** A complete SaaS landing page built from free GetUrUI blocks. Render it from app/page.tsx. */
export interface SaasLandingLiteProps {
/** Replaces the default plan handler below (the site preview passes a no-op). */
onSelectPlan?: typeof onSelectPlan;
}
export function SaasLandingLite({ onSelectPlan: selectPlan = onSelectPlan }: SaasLandingLiteProps = {}) {
return (
<div className="min-h-dvh overflow-x-clip bg-bg text-ink">
<SiteHeader />
<main>
<HeroSection
variant="centered"
eyebrow={HERO.eyebrow}
eyebrowBadge={HERO.eyebrowBadge}
eyebrowHref={HERO.eyebrowHref}
headline={HERO.headline}
mark={HERO.mark}
subcopy={HERO.subcopy}
primaryAction={HERO.primaryAction}
secondaryAction={HERO.secondaryAction}
installCommand={null}
showProductShot={false}
className="pb-6 pt-20 sm:pb-8 sm:pt-28"
/>
<LogoCloud heading={LOGOS.heading} logos={LOGOS.logos} variant="marquee" className="py-10 sm:py-12" />
<div id="features" className="scroll-mt-16">
<FeatureGrid {...FEATURES_COPY} features={FEATURES} className={SECTION_Y} />
</div>
<StatsSection {...STATS} className={SECTION_Y} />
<div id="pricing" className="scroll-mt-16">
<PricingSection {...PRICING} onSelectPlan={selectPlan} className={SECTION_Y} />
</div>
<TestimonialsWall {...TESTIMONIALS} variant="marquee" className={SECTION_Y} />
<div id="faq" className="scroll-mt-16">
<FaqSection {...FAQ} className={SECTION_Y} />
</div>
<CtaBanner {...CTA} align="center" className="pb-20 pt-4 sm:pb-24 sm:pt-4" />
</main>
<FooterBlock
brand={{ name: BRAND.name, description: BRAND.tagline, href: "#top" }}
columns={FOOTER.columns}
newsletter={{ ...FOOTER.newsletter, onSubscribe }}
copyright={FOOTER.copyright}
legal={FOOTER.legal}
status={FOOTER.status}
/>
</div>
);
}
// ─── Header ─────────────────────────────────────────────────────────────────
const FOCUS = "outline-none focus-visible:shadow-[var(--focus-ring)]";
function SiteHeader() {
const [open, setOpen] = React.useState(false);
const menuId = React.useId();
const toggleRef = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setOpen(false);
toggleRef.current?.focus();
}
};
const onResize = () => window.innerWidth >= 768 && setOpen(false);
document.addEventListener("keydown", onKey);
window.addEventListener("resize", onResize);
return () => {
document.removeEventListener("keydown", onKey);
window.removeEventListener("resize", onResize);
};
}, [open]);
return (
<header id="top" className="sticky top-0 z-50 border-b border-border bg-bg/80 backdrop-blur-md supports-[backdrop-filter]:bg-bg/70">
<div className="mx-auto flex h-16 max-w-6xl items-center gap-3 px-4 sm:px-6">
<a href="#top" className={`-ml-1 inline-flex min-w-0 items-center gap-2 rounded-lg px-1 py-1 ${FOCUS}`}>
<span aria-hidden className="grid size-7 shrink-0 place-items-center rounded-lg bg-volt font-display text-sm font-bold text-on-volt">
{BRAND.name.charAt(0)}
</span>
<span className="truncate font-display text-[17px] font-semibold tracking-[-0.3px] text-ink">{BRAND.name}</span>
</a>
<nav aria-label="Main" className="ml-6 hidden md:block">
<ul className="flex items-center gap-1">
{NAV.links.map((l) => (
<li key={l.href}>
<a href={l.href} className={`inline-flex h-9 items-center rounded-lg px-3 text-sm font-medium text-ink-muted transition-colors hover:bg-surface hover:text-ink ${FOCUS}`}>
{l.label}
</a>
</li>
))}
</ul>
</nav>
<div className="ml-auto flex shrink-0 items-center gap-2">
<a
href={NAV.secondary.href}
className={`hidden h-9 items-center rounded-lg px-3 text-sm font-medium text-ink-muted transition-colors hover:text-ink sm:inline-flex ${FOCUS}`}
>
{NAV.secondary.label}
</a>
<a
href={NAV.cta.href}
className={`inline-flex h-9 items-center rounded-lg bg-volt px-4 text-sm font-semibold text-on-volt transition-[filter] hover:brightness-105 ${FOCUS}`}
>
{NAV.cta.label}
</a>
<button
ref={toggleRef}
type="button"
aria-expanded={open}
aria-controls={menuId}
aria-label={open ? "Close menu" : "Open menu"}
onClick={() => setOpen((o) => !o)}
className={`grid size-9 place-items-center rounded-lg border border-border text-ink transition-colors hover:bg-surface md:hidden ${FOCUS}`}
>
{open ? <X aria-hidden className="size-4" /> : <Menu aria-hidden className="size-4" />}
</button>
</div>
</div>
<nav id={menuId} aria-label="Main menu" hidden={!open} className="border-t border-border bg-bg md:hidden">
<ul className="mx-auto grid max-w-6xl gap-1 px-4 py-3 sm:px-6">
{[...NAV.links, NAV.secondary].map((l) => (
<li key={l.href}>
<a
href={l.href}
onClick={() => setOpen(false)}
className={`flex h-11 items-center rounded-lg px-3 text-[15px] font-medium text-ink transition-colors hover:bg-surface ${FOCUS}`}
>
{l.label}
</a>
</li>
))}
</ul>
</nav>
</header>
);
}
// ─── Feature illustrations (decorative; text comes from FEATURE_ART) ───────
function QueueArt({ items }: { items: { title: string; meta: string; done: boolean }[] }) {
return (
<div aria-hidden className="grid w-full max-w-[360px] gap-1.5">
{items.map((t, i) => (
<div
key={t.title}
className={`flex h-9 items-center gap-2.5 rounded-md border px-3 ${i === 1 ? "border-border-strong bg-surface-raised shadow-sm" : "border-border bg-surface"}`}
>
<span className={`grid size-4 shrink-0 place-items-center rounded-full ${t.done ? "bg-volt text-on-volt" : "ring-1 ring-inset ring-border-strong"}`}>
{t.done && <Check className="size-2.5" strokeWidth={3} />}
</span>
<span className={`min-w-0 flex-1 truncate text-[12.5px] font-medium ${t.done ? "text-ink-subtle line-through" : "text-ink"}`}>{t.title}</span>
<span className="shrink-0 text-[11px] text-ink-subtle">{t.meta}</span>
</div>
))}
</div>
);
}
function RuleArt({ when, then }: { when: string; then: string }) {
return (
<div aria-hidden className="grid w-full max-w-[250px] gap-1.5 text-[12px]">
<span className="rounded-md border border-border bg-surface-raised px-3 py-2 text-ink-muted">
<span className="font-semibold text-ink">When</span> {when}
</span>
<span className="ml-4 h-3 w-px bg-border-strong" />
<span className="rounded-md border border-volt-ink/40 bg-volt-soft px-3 py-2 text-ink">
<span className="font-semibold text-volt-ink">Then</span> {then}
</span>
</div>
);
}
function DueArt({ label, detail }: { label: string; detail: string }) {
return (
<div aria-hidden className="grid w-full max-w-[220px] gap-2 rounded-md border border-border bg-surface-raised p-3">
<span className="inline-flex w-fit items-center gap-1.5 rounded-full bg-ember-soft px-2 py-0.5 text-[11px] font-semibold text-ember-ink">
<CalendarClock className="size-3" /> {label}
</span>
<span className="h-1.5 overflow-hidden rounded-full bg-surface-sunken">
<span className="block h-full w-3/4 rounded-full bg-volt" />
</span>
<span className="text-[11px] text-ink-subtle">{detail}</span>
</div>
);
}