Lumen
A sellable admin template for a privacy-friendly analytics product: marketing site (home, pricing, customers and case studies, blog, careers, about, contact, legal), every account flow (sign-in and sign-up variants, reset, two-factor, email checks, lock screen), system pages (404, 500, 403, maintenance, coming soon, offline), a public storefront with its own header, cart drawer, collections, product pages, cart, checkout and confirmation, a page index and a 45-route app with grouped navigation: six dashboards, eight apps, e-commerce, invoices, users, settings and support.
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/glass/template-lumen.jsonAlso installs the theme, cn() and navbar, notification-bell, aurora-backdrop, hero-product, bento-features, install-snippet, proof-band, vs-list, app-window, bar-list, chart-funnel, copy-button, faq, cta-panel, pricing-tiers, pricing-compare, changelog, newsletter, footer-columns, about, careers, careers-role, blog-cards, blog-post, contact-page, customers, case-study, auth-sign-in, auth-sign-in-split, auth-sign-up, auth-sign-up-split, auth-forgot, auth-check-email, auth-reset, auth-otp, auth-verify-email, auth-lock-screen, error-404, error-500, error-403, maintenance, coming-soon, offline, app-shell, dashboard-analytics, dashboard-finance, dashboard-devops, screen-dashboard-ecommerce, screen-dashboard-crm, screen-dashboard-projects, screen-mail, screen-chat, screen-calendar, screen-kanban, screen-tasks, screen-notes, screen-file-manager, screen-contacts, screen-products, screen-product-detail, screen-product-form, screen-orders, screen-order-detail, store-header, store-footer, screen-store, screen-store-product, screen-store-cart, screen-store-checkout, screen-store-confirmation, screen-customers, screen-customer-detail, screen-invoices, screen-invoice-preview, screen-invoice-editor, screen-users, screen-user-profile, screen-roles, screen-team, screen-settings-layout, screen-settings-general, screen-settings-security, screen-settings-notifications, screen-settings-billing, screen-settings-api, screen-settings-connections, screen-help-center, screen-help-article, screen-faq, screen-search-results, screen-timeline, progress, sidebar-nav, brand-mark, button, scroll-text-reveal, switch, badge, live-indicator. Packages: clsx, tailwind-merge.
Usage
import { TemplateLumen } from "@/registry/kit/templates/template-lumen";
export default function TemplateLumenDemo() {
return <TemplateLumen />;
}Source
"use client";
import { useEffect, useSyncExternalStore, type ReactNode } from "react";
import { About } from "@/registry/kit/blocks/about";
import { AuthCheckEmail } from "@/registry/kit/blocks/auth-check-email";
import { AuthForgot } from "@/registry/kit/blocks/auth-forgot";
import { AuthLockScreen } from "@/registry/kit/blocks/auth-lock-screen";
import { AuthOtp } from "@/registry/kit/blocks/auth-otp";
import { AuthReset } from "@/registry/kit/blocks/auth-reset";
import { AuthSignIn } from "@/registry/kit/blocks/auth-sign-in";
import { AuthSignInSplit } from "@/registry/kit/blocks/auth-sign-in-split";
import { AuthSignUp } from "@/registry/kit/blocks/auth-sign-up";
import { AuthSignUpSplit } from "@/registry/kit/blocks/auth-sign-up-split";
import { AuthVerifyEmail } from "@/registry/kit/blocks/auth-verify-email";
import { BlogCards } from "@/registry/kit/blocks/blog-cards";
import { BlogPost } from "@/registry/kit/blocks/blog-post";
import { Careers } from "@/registry/kit/blocks/careers";
import { CareersRole } from "@/registry/kit/blocks/careers-role";
import { CaseStudy } from "@/registry/kit/blocks/case-study";
import { Changelog, type Release } from "@/registry/kit/blocks/changelog";
import { ComingSoon } from "@/registry/kit/blocks/coming-soon";
import { ContactPage } from "@/registry/kit/blocks/contact-page";
import { CtaPanel } from "@/registry/kit/blocks/cta-panel";
import { Customers } from "@/registry/kit/blocks/customers";
import { Error403 } from "@/registry/kit/blocks/error-403";
import { Error404 } from "@/registry/kit/blocks/error-404";
import { Error500 } from "@/registry/kit/blocks/error-500";
import { Faq, type FaqItem } from "@/registry/kit/blocks/faq";
import { FooterColumns, type FooterColumn } from "@/registry/kit/blocks/footer-columns";
import { Maintenance } from "@/registry/kit/blocks/maintenance";
import { Navbar } from "@/registry/kit/blocks/navbar";
import { Newsletter } from "@/registry/kit/blocks/newsletter";
import { Offline } from "@/registry/kit/blocks/offline";
import { PricingCompare, type CompareGroup } from "@/registry/kit/blocks/pricing-compare";
import { PricingTiers, type Tier } from "@/registry/kit/blocks/pricing-tiers";
import { appPages, go, href, LumenAppPage, LumenGlyph } from "@/registry/kit/templates/template-lumen-app";
import { LumenHome, primaryCta, riskReducer } from "@/registry/kit/templates/template-lumen-home";
import { storeRoutes } from "@/registry/kit/templates/template-lumen-store";
import { about, author, blogPosts, caseStudies, channels, offices, posts, relatedTo, roleDetail, roles, stories } from "@/registry/kit/templates/template-lumen-content";
import { AuroraBackdrop } from "@/registry/kit/ui/aurora-backdrop";
import { BrandMark } from "@/registry/kit/ui/brand-mark";
import { buttonVariants } from "@/registry/kit/ui/button";
/*
* Lumen — a complete marketing site plus a 40-screen admin app, composed only
* from registry blocks, screens and dashboards (AGENTS.md: templates compose,
* never copy). Pages switch on the URL hash, so it runs in any React app.
* Every route is listed in `pages` below and on the #/pages index; anything
* else renders the 404 page.
*/
const delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
/* --------------------------------------------------------------- content */
const nav = [
{ label: "Product", href: href("/") },
{ label: "Pricing", href: href("/pricing") },
{ label: "Customers", href: href("/customers") },
{ label: "Blog", href: href("/blog") },
];
const tiers: Tier[] = [
{ name: "Starter", description: "For personal sites and side projects.", monthly: 0, yearly: 0, features: ["10K events / month", "1 site", "Realtime dashboard", "30-day history"], cta: "Start free" },
{ name: "Growth", description: "For products finding their market.", monthly: 24, yearly: 19, features: ["1M events / month", "10 sites", "Funnels and goals", "Unlimited history", "Shared reports"], cta: "Start 14-day trial", featured: true },
{ name: "Business", description: "For teams with many products.", monthly: 96, yearly: 79, features: ["10M events / month", "Unlimited sites", "Retention cohorts", "SSO and roles", "Priority support"], cta: "Start 14-day trial" },
];
const compare: CompareGroup[] = [
{
title: "Measurement",
rows: [
{ feature: "Events / month", values: ["10K", "1M", "10M"] },
{ feature: "Sites", values: ["1", "10", "Unlimited"] },
{ feature: "Data history", values: ["30 days", "Unlimited", "Unlimited"] },
{ feature: "Realtime dashboard", values: [true, true, true] },
],
},
{
title: "Insights",
rows: [
{ feature: "Goals and custom events", values: [false, true, true] },
{ feature: "Funnels", values: [false, true, true] },
{ feature: "Retention cohorts", values: [false, false, true] },
{ feature: "Shared reports", values: [false, true, true] },
],
},
{
title: "Team",
rows: [
{ feature: "Seats", values: ["1", "5", "Unlimited"] },
{ feature: "SSO and roles", values: [false, false, true] },
{ feature: "Support", values: ["Community", "Email", "Priority"] },
],
},
];
const pricingFaq: FaqItem[] = [
{ question: "What counts as an event?", answer: "A page view or a custom event you send. Visitors themselves are free, however many there are." },
{ question: "What happens if I go over my limit?", answer: "Nothing breaks. We email you at 80% and 100% and suggest a plan; we never stop counting mid-month." },
{ question: "Do I need a card for the trial?", answer: "No. Start the 14-day trial with just an email. We ask for billing details only if you keep a paid plan." },
{ question: "Can I cancel anytime?", answer: "Yes, in one click from Settings. Plans are month to month, or yearly with two months free." },
{ question: "Do you offer discounts?", answer: "Registered non-profits and open-source projects get 50% off Growth. Email billing@lumen.example from your organisation's domain." },
{ question: "Which currencies and payment methods do you take?", answer: "USD, EUR and GBP, by card or, on yearly Business plans, by bank transfer against an invoice." },
];
const releases: Release[] = [
{ version: "2.0", date: "Oct 2026", title: "Funnels 2.0", notes: ["Build funnels from any event, not only page views.", "See the median time between steps.", "Compare two date ranges side by side."] },
{ version: "1.9", date: "Sep 2026", title: "Retention cohorts", notes: ["Weekly and monthly cohorts on the Business plan.", "Cohorts can be filtered by source or country."] },
{ version: "1.8", date: "Aug 2026", title: "Shared reports", notes: ["Share a live, read-only dashboard with a link.", "Optional password and expiry date per link."] },
{ version: "1.7", date: "Jul 2026", title: "Google Analytics import", notes: ["Bring in up to two years of history during signup.", "Imported data is marked so you can tell it apart."] },
];
const footer: FooterColumn[] = [
{
title: "Product",
links: [
{ label: "Features", href: href("/") },
{ label: "Pricing", href: href("/pricing") },
{ label: "Changelog", href: href("/changelog") },
{ label: "Customers", href: href("/customers") },
{ label: "Sign in", href: href("/signin") },
],
},
{
title: "Company",
links: [
{ label: "About", href: href("/about") },
{ label: "Careers", href: href("/careers") },
{ label: "Blog", href: href("/blog") },
{ label: "Contact", href: href("/contact") },
],
},
{
title: "Resources",
links: [
{ label: "Help center", href: href("/app/help") },
{ label: "Platform status", href: href("/app/status") },
{ label: "Live demo", href: href("/app") },
{ label: "All pages", href: href("/pages") },
],
},
{ title: "Legal", links: [{ label: "Privacy", href: href("/privacy") }, { label: "Terms", href: href("/terms") }] },
];
type LegalRoute = "/privacy" | "/terms";
const legal: Record<LegalRoute, { title: string; updated: string; sections: { heading: string; body: string }[] }> = {
"/privacy": {
title: "Privacy policy",
updated: "Updated 3 September 2026",
sections: [
{ heading: "What we collect from your visitors", body: "Page URL, referrer, browser, operating system, screen size and country, worked out from the IP address and then discarded. We never store IP addresses, set cookies or build profiles." },
{ heading: "What we collect from you", body: "Your name, email and billing details, so we can run your account and send invoices. We don't sell or share them, and we don't use them for advertising." },
{ heading: "Where it lives", body: "On servers in Frankfurt and Helsinki, run by an EU company. Our data processing agreement is part of the Terms and applies to every plan." },
{ heading: "Your rights", body: "Export or delete your data from Settings at any time. For anything else, email privacy@lumen.example and a person replies within two working days." },
],
},
"/terms": {
title: "Terms of service",
updated: "Updated 3 September 2026",
sections: [
{ heading: "Your account", body: "You're responsible for the sites you add and for keeping your password safe. Invite colleagues instead of sharing a login." },
{ heading: "Billing", body: "Plans renew monthly or yearly until you cancel. If you go over your event limit we email you first; we never stop counting mid-month or charge without warning." },
{ heading: "Cancelling", body: "Cancel from Settings in one click. Your dashboard stays readable until the end of the period, and you can export everything before it closes." },
{ heading: "Data processing", body: "Lumen acts as your processor under the GDPR. We process visitor data only to produce your reports and delete raw events after 24 months." },
],
},
};
function Pricing() {
return (
<>
<PricingTiers
headingLevel="h1"
title="Simple pricing, based on events"
tiers={tiers}
ctaHref={href("/signup")}
unit="/ month"
note="Starter is free up to 10,000 events · paid plans start with 14 days free, no card required"
/>
<PricingCompare title="Compare plans" plans={["Starter", "Growth", "Business"]} groups={compare} highlight={1} />
<Faq
title="Billing, plainly"
description="Anything else? We answer every message ourselves."
contact={{ label: "Email billing@lumen.example", href: "mailto:billing@lumen.example" }}
items={pricingFaq}
/>
<CtaPanel
title="Try every Growth feature free for 14 days"
description={`Downgrade to Starter at any time and your history stays. ${riskReducer}`}
primary={primaryCta}
secondary={{ label: "Read the changelog", href: href("/changelog") }}
/>
</>
);
}
function ChangelogPage() {
return (
<>
<Changelog title="What's new in Lumen" releases={releases} headingLevel="h1" />
<Newsletter title="Release notes, once a month" description="New features and the occasional analytics essay. Unsubscribe in one click." />
</>
);
}
function LegalPage({ page }: { page: LegalRoute }) {
const doc = legal[page];
return (
<article className="bg-bg px-6 py-20 text-fg md:px-8 md:py-28">
<div className="mx-auto max-w-2xl">
<p className="text-sm text-muted">{doc.updated}</p>
<h1 className="mt-3 font-display text-4xl leading-tight md:text-5xl">{doc.title}</h1>
{doc.sections.map((s) => (
<section key={s.heading} className="mt-10">
<h2 className="text-lg font-medium">{s.heading}</h2>
<p className="mt-2 leading-relaxed text-muted">{s.body}</p>
</section>
))}
<a href={href("/")} className={buttonVariants({ variant: "surface", className: "mt-12" })}>
Back to home
</a>
</div>
</article>
);
}
/* ----------------------------------------------------------------- pages */
const logo = (
<BrandMark>
<LumenGlyph />
</BrandMark>
);
/** Site chrome for marketing pages: sticky navbar, main landmark and footer. */
function MarketingPage({ active, children }: { active: string; children: ReactNode }) {
return (
<div className="relative isolate min-h-dvh bg-bg text-fg">
{/* The same page backdrop as the app and store; the theme's --aurora-strength sets it. */}
<AuroraBackdrop grid={false} className="fixed print:hidden" />
<div className="sticky top-0 z-40 border-b border-line bg-bg/80 backdrop-blur-xl">
<Navbar
backdrop={false}
items={nav}
activeHref={href(active)}
homeHref={href("/")}
logo={logo}
signIn={{ label: "Sign in", href: href("/signin") }}
cta={primaryCta}
/>
</div>
<main id="main">{children}</main>
<FooterColumns
brand="lumen"
logo={
<BrandMark size="lg">
<LumenGlyph />
</BrandMark>
}
tagline="Privacy-friendly product analytics."
columns={footer}
homeHref={href("/")}
// Lumen has no social accounts to link yet.
socials={[]}
/>
</div>
);
}
const lastSegment = (path: string) => path.split("/").pop() ?? "";
function BlogPostPage({ path }: { path: string }) {
const p = blogPosts.find((x) => x.slug === lastSegment(path)) ?? blogPosts[0]!;
return (
<BlogPost
tag={p.tag}
title={p.title}
excerpt={p.excerpt}
date={p.date}
readingTime={p.readingTime}
sections={p.sections}
pullQuote={p.pullQuote}
author={author}
related={relatedTo(p.slug)}
blogHref={href("/blog")}
/>
);
}
function CaseStudyPage({ path }: { path: string }) {
const index = Math.max(
0,
caseStudies.findIndex((c) => c.slug === lastSegment(path)),
);
const study = caseStudies[index]!;
const next = caseStudies[(index + 1) % caseStudies.length]!;
return (
<CaseStudy
company={study.company}
industry={study.industry}
title={study.title}
summary={study.summary}
metrics={study.metrics}
facts={study.facts}
chapters={study.chapters}
quote={study.quote}
customersHref={href("/customers")}
signUpHref={href("/signup")}
salesHref={href("/contact")}
next={{ company: next.company, title: next.card.metricLabel, href: href(`/customers/${next.slug}`) }}
/>
);
}
/* ---------------------------------------------------------------- routes */
interface SitePage {
/** Hash path; `:slug` matches any one segment. */
path: string;
title: string;
/** Group on the #/pages index. */
section: string;
/** A concrete path for `:slug` routes. */
sample?: string;
/** "site" wraps the page in the navbar and footer; "bare" pages fill the screen; "own" pages bring their own chrome. */
chrome: "site" | "bare" | "own";
/** The navbar link to mark; defaults to `path`. */
nav?: string;
render: (path: string) => ReactNode;
}
const me = { name: "Ada Kaya", email: "ada@lumen.example" };
const sitePages: SitePage[] = [
// Marketing.
{ path: "/", title: "Home", section: "Marketing", chrome: "site", render: () => <LumenHome /> },
{ path: "/pricing", title: "Pricing", section: "Marketing", chrome: "site", render: () => <Pricing /> },
{ path: "/changelog", title: "Changelog", section: "Marketing", chrome: "site", render: () => <ChangelogPage /> },
{
path: "/customers",
title: "Customers",
section: "Marketing",
chrome: "site",
render: () => (
<Customers
title="38,214 sites count visits with Lumen"
lead="From personal blogs to listed retailers. Here's what changed for a few of them after the cookie banner went."
stories={stories}
signUpHref={href("/signup")}
salesHref={href("/contact")}
/>
),
},
{ path: "/customers/:slug", title: "Case study", section: "Marketing", sample: "/customers/polaris", chrome: "site", nav: "/customers", render: (path) => <CaseStudyPage path={path} /> },
{ path: "/privacy", title: "Privacy policy", section: "Marketing", chrome: "site", render: () => <LegalPage page="/privacy" /> },
{ path: "/terms", title: "Terms of service", section: "Marketing", chrome: "site", render: () => <LegalPage page="/terms" /> },
// Company.
{
path: "/about",
title: "About",
section: "Company",
chrome: "site",
render: () => (
<About
title={about.title}
lead={about.lead}
stats={about.stats}
story={about.story}
milestones={about.milestones}
values={about.values}
team={about.team}
careersHref={href("/careers")}
contactHref={href("/contact")}
hiringNote={`${roles.length} open roles across engineering, design and support. Remote within Europe and Türkiye.`}
/>
),
},
{
path: "/careers",
title: "Careers",
section: "Company",
chrome: "site",
render: () => (
<Careers
title="Build analytics people don't have to consent to"
lead="We're 27 people across 9 countries. Small teams, real ownership and a pace you can keep for years."
roles={roles}
roleHref={(role) => href(`/careers/${role.slug}`)}
openApplicationHref="mailto:jobs@lumen.example"
/>
),
},
{
path: "/careers/:slug",
title: "Open role",
section: "Company",
sample: `/careers/${roles[0]!.slug}`,
chrome: "site",
nav: "/careers",
render: (path) => (
<CareersRole role={roles.find((r) => r.slug === lastSegment(path)) ?? roles[0]} detail={roleDetail} careersHref={href("/careers")} privacyHref={href("/privacy")} />
),
},
{ path: "/blog", title: "Blog", section: "Company", chrome: "site", render: () => <BlogCards title="The Lumen journal" posts={posts} allHref={null} headingLevel="h1" /> },
{ path: "/blog/:slug", title: "Blog post", section: "Company", sample: `/blog/${blogPosts[0]!.slug}`, chrome: "site", nav: "/blog", render: (path) => <BlogPostPage path={path} /> },
{
path: "/contact",
title: "Contact",
section: "Company",
chrome: "site",
render: () => <ContactPage channels={channels} offices={offices} privacyHref={href("/privacy")} />,
},
// Account: sign-in, sign-up and recovery flows.
{
path: "/signin",
title: "Sign in",
section: "Account",
chrome: "bare",
render: () => (
<AuthSignIn
brand="lumen"
logo={logo}
homeHref={href("/")}
signUpHref={href("/signup")}
forgotHref={href("/forgot")}
// Demo: prefilled, and every way in leads straight to the app.
defaultValues={{ email: me.email, password: "demo-password" }}
onProvider={() => go("/app")}
onSubmit={() => go("/app")}
/>
),
},
{
path: "/signin/split",
title: "Sign in, split with two-factor",
section: "Account",
chrome: "bare",
render: () => (
<AuthSignInSplit
brand="lumen"
logo={logo}
homeHref={href("/")}
signUpHref={href("/signup/split")}
forgotHref={href("/forgot")}
quote={{ quote: "We dropped our cookie banner and our bounce rate in the same week.", name: "Ada Kowalski", role: "Head of growth, Polaris" }}
proof="Counting visits on 38,214 sites, without cookies."
defaultValues={{ email: me.email, password: "demo-password" }}
onProvider={() => go("/otp")}
// This variant asks for a two-factor code before the app opens.
onSubmit={() => go("/otp")}
/>
),
},
{
path: "/signup",
title: "Sign up",
section: "Account",
chrome: "bare",
render: () => (
<AuthSignUp
brand="lumen"
logo={logo}
title="Know your product in minutes"
perks={["14-day free trial, no card required", "No cookies, no consent banner", "Import your Google Analytics history"]}
homeHref={href("/")}
signInHref={href("/signin")}
termsHref={href("/terms")}
privacyHref={href("/privacy")}
defaultValues={{ firstName: "Ada", lastName: "Kaya", email: me.email, password: "Demo-password-2026" }}
onSubmit={() => go("/app")}
/>
),
},
{
path: "/signup/split",
title: "Sign up, split with email check",
section: "Account",
chrome: "bare",
render: () => (
<AuthSignUpSplit
brand="lumen"
logo={logo}
homeHref={href("/")}
signInHref={href("/signin/split")}
termsHref={href("/terms")}
privacyHref={href("/privacy")}
quote={{ quote: "Funnels showed us one confusing step in onboarding. We fixed it and trials went up 18%.", name: "Jun Lee", role: "Product manager, Orbitly" }}
proof="14-day trial. No card needed."
defaultValues={{ name: me.name, email: me.email, password: "Demo-password-2026" }}
onProvider={() => go("/app")}
onSubmit={async () => {
await delay(700);
go("/verify-email");
}}
/>
),
},
{
path: "/forgot",
title: "Forgot password",
section: "Account",
chrome: "bare",
render: () => (
<AuthForgot
brand="lumen"
logo={logo}
homeHref={href("/")}
signInHref={href("/signin")}
defaultEmail={me.email}
onSubmit={async () => {
await delay(700);
go("/check-email");
}}
/>
),
},
{
path: "/check-email",
title: "Check your email",
section: "Account",
chrome: "bare",
render: () => (
<AuthCheckEmail
brand="lumen"
logo={logo}
homeHref={href("/")}
email={me.email}
title="Check your inbox"
description={
<>
We sent a password reset link to <span className="break-all text-fg">{me.email}</span>. It works for 30 minutes.
</>
}
changeEmailHref={href("/forgot")}
/>
),
},
{
path: "/reset",
title: "Choose a new password",
section: "Account",
chrome: "bare",
render: () => <AuthReset brand="lumen" logo={logo} homeHref={href("/")} signInHref={href("/signin")} email={me.email} />,
},
{
path: "/otp",
title: "Two-factor code",
section: "Account",
chrome: "bare",
render: () => (
<AuthOtp
brand="lumen"
logo={logo}
homeHref={href("/")}
destination="ada@l•••••.example"
recoveryHref={href("/contact")}
signInHref={href("/signin")}
continueHref={href("/app")}
onVerify={async () => {
await delay(700);
go("/app");
}}
/>
),
},
{
path: "/verify-email",
title: "Verify email",
section: "Account",
chrome: "bare",
render: () => <AuthVerifyEmail brand="lumen" logo={logo} homeHref={href("/")} email={me.email} continueHref={href("/app")} supportHref={href("/contact")} />,
},
{
path: "/lock",
title: "Lock screen",
section: "Account",
chrome: "bare",
render: () => (
<AuthLockScreen
user={me}
switchAccountHref={href("/signin")}
forgotHref={href("/forgot")}
onUnlock={async () => {
await delay(700);
go("/app");
}}
/>
),
},
// System pages.
{
path: "/404",
title: "Page not found",
section: "System",
chrome: "site",
render: () => (
<Error404
title="We couldn't find that page"
description="The link may be old or mistyped. The product, pricing and changelog are one click away."
homeHref={href("/")}
supportHref={href("/contact")}
/>
),
},
{
path: "/500",
title: "Server error",
section: "System",
chrome: "site",
render: () => (
<Error500
description="Your dashboards and data are safe. We've logged the error and the on-call engineer has been paged."
reference="LUM-7Q4K-2291"
homeHref={href("/")}
statusHref={href("/app/status")}
supportHref={href("/contact")}
onRetry={async () => {
await delay(900);
go("/app");
}}
/>
),
},
{
path: "/403",
title: "Access denied",
section: "System",
chrome: "site",
render: () => (
<Error403
description="It belongs to the Finance workspace. An admin can add you, usually within a working day."
account={me}
homeHref={href("/app")}
switchAccountHref={href("/signin")}
/>
),
},
{
path: "/maintenance",
title: "Maintenance",
section: "System",
chrome: "bare",
render: () => (
<Maintenance
title="We're moving to a faster database"
description="Your data is safe and visits are still being counted. Dashboards are paused until the move finishes."
statusHref={href("/app/status")}
privacyHref={href("/privacy")}
/>
),
},
{
path: "/coming-soon",
title: "Coming soon",
section: "System",
chrome: "bare",
render: () => (
<ComingSoon
brand="lumen"
logo={logo}
homeHref={href("/")}
eyebrow="Lumen Heatmaps · Private beta"
title="See where visitors click, still without cookies"
description="Aggregated click and scroll maps for every page. Join 2,417 teams on the launch list."
privacyHref={href("/privacy")}
/>
),
},
{
path: "/offline",
title: "Offline",
section: "System",
chrome: "bare",
render: () => (
<Offline
description="Dashboards you opened recently are kept on this device. New visits appear when you reconnect."
saved={[
{ label: "Analytics overview", href: href("/app") },
{ label: "Notes", href: href("/app/notes") },
{ label: "Help: set up single sign-on", href: href("/app/help/article") },
]}
/>
),
},
{ path: "/pages", title: "All pages", section: "System", chrome: "site", render: () => <PagesIndex /> },
// The public storefront, with its own header, cart drawer and footer.
...storeRoutes.map((r): SitePage => ({ ...r, section: "Store", chrome: "own" })),
];
type Match = { kind: "site"; page: SitePage } | { kind: "app"; page: (typeof appPages)[number] };
const allPages: Match[] = [...sitePages.map((page) => ({ kind: "site" as const, page })), ...appPages.map((page) => ({ kind: "app" as const, page }))];
const toPattern = (path: string) => new RegExp(`^${path.replace(/:[a-z]+/g, "[^/]+")}$`);
/** Exact paths win over `:slug` patterns, so /app/products/new isn't a product id. */
function findPage(path: string): Match | undefined {
return allPages.find((m) => m.page.path === path) ?? allPages.find((m) => m.page.path.includes(":") && toPattern(m.page.path).test(path));
}
/** The path in the hash without query or trailing slash, or null for an in-page anchor such as "#main". */
function readRoute(): string | null {
const raw = window.location.hash.replace(/^#/, "") || "/";
if (!raw.startsWith("/")) return null;
const path = raw.split("?")[0]!.replace(/(.)\/+$/, "$1");
return path;
}
const subscribeToHash = (onChange: () => void) => {
window.addEventListener("hashchange", onChange);
return () => window.removeEventListener("hashchange", onChange);
};
/** Last page path; an in-page anchor such as "#main" keeps the page it's on. */
let lastRoute = "/";
const hashSnapshot = () => (lastRoute = readRoute() ?? lastRoute);
/**
* Tiny hash router. The server can't see the hash, so it renders no page
* (null) rather than guessing "/": guessing painted the landing page for a
* moment before a deep link such as #/app/mail took over.
*/
function useHashRoute() {
const route = useSyncExternalStore(subscribeToHash, hashSnapshot, () => null);
useEffect(() => {
if (route !== null) window.scrollTo({ top: 0 });
}, [route]);
return route;
}
/* ------------------------------------------------------------ page index */
const sectionOrder = ["Marketing", "Company", "Account", "System", "Dashboards", "Apps", "E-commerce", "Store", "Invoices", "Users", "Settings", "Support"];
/** Every route, grouped, like a premium template's page index. */
function PagesIndex() {
const sections = sectionOrder.map((name) => ({
name,
links: allPages.filter((m) => m.page.section === name).map((m) => ({ title: m.page.title, path: m.page.sample ?? m.page.path })),
}));
const total = sections.reduce((sum, s) => sum + s.links.length, 0);
return (
<section className="bg-bg px-6 py-20 text-fg md:px-8 md:py-28">
<div className="mx-auto max-w-6xl">
<p className="text-sm text-muted">{total} pages · site, account flows, system states and the app</p>
<h1 className="mt-3 font-display text-4xl leading-tight md:text-5xl">Every page in Lumen</h1>
<p className="mt-4 max-w-2xl leading-relaxed text-muted">
Open any page directly. App pages share one sidebar, so once you're inside you can reach the rest from the menu.
</p>
<div className="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{sections.map((s) => (
<section key={s.name} className="surface rounded-card p-5" aria-labelledby={`pages-${s.name}`}>
<h2 id={`pages-${s.name}`} className="flex items-baseline justify-between gap-3 text-sm font-medium">
{s.name}
<span className="text-xs font-normal text-subtle tabular-nums">{s.links.length}</span>
</h2>
<ul className="mt-3 grid gap-0.5">
{s.links.map((link) => (
<li key={link.path}>
<a
href={href(link.path)}
className="-mx-2 flex items-baseline justify-between gap-3 rounded-control px-2 py-1.5 text-sm transition-colors outline-none hover:bg-surface-hover focus-visible:outline-2 focus-visible:outline-accent"
>
<span className="min-w-0 truncate">{link.title}</span>
<span className="shrink-0 font-mono text-[11px] text-subtle">{link.path}</span>
</a>
</li>
))}
</ul>
</section>
))}
</div>
<a href={href("/")} className={buttonVariants({ variant: "surface", className: "mt-12" })}>
Back to home
</a>
</div>
</section>
);
}
/* ------------------------------------------------------------- template */
export function TemplateLumen() {
const route = useHashRoute();
// Server and first paint: just the page colour, until the hash says which page.
if (route === null) return <div className="min-h-dvh bg-bg" />;
const match = findPage(route) ?? { kind: "site" as const, page: sitePages.find((p) => p.path === "/404")! };
if (match.kind === "app") return <LumenAppPage page={match.page} route={route} />;
const { page } = match;
if (page.chrome === "own") return <>{page.render(route)}</>;
if (page.chrome === "bare") return <main id="main">{page.render(route)}</main>;
return <MarketingPage active={page.nav ?? page.path}>{page.render(route)}</MarketingPage>;
}"use client";
import { useEffect, useState } from "react";
import { BentoFeatures, type BentoItem } from "@/registry/kit/blocks/bento-features";
import { CtaPanel } from "@/registry/kit/blocks/cta-panel";
import { Faq, type FaqItem } from "@/registry/kit/blocks/faq";
import { HeroProduct, ProductPreview } from "@/registry/kit/blocks/hero-product";
import { InstallSnippet, type InstallSnippetTab, type InstallStep } from "@/registry/kit/blocks/install-snippet";
import { ProofBand, type ProofMetric, type ProofQuote } from "@/registry/kit/blocks/proof-band";
import { VsList, type VsRow } from "@/registry/kit/blocks/vs-list";
import { href } from "@/registry/kit/templates/template-lumen-app";
import { Badge } from "@/registry/kit/ui/badge";
import { BarList } from "@/registry/kit/ui/bar-list";
import { ChartFunnel } from "@/registry/kit/ui/chart-funnel";
import { CopyButton } from "@/registry/kit/ui/copy-button";
import { LiveIndicator } from "@/registry/kit/ui/live-indicator";
import { ScrollTextReveal } from "@/registry/kit/ui/scroll-text-reveal";
import { Switch } from "@/registry/kit/ui/switch";
/*
* Lumen's home page: product-led hero, a bento of working product moments,
* install, proof, a statement, the comparison, FAQ and the final CTA.
* Composed only from registry blocks and components.
*/
// The hero and the final CTA ask for the same thing in the same words (rule 4b).
export const primaryCta = { label: "Start free trial", href: href("/signup") };
export const riskReducer = "Free for 14 days, no card required. Cancel in one click.";
const script = '<script defer src="https://cdn.lumen.example/l.js" data-site="polaris.example"></script>';
/* ------------------------------------------------------- bento visuals */
/** Flip the switch to see the page with the consent pop-up Lumen makes unnecessary. */
function ConsentToggle() {
const [withBanner, setWithBanner] = useState(false);
return (
<div className="grid w-full grid-cols-1 gap-3">
<div aria-hidden className="surface relative h-28 overflow-hidden rounded-field p-3">
<div className="h-2 w-1/3 rounded-full bg-fg/15" />
<div className="mt-3 h-2 w-4/5 rounded-full bg-fg/10" />
<div className="mt-2 h-2 w-3/5 rounded-full bg-fg/10" />
<div className="mt-2 h-2 w-2/3 rounded-full bg-fg/10" />
<div
className={`surface-raised absolute inset-x-2 bottom-2 rounded-field p-2.5 text-[11px] leading-snug transition-[translate,opacity] duration-500 ease-theme ${withBanner ? "translate-y-0 opacity-100" : "translate-y-4 opacity-0"}`}
>
We use cookies to improve your experience.
<span className="mt-1.5 flex gap-1.5">
<span className="rounded-control bg-fg px-2 py-0.5 text-bg">Accept all</span>
<span className="rounded-control border border-line px-2 py-0.5">Manage</span>
</span>
</div>
</div>
<div className="flex items-center justify-between gap-3">
<label htmlFor="lumen-consent" className="text-xs text-muted">
Show the usual cookie pop-up
</label>
<Switch id="lumen-consent" checked={withBanner} onCheckedChange={setWithBanner} />
</div>
<p aria-live="polite" className="text-xs">
{withBanner ? (
<Badge variant="warning">Consent needed before counting</Badge>
) : (
<Badge variant="success">0 cookies set · nothing to consent to</Badge>
)}
</p>
</div>
);
}
const liveDrift = [1, -2, 2, 1, -1, -2, 3, -1];
/** Visitors on the site right now; nudges by a visitor or two every few seconds. */
function LiveVisitors() {
const [now, setNow] = useState(128);
useEffect(() => {
let i = 0;
const id = setInterval(() => setNow((n) => n + liveDrift[i++ % liveDrift.length]!), 3000);
return () => clearInterval(id);
}, []);
return (
<div className="grid w-full grid-cols-1 gap-3">
<div className="flex items-baseline justify-between gap-3">
<span className="text-3xl font-semibold tracking-tight tabular-nums">{now}</span>
<LiveIndicator className="text-xs text-muted" label="Online now" />
</div>
<BarList
label="Visitors online now, by country"
size="sm"
items={[
{ label: "Germany", value: 41, leading: <span className="font-mono text-[10px] text-muted">DE</span> },
{ label: "Türkiye", value: 29, leading: <span className="font-mono text-[10px] text-muted">TR</span> },
{ label: "United States", value: 23, leading: <span className="font-mono text-[10px] text-muted">US</span> },
]}
/>
</div>
);
}
const bento: BentoItem[] = [
{
title: "Find the step where people give up",
body: "Build a funnel from any page or event. Orbitly fixed one confusing onboarding screen and lifted trials 18%.",
size: "lg",
visual: (
<ChartFunnel
label="Trial funnel, last 30 days"
height={128}
steps={[
{ label: "Viewed pricing", value: 9412 },
{ label: "Started trial", value: 2318 },
{ label: "Added script", value: 1604 },
{ label: "Upgraded", value: 412 },
]}
/>
),
},
{
title: "Drop the cookie banner",
body: "No cookies and no personal data, so most sites can remove the pop-up. Polaris saw 11.4% fewer bounces that week.",
visual: <ConsentToggle />,
},
{
title: "See who's here right now",
body: "Realtime visitors, pages and countries, updated every few seconds without a refresh.",
visual: <LiveVisitors />,
},
{
title: "1.4 kB, loaded after your page",
body: "One script tag, no tag manager. On a typical site it adds less than 5 ms.",
visual: (
<div className="grid w-full grid-cols-1 gap-3">
<div className="surface-field flex items-center justify-between gap-2 rounded-field px-3 py-2">
<code className="truncate font-mono text-xs text-fg/85">{script}</code>
<CopyButton value={script} iconOnly label="Copy script tag" variant="ghost" />
</div>
<dl className="grid grid-cols-3 gap-2 text-center text-xs">
{[
["1.4 kB", "Script"],
["<5 ms", "Added"],
["0", "Cookies"],
].map(([value, label]) => (
<div key={label} className="surface flex flex-col-reverse rounded-field px-2 py-2">
<dt className="text-muted">{label}</dt>
<dd className="text-sm font-semibold tabular-nums">{value}</dd>
</div>
))}
</dl>
</div>
),
},
{
title: "Share a live dashboard",
body: "Send clients or the whole company a read-only link, with an optional password and expiry date.",
visual: (
<div className="grid w-full grid-cols-1 gap-3">
<div className="surface-field flex items-center justify-between gap-2 rounded-field px-3 py-2">
<span className="truncate font-mono text-xs text-fg/85">lumen.example/share/polaris-q4</span>
<CopyButton value="https://lumen.example/share/polaris-q4" iconOnly label="Copy share link" variant="ghost" />
</div>
<div className="flex flex-wrap items-center gap-1.5">
<Badge variant="accent">Password on</Badge>
<Badge>Expires 30 Nov</Badge>
<span className="ml-auto text-xs text-muted tabular-nums">Opened 214 times</span>
</div>
</div>
),
},
];
/* --------------------------------------------------------------- content */
const steps: InstallStep[] = [
{ title: "Add one script tag", body: "Paste it into your site's head, or install the npm package for your framework." },
{ title: "Bring your history", body: "Connect Google Analytics during signup and we import the last two years, marked so you can tell it apart." },
{ title: "Share what matters", body: "Send a live, read-only link to a client or the whole company. Everyone reads the same numbers." },
];
const snippets: InstallSnippetTab[] = [
{ label: "HTML", file: "index.html", code: `<!-- In <head>, on every page -->\n<script defer\n src="https://cdn.lumen.example/l.js"\n data-site="polaris.example"></script>` },
{
label: "Next.js",
file: "app/layout.tsx",
code: `import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
<Script src="https://cdn.lumen.example/l.js" data-site="polaris.example" />
</html>
);
}`,
},
{
label: "npm",
file: "src/analytics.ts",
code: `// npm install @lumen/tracker
import { init, track } from "@lumen/tracker";
init({ site: "polaris.example" });
track("Signed up", { plan: "growth" });`,
},
];
const metrics: ProofMetric[] = [
{ value: "38,214", label: "Sites measured" },
{ value: "2.14B", label: "Events a month" },
{ value: "1.4 kB", label: "Script size" },
{ value: "0", label: "Cookies set" },
];
const quotes: ProofQuote[] = [
{ result: "11.4% fewer bounces", quote: "We dropped our cookie banner and our bounce rate in the same week. The numbers finally match our sales data.", name: "Ada Kowalski", role: "Head of growth, Polaris" },
{ result: "+18% trials", quote: "Funnels showed us one confusing step in onboarding. We fixed it and trials went up.", name: "Jun Lee", role: "Product manager, Orbitly" },
{ result: "23 people read it weekly", quote: "Our whole team reads the dashboard now. It answers the question before you've finished asking it.", name: "Mateo Ruiz", role: "Founder, Kitebase" },
{ result: "2 years imported in 6 minutes", quote: "The Google Analytics import kept our history, so the switch cost us nothing.", name: "Sara Okafor", role: "Marketing lead, Fernhill" },
];
const vs: VsRow[] = [
{ topic: "Consent banner", them: "Required for EU visitors, and many click it away", us: "Not needed: no cookies, no personal data" },
{ topic: "Script weight", them: "45 kB or more, plus a tag manager", us: "1.4 kB, loaded after your page" },
{ topic: "Dashboard", them: "Dozens of reports to dig through", us: "Visitors, sources, pages and goals on one screen" },
{ topic: "Your data", them: "Shared with an ad network", us: "Yours, stored in Frankfurt and Helsinki" },
{ topic: "Price", them: "Free, paid for with your visitors' data", us: "Free up to 10,000 events, then from $19 a month" },
];
const faq: FaqItem[] = [
{ question: "How is Lumen different from Google Analytics?", answer: "Lumen counts visits without cookies or personal data, so you don't need a consent banner, and the dashboard fits on one screen." },
{ question: "Do I really not need a cookie banner?", answer: "For most sites, no: Lumen stores nothing on the visitor's device and never processes personal data. Your legal team can read our DPA on the Privacy page." },
{ question: "Will it slow my site down?", answer: "The script is 1.4 kB and loads after your page. On a typical site it adds less than 5 ms." },
{ question: "Can I import my old data?", answer: "Yes. Connect Google Analytics during signup and we bring in the last two years." },
{ question: "Where is my data stored?", answer: "On servers in Frankfurt and Helsinki, owned by an EU company. Export or delete everything at any time." },
{ question: "What happens when the trial ends?", answer: "You choose a plan or drop to Starter, which stays free up to 10,000 events a month. We never delete your history without asking." },
{ question: "Can my clients see their own numbers?", answer: "Yes. Shared reports give anyone a live, read-only dashboard, with an optional password and expiry date." },
];
/* ------------------------------------------------------------------ page */
export function LumenHome() {
return (
<>
<HeroProduct
badge="Read what changed in Funnels 2.0"
badgeHref={href("/changelog")}
title="Know what visitors do, without cookies"
description="Where people come from, what they read and where they drop off, on one screen. No consent banner in their way."
primary={primaryCta}
secondary={{ label: "Open the live demo", href: href("/app") }}
notes={["14 days free, no card", "No cookie banner needed", "Hosted in the EU"]}
url="app.lumen.example/polaris"
visual={<ProductPreview site="polaris.example" />}
logos={["Polaris", "Kitebase", "Halcyon", "Orbitly", "Fernhill", "Northwind"]}
logosLabel="Counting visits on 38,214 sites, including"
/>
<BentoFeatures
eyebrow="Why Lumen"
title="Clear answers, and nothing stored on their devices"
description="What teams get in their first month, in their own numbers."
items={bento}
/>
<InstallSnippet
eyebrow="How it works"
title="Live in five minutes, in three steps"
description="No tag manager, no consent setup, no data team."
steps={steps}
snippets={snippets}
note="1.4 kB gzipped · loads after your page · sets no cookies"
action={{ label: "Read the install guide", href: href("/app/help") }}
/>
<ProofBand
eyebrow="Customers"
title="Teams switched and stopped worrying"
metrics={metrics}
quotes={quotes}
action={{ label: "Read customer stories", href: href("/customers") }}
/>
<section className="bg-bg px-4 py-12 text-fg sm:px-6 md:px-8 md:py-16">
<ScrollTextReveal as="h2" className="mx-auto max-w-4xl font-display text-2xl leading-snug md:text-4xl">
Your visitors came to read, buy or sign up, not to be followed around the web. Lumen counts what happened on your site and forgets who did it.
</ScrollTextReveal>
</section>
<VsList
eyebrow="Compared"
title="Lumen vs the usual analytics"
description="Same questions answered, without the banner, the weight or the ad network."
us="Lumen"
them="The usual analytics"
rows={vs}
note={riskReducer}
/>
<Faq
compact
structuredData
title="Questions, answered"
description="Still curious? A person replies within a day."
contact={{ label: "Email hello@lumen.example", href: "mailto:hello@lumen.example" }}
items={faq}
/>
<CtaPanel
compact
title="Start measuring in five minutes"
description="Add one script tag and watch your first visitors arrive."
primary={primaryCta}
secondary={{ label: "Compare plans", href: href("/pricing") }}
note={riskReducer}
/>
</>
);
}"use client";
import type { ReactNode } from "react";
import { AppShell, type AppShellMenuItem } from "@/registry/kit/blocks/app-shell";
import { DashboardAnalytics, type DashboardChrome } from "@/registry/kit/dashboards/dashboard-analytics";
import { DashboardDevops, type DashboardNotice, type Deployment, type Service } from "@/registry/kit/dashboards/dashboard-devops";
import { DashboardFinance, type FinanceContent } from "@/registry/kit/dashboards/dashboard-finance";
import { CalendarScreen } from "@/registry/kit/screens/screen-calendar";
import { ChatScreen } from "@/registry/kit/screens/screen-chat";
import { ContactsScreen } from "@/registry/kit/screens/screen-contacts";
import { CustomerDetailScreen } from "@/registry/kit/screens/screen-customer-detail";
import { CustomersScreen, sampleCustomers } from "@/registry/kit/screens/screen-customers";
import { CrmDashboardScreen } from "@/registry/kit/screens/screen-dashboard-crm";
import { EcommerceDashboardScreen } from "@/registry/kit/screens/screen-dashboard-ecommerce";
import { ProjectsDashboardScreen } from "@/registry/kit/screens/screen-dashboard-projects";
import { FaqScreen } from "@/registry/kit/screens/screen-faq";
import { FileManagerScreen } from "@/registry/kit/screens/screen-file-manager";
import { HelpArticleScreen } from "@/registry/kit/screens/screen-help-article";
import { HelpCenterScreen } from "@/registry/kit/screens/screen-help-center";
import { InvoiceEditorScreen } from "@/registry/kit/screens/screen-invoice-editor";
import { InvoicePreviewScreen } from "@/registry/kit/screens/screen-invoice-preview";
import { InvoicesScreen, sampleInvoices } from "@/registry/kit/screens/screen-invoices";
import { KanbanScreen } from "@/registry/kit/screens/screen-kanban";
import { MailScreen, defaultMessages } from "@/registry/kit/screens/screen-mail";
import { NotesScreen } from "@/registry/kit/screens/screen-notes";
import { OrderDetailScreen, sampleOrderDetail } from "@/registry/kit/screens/screen-order-detail";
import { OrdersScreen, sampleOrders } from "@/registry/kit/screens/screen-orders";
import { ProductDetailScreen, sampleProductDetail } from "@/registry/kit/screens/screen-product-detail";
import { ProductFormScreen, emptyProductForm } from "@/registry/kit/screens/screen-product-form";
import { ProductsScreen, sampleProducts } from "@/registry/kit/screens/screen-products";
import { RolesScreen } from "@/registry/kit/screens/screen-roles";
import { SearchResultsScreen } from "@/registry/kit/screens/screen-search-results";
import { SettingsApiScreen } from "@/registry/kit/screens/screen-settings-api";
import { SettingsBillingScreen } from "@/registry/kit/screens/screen-settings-billing";
import { SettingsConnectionsScreen } from "@/registry/kit/screens/screen-settings-connections";
import { SettingsGeneralScreen } from "@/registry/kit/screens/screen-settings-general";
import { SettingsNotificationsScreen } from "@/registry/kit/screens/screen-settings-notifications";
import { SettingsSecurityScreen } from "@/registry/kit/screens/screen-settings-security";
import { storeItemCount } from "@/registry/kit/screens/screen-store";
import { TasksScreen } from "@/registry/kit/screens/screen-tasks";
import { TeamScreen } from "@/registry/kit/screens/screen-team";
import { storeHome, useLumenCart } from "@/registry/kit/templates/template-lumen-store";
import { TimelineScreen } from "@/registry/kit/screens/screen-timeline";
import { UserProfileScreen, sampleProfile } from "@/registry/kit/screens/screen-user-profile";
import { UsersScreen, sampleUsers } from "@/registry/kit/screens/screen-users";
import type { SettingsLink } from "@/registry/kit/screens/settings-layout";
import type { NotificationItem } from "@/registry/kit/ui/notification-bell";
import { Progress } from "@/registry/kit/ui/progress";
import type { SidebarGroup } from "@/registry/kit/ui/sidebar-nav";
/*
* Lumen's app area: one navigation, one person and one shell shared by every
* screen. The three full dashboards bring their own shell, so they get the
* same nav, brand and profile through their DashboardChrome props.
*/
export const href = (route: string) => `#${route}`;
export const go = (route: string) => {
window.location.hash = route;
};
/** Lumen's glyph: a small sun, set inside the accent BrandMark tile. */
export function LumenGlyph() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" aria-hidden>
<circle cx="12" cy="12" r="3.25" fill="currentColor" stroke="none" />
<path d="M12 3.5v2M12 18.5v2M3.5 12h2M18.5 12h2M6 6l1.4 1.4M16.6 16.6 18 18M6 18l1.4-1.4M16.6 7.4 18 6" />
</svg>
);
}
function AppIcon({ d }: { d: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden className="size-4.5 shrink-0">
<path d={d} />
</svg>
);
}
const icon = {
analytics: "M4 19V9M10 19V5M16 19v-7M22 19H2",
cart: "M3 4h2l2.4 11.2a2 2 0 0 0 2 1.6h7.7a2 2 0 0 0 2-1.5L21 8H6M10 21h.01M18 21h.01",
crm: "M16 11a4 4 0 1 0-8 0M3 21a9 9 0 0 1 18 0M12 7V3M8 5l4 2 4-2",
projects: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zM8 13h8",
billing: "M3 6a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zM3 10h18M7 15h3",
status: "M3 12h4l3-8 4 16 3-8h4",
mail: "M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zM3.5 6l8.5 7 8.5-7",
chat: "M21 12a8 8 0 0 1-11.8 7L4 20l1.1-4.6A8 8 0 1 1 21 12z",
calendar: "M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zM4 10h16M8 2v4M16 2v4",
kanban: "M4 4h4v16H4zM10 4h4v10h-4zM16 4h4v13h-4z",
tasks: "M9 6h11M9 12h11M9 18h11M4 6l1 1 2-2M4 12l1 1 2-2M4 18l1 1 2-2",
notes: "M6 3h9l5 5v13H6zM14 3v6h6M9 13h7M9 17h5",
files: "M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z",
contacts: "M5 3h13a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H5zM12 11a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM8 17a4 4 0 0 1 8 0M3 7h2M3 12h2M3 17h2",
products: "M21 8 12 3 3 8v8l9 5 9-5zM3 8l9 5 9-5M12 13v8",
store: "M4 9h16l-1 11a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1zM8 9V7a4 4 0 0 1 8 0v2",
orders: "M6 3h12l2 4v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7zM4 7h16M9 11a3 3 0 0 0 6 0",
customers: "M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM2 21a7 7 0 0 1 14 0M17 11a3 3 0 1 0 0-6M22 21a6 6 0 0 0-4-5.7",
invoices: "M6 2h9l5 5v15H6zM14 2v6h6M9 13h6M9 17h6M9 9h2",
users: "M16 21v-1a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v1M9 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM19 8v6M22 11h-6",
profile: "M12 12a4.5 4.5 0 1 0 0-9 4.5 4.5 0 0 0 0 9zM4 21a8 8 0 0 1 16 0",
roles: "M12 3 4 6v6c0 4.5 3.4 8.3 8 9 4.6-.7 8-4.5 8-9V6zM9 12l2 2 4-4",
team: "M17 20a5 5 0 0 0-10 0M12 12a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM20 18a4 4 0 0 0-3-3.8M4 18a4 4 0 0 1 3-3.8",
general: "M4 7h10M18 7h2M4 17h4M12 17h8M16 5v4M10 15v4",
security: "M6 10V8a6 6 0 0 1 12 0v2M5 10h14v11H5zM12 14v3",
bell: "M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9M13.7 21a2 2 0 0 1-3.4 0",
card: "M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zM2 10h20M6 15h4",
api: "M8 6 2 12l6 6M16 6l6 6-6 6M14 4l-4 16",
plug: "M9 2v6M15 2v6M6 8h12v4a6 6 0 0 1-12 0zM12 18v4",
help: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.6.3-1 .9-1 1.6V14M12 17h.01",
faq: "M4 5h16v11H8l-4 4zM9 9h6M9 12h4",
search: "M11 17.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13M20 20l-4-4",
activity: "M12 8v4l3 2M3 12a9 9 0 1 0 3-6.7L3 8M3 3v5h5",
lock: "M7 11V8a5 5 0 0 1 10 0v3M5 11h14v10H5z",
signOut: "M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3M10 17l5-5-5-5M15 12H4",
};
const item = (label: string, route: string, d: string, extra?: Partial<SidebarGroup["items"][number]>) => ({
label,
href: href(route),
icon: <AppIcon d={d} />,
...extra,
});
const unreadMail = defaultMessages.filter((m) => m.folder === "inbox" && m.unread).length;
/** The one navigation every app page shows. */
export const appNav: SidebarGroup[] = [
{
label: "Dashboards",
items: [
item("Analytics", "/app", icon.analytics),
item("eCommerce", "/app/ecommerce", icon.cart),
item("CRM", "/app/crm", icon.crm),
item("Projects", "/app/projects", icon.projects),
item("Billing", "/app/billing", icon.billing),
item("Status", "/app/status", icon.status, { badge: 1 }),
],
},
{
label: "Apps",
items: [
item("Mail", "/app/mail", icon.mail, unreadMail ? { badge: unreadMail } : undefined),
item("Chat", "/app/chat", icon.chat),
item("Calendar", "/app/calendar", icon.calendar),
item("Kanban", "/app/kanban", icon.kanban),
item("Tasks", "/app/tasks", icon.tasks),
item("Notes", "/app/notes", icon.notes),
item("Files", "/app/files", icon.files),
item("Contacts", "/app/contacts", icon.contacts),
],
},
{
label: "E-commerce",
items: [
item("Products", "/app/products", icon.products, {
overviewLabel: "All products",
children: [
{ label: "Add product", href: href("/app/products/new") },
{ label: "Edit product", href: href("/app/products/edit") },
],
}),
item("Orders", "/app/orders", icon.orders),
// Opens the public storefront, like "View store" in a commerce admin.
item("Store", "/store", icon.store, {
overviewLabel: "Storefront",
children: [
{ label: "Cart", href: href("/store/cart") },
{ label: "Checkout", href: href("/store/checkout") },
],
}),
item("Customers", "/app/customers", icon.customers),
item("Invoices", "/app/invoices", icon.invoices, {
overviewLabel: "All invoices",
children: [
{ label: "Invoice preview", href: href("/app/invoices/preview") },
{ label: "New invoice", href: href("/app/invoices/new") },
],
}),
],
},
{
label: "Users",
items: [
item("Users", "/app/users", icon.users),
item("Profile", "/app/profile", icon.profile),
item("Roles", "/app/roles", icon.roles),
item("Team", "/app/team", icon.team),
],
},
{
label: "Settings",
items: [
item("General", "/app/settings", icon.general),
item("Security", "/app/settings/security", icon.security),
item("Notifications", "/app/settings/notifications", icon.bell),
item("Plan and billing", "/app/settings/billing", icon.card),
item("API & webhooks", "/app/settings/api", icon.api),
item("Connections", "/app/settings/connections", icon.plug),
],
},
{
label: "Support",
items: [
item("Help center", "/app/help", icon.help),
item("FAQ", "/app/help/faq", icon.faq),
item("Search", "/app/search", icon.search),
item("Activity", "/app/activity", icon.activity),
],
},
];
const settingsLinks: SettingsLink[] = [
{ id: "general", label: "General", href: href("/app/settings") },
{ id: "security", label: "Security", href: href("/app/settings/security") },
{ id: "notifications", label: "Notifications", href: href("/app/settings/notifications") },
{ id: "billing", label: "Billing", href: href("/app/settings/billing") },
{ id: "api", label: "API & webhooks", href: href("/app/settings/api") },
{ id: "connections", label: "Connections", href: href("/app/settings/connections") },
];
const me = { name: "Ada Kaya", role: "Growth lead", email: "ada@lumen.example" };
const userMenu: AppShellMenuItem[] = [
{ label: "Your profile", href: href("/app/profile"), icon: <AppIcon d={icon.profile} /> },
{ label: "Settings", href: href("/app/settings"), icon: <AppIcon d={icon.general} /> },
{ label: "Plan and billing", href: href("/app/settings/billing"), icon: <AppIcon d={icon.card} /> },
{ label: "Help center", href: href("/app/help"), icon: <AppIcon d={icon.help} /> },
{ label: "Lock screen", href: href("/lock"), icon: <AppIcon d={icon.lock} />, separated: true },
{ label: "Sign out", href: href("/signin"), icon: <AppIcon d={icon.signOut} />, variant: "danger" },
];
const usageCard = (
<div className="rounded-field bg-fg/[0.04] p-4 text-sm">
<div className="font-medium">Events this month</div>
<Progress value={68.3} aria-label="Events used this month" className="mt-2 h-1.5" />
<div className="mt-2 text-xs text-muted">683,412 of 1M on the Growth plan</div>
</div>
);
const brand = { title: "Lumen", subtitle: "Product analytics", href: href("/app") };
const firstOrder = sampleOrders[0]!;
const overdueInvoice = sampleInvoices.find((i) => i.status === "overdue") ?? sampleInvoices[0]!;
// Every bell in the app links to a real page; the read state is shared, so a
// notification read in one shell stays read on the next page.
const readNotices = new Set<string>();
const notices = <T extends { id?: string; read?: boolean }>(list: T[]) => list.map((n) => (n.id && readNotices.has(n.id) ? { ...n, read: true } : n));
// The analytics dashboard is the app's home, so its bell shows the shell's notifications.
const asDashboardNotices = (list: NotificationItem[]): DashboardNotice[] => list.map(({ detail, time, ...n }) => ({ ...n, body: detail ?? "", time: time ?? "" }));
const rememberRead = (list: NotificationItem[]) => list.forEach((n) => n.read && readNotices.add(n.id));
const notificationsHref = href("/app/activity");
const shellNotices: NotificationItem[] = [
{
id: "order",
title: `Order ${firstOrder.number} is ready to fulfil`,
detail: `${firstOrder.customer} · ${firstOrder.items} ${firstOrder.items === 1 ? "item" : "items"}`,
time: "6 minutes ago",
href: href(`/app/orders/${firstOrder.id}`),
},
{ id: "spike", title: "Spike from news.ycombinator.com", detail: "1,283 visitors in the last hour", time: "12 minutes ago", tone: "info", href: href("/app") },
{
id: "invoice",
title: `Invoice ${overdueInvoice.number} is overdue`,
detail: `${overdueInvoice.client} · due ${overdueInvoice.due}`,
time: "2 hours ago",
tone: "warning",
href: href(`/app/invoices/${overdueInvoice.id}`),
},
{ id: "member", title: "Hana Sato accepted your invite", detail: "Joined Lumen as Editor", time: "Yesterday", tone: "success", href: href("/app/team"), read: true },
];
// Shared by the three dashboards, which render their own shell.
const dashboardChrome: DashboardChrome = {
logo: <LumenGlyph />,
brand,
profile: { ...me, menu: userMenu },
nav: appNav,
sidebarFooter: usageCard,
exitHref: href("/signin"),
notificationsHref,
onNotificationsChange: rememberRead,
};
/* ------------------------------------------------- dashboard content */
// The finance dashboard, reframed as Lumen's own revenue and billing screen.
const revenue: Partial<FinanceContent> = {
balanceLabel: "Monthly recurring revenue",
seriesName: "MRR",
actions: [
{ label: "Download invoices", icon: "list", toast: { title: "Exported invoices-oct-2026.zip", description: "412 invoices from Oct 1 to Oct 25, as PDF." } },
{ label: "Send payment reminders", icon: "send", toast: { title: "Reminders sent to 3 customers", description: "Each has an invoice more than 7 days overdue." } },
],
goal: {
title: "ARR goal",
target: 600_000,
start: 498_960,
note: (
<>
Year-end target. At October's growth you reach it in <span className="text-fg">4 months</span>.
</>
),
progressLabel: "annual run rate",
},
tiles: [{ label: "New MRR" }, { label: "Churned MRR", upIsGood: false }, { label: "Net new MRR" }],
categoryLabels: ["Business", "Growth", "Business yearly", "Add-ons"],
breakdownTitle: "Revenue by plan",
breakdownCenter: "Revenue",
budgetsTitle: "Costs against budget",
budgets: [
{ label: "Hosting", spent: 612, limit: 700 },
{ label: "Ads", spent: 284, limit: 250 },
{ label: "Support tools", spent: 198, limit: 320 },
{ label: "Email delivery", spent: 86, limit: 90 },
],
ledgerTitle: "Recent payments",
ledgerColumns: ["Customer", "Plan"],
ledgerSearch: "Search payments",
ledgerEmptyHint: "Search by customer or plan, such as Growth.",
viewAllLabel: "View all payments",
transactions: [
{ merchant: "Polaris", category: "Business yearly", date: "Oct 25", amount: 948 },
{ merchant: "Kitebase", category: "Growth", date: "Oct 25", amount: 24 },
{ merchant: "Orbitly", category: "Business", date: "Oct 24", amount: 96 },
{ merchant: "Hetzner Online", category: "Hosting", date: "Oct 24", amount: -612.4 },
{ merchant: "Fernwood Studio", category: "Refund · Growth", date: "Oct 23", amount: -24 },
{ merchant: "Marlow & Finch", category: "Growth yearly", date: "Oct 22", amount: 228 },
],
notifications: [
{ id: "pay-failed", title: "Payment failed: Fernwood Studio", body: "Card declined on a $24.00 Growth renewal. We retry on Oct 28.", time: "40 min ago", tone: "danger", href: href("/app/invoices") },
{ id: "polaris", title: "Polaris moved to Business yearly", body: "+$948.00, starting today.", time: "3 h ago", tone: "success", href: href("/app/customers") },
{ id: "ads", title: "Ads are over budget", body: "$284 of $250 with six days left.", time: "Yesterday", tone: "warning", href: href("/app/settings/billing") },
],
};
// The devops dashboard, reframed as the status of Lumen's own platform.
const services: Service[] = [
{ name: "ingest", region: "eu-central", status: "operational", uptime: "99.99%" },
{ name: "dashboard", region: "eu-central", status: "operational", uptime: "99.98%" },
{ name: "script-cdn", region: "global", status: "operational", uptime: "99.99%" },
{ name: "exports", region: "eu-north", status: "degraded", uptime: "99.71%" },
{ name: "ga-importer", region: "eu-north", status: "operational", uptime: "99.95%" },
];
const deployments: Deployment[] = [
{ sha: "a41f9c2", message: "Stream CSV exports instead of buffering", service: "exports", author: "MK", status: "building", duration: "1m 12s", when: "just now" },
{ sha: "7be03d1", message: "Funnels 2.0: median time between steps", service: "dashboard", author: "AK", status: "success", duration: "2m 48s", when: "18 min ago" },
{ sha: "c90e4a7", message: "Drop bot traffic before counting", service: "ingest", author: "JL", status: "success", duration: "3m 05s", when: "1 h ago" },
{ sha: "1d2c8f0", message: "Import GA4 custom dimensions", service: "ga-importer", author: "SR", status: "failed", duration: "0m 54s", when: "2 h ago" },
{ sha: "e5a7b33", message: "Serve l.js with Brotli at the edge", service: "script-cdn", author: "AK", status: "success", duration: "2m 11s", when: "4 h ago" },
];
const statusNotifications: DashboardNotice[] = [
{ id: "exports", title: "exports is degraded", body: "Large CSV exports take 3× longer than usual. A fix is deploying.", time: "12 min ago", tone: "warning", href: href("/app/activity") },
{ id: "deploy", title: "Deploy failed: 1d2c8f0", body: "GA4 import tests timed out. Nothing reached production.", time: "2 h ago", tone: "danger", href: href("/app/activity") },
{ id: "cdn", title: "script-cdn p95 at 41 ms", body: "Down from 58 ms after enabling Brotli.", time: "4 h ago", tone: "success", read: true },
];
const myProfile = {
...sampleProfile,
name: me.name,
title: me.role,
team: "Growth",
location: "Istanbul, Türkiye",
localTime: "16:32",
email: me.email,
joined: "Joined January 2024",
bio: "Runs growth at Lumen: onboarding, pricing experiments and the funnels everyone else reads. Before Lumen she led lifecycle marketing at Kitebase.",
};
/** The app menu with the storefront cart's item count on Store. */
function navWithCart(count: number): SidebarGroup[] {
return appNav.map((group) => ({
...group,
items: group.items.map((i) => (i.href === href(storeHome) && count > 0 ? { ...i, badge: count } : i)),
}));
}
/* -------------------------------------------------------------- pages */
export interface AppPage {
/** Hash path; `:id` matches any one segment. */
path: string;
/** Top-bar title and the label in the page index. */
title: string;
/** Group in the page index. */
section: string;
/** A concrete path for `:id` routes. */
sample?: string;
/** The nav link to mark; defaults to `path`. */
nav?: string;
/** Full-page dashboards render their own shell. */
dashboard?: boolean;
/** `route` is the matched path; `nav` the app menu with live counts. */
render: (ctx: { route: string; nav: SidebarGroup[] }) => ReactNode;
}
const settings = (path: string, title: string, render: () => ReactNode): AppPage => ({ path, title, section: "Settings", render });
export const appPages: AppPage[] = [
// Dashboards: three full dashboards and three screens in the shared shell.
{
path: "/app",
title: "Analytics",
section: "Dashboards",
dashboard: true,
render: ({ nav }) => <DashboardAnalytics {...dashboardChrome} notifications={notices(asDashboardNotices(shellNotices))} nav={nav} currentHref={href("/app")} title="Analytics" subtitle="lumen.app · all traffic" />,
},
{ path: "/app/ecommerce", title: "eCommerce", section: "Dashboards", render: () => <EcommerceDashboardScreen /> },
{ path: "/app/crm", title: "CRM", section: "Dashboards", render: () => <CrmDashboardScreen /> },
{ path: "/app/projects", title: "Projects", section: "Dashboards", render: () => <ProjectsDashboardScreen projectHref={href("/app/kanban")} /> },
{
path: "/app/billing",
title: "Billing",
section: "Dashboards",
dashboard: true,
render: ({ nav }) => <DashboardFinance {...dashboardChrome} nav={nav} currentHref={href("/app/billing")} title="Billing" subtitle="Lumen Inc. · revenue in USD" content={{ ...revenue, notifications: notices(revenue.notifications ?? []) }} />,
},
{
path: "/app/status",
title: "Status",
section: "Dashboards",
dashboard: true,
render: ({ nav }) => (
<DashboardDevops
{...dashboardChrome}
nav={nav}
currentHref={href("/app/status")}
title="Status"
subtitle={`lumen-platform · ${services.length} services`}
services={services}
deployments={deployments}
notifications={notices(statusNotifications)}
/>
),
},
// Apps.
{ path: "/app/mail", title: "Mail", section: "Apps", render: () => <MailScreen me={{ name: me.name, email: me.email }} /> },
{ path: "/app/chat", title: "Chat", section: "Apps", render: () => <ChatScreen /> },
{ path: "/app/calendar", title: "Calendar", section: "Apps", render: () => <CalendarScreen /> },
{ path: "/app/kanban", title: "Kanban", section: "Apps", render: () => <KanbanScreen /> },
// Project names on the projects dashboard open that project's board.
{ path: "/app/kanban/:id", title: "Project board", section: "Apps", sample: "/app/kanban/funnels-2", nav: "/app/kanban", render: () => <KanbanScreen /> },
{ path: "/app/tasks", title: "Tasks", section: "Apps", render: () => <TasksScreen /> },
{ path: "/app/notes", title: "Notes", section: "Apps", render: () => <NotesScreen /> },
{ path: "/app/files", title: "Files", section: "Apps", render: () => <FileManagerScreen me={me.name} /> },
{ path: "/app/contacts", title: "Contacts", section: "Apps", render: () => <ContactsScreen /> },
// E-commerce.
{ path: "/app/products", title: "Products", section: "E-commerce", render: () => <ProductsScreen newHref={href("/app/products/new")} detailHref={href("/app/products")} /> },
{
path: "/app/products/new",
title: "Add product",
section: "E-commerce",
render: () => <ProductFormScreen initialValues={emptyProductForm} cancelHref={href("/app/products")} />,
},
{ path: "/app/products/edit", title: "Edit product", section: "E-commerce", render: () => <ProductFormScreen cancelHref={href(`/app/products/${sampleProducts[0]!.id}`)} /> },
{
path: "/app/products/:id",
title: sampleProductDetail.name,
section: "E-commerce",
sample: `/app/products/${sampleProducts[0]!.id}`,
nav: "/app/products",
render: () => <ProductDetailScreen editHref={href("/app/products/edit")} />,
},
{ path: "/app/orders", title: "Orders", section: "E-commerce", render: () => <OrdersScreen detailHref={href("/app/orders")} /> },
{
path: "/app/orders/:id",
title: `Order ${sampleOrderDetail.number}`,
section: "E-commerce",
sample: `/app/orders/${sampleOrders[0]!.id}`,
nav: "/app/orders",
render: () => <OrderDetailScreen />,
},
{ path: "/app/customers", title: "Customers", section: "E-commerce", render: () => <CustomersScreen detailHref={href("/app/customers")} /> },
{
path: "/app/customers/:id",
title: "Customer",
section: "E-commerce",
sample: `/app/customers/${sampleCustomers[0]!.id}`,
nav: "/app/customers",
render: () => <CustomerDetailScreen orderHref={href("/app/orders")} />,
},
// Invoices.
{
path: "/app/invoices",
title: "Invoices",
section: "Invoices",
render: () => <InvoicesScreen previewHref={href("/app/invoices")} newHref={href("/app/invoices/new")} />,
},
{ path: "/app/invoices/new", title: "New invoice", section: "Invoices", render: () => <InvoiceEditorScreen previewHref={href("/app/invoices/preview")} /> },
{
path: "/app/invoices/edit",
title: "Edit invoice",
section: "Invoices",
nav: "/app/invoices",
render: () => <InvoiceEditorScreen title="Edit invoice" previewHref={href("/app/invoices/preview")} />,
},
{ path: "/app/invoices/preview", title: "Invoice preview", section: "Invoices", render: () => <InvoicePreviewScreen editHref={href("/app/invoices/edit")} /> },
{
path: "/app/invoices/:id",
title: "Invoice",
section: "Invoices",
sample: `/app/invoices/${sampleInvoices[0]!.id}`,
nav: "/app/invoices",
render: () => <InvoicePreviewScreen editHref={href("/app/invoices/edit")} />,
},
// Users.
{ path: "/app/users", title: "Users", section: "Users", render: () => <UsersScreen profileHref={href("/app/users")} /> },
{
path: "/app/users/:id",
title: sampleProfile.name,
section: "Users",
sample: `/app/users/${sampleUsers[1]!.id}`,
nav: "/app/users",
render: () => <UserProfileScreen />,
},
{ path: "/app/profile", title: "Your profile", section: "Users", nav: "/app/profile", render: () => <UserProfileScreen person={myProfile} /> },
{ path: "/app/roles", title: "Roles", section: "Users", render: () => <RolesScreen /> },
{ path: "/app/team", title: "Team", section: "Users", render: () => <TeamScreen billingHref={href("/app/settings/billing")} /> },
// Settings.
settings("/app/settings", "General settings", () => <SettingsGeneralScreen links={settingsLinks} onDeleteAccount={() => go("/")} />),
settings("/app/settings/security", "Security", () => <SettingsSecurityScreen links={settingsLinks} accountLabel={me.email} />),
settings("/app/settings/notifications", "Notifications", () => <SettingsNotificationsScreen links={settingsLinks} />),
settings("/app/settings/billing", "Plan and billing", () => <SettingsBillingScreen links={settingsLinks} />),
settings("/app/settings/api", "API & webhooks", () => <SettingsApiScreen links={settingsLinks} />),
settings("/app/settings/connections", "Connections", () => <SettingsConnectionsScreen links={settingsLinks} />),
// Support.
{
path: "/app/help",
title: "Help center",
section: "Support",
render: () => (
<HelpCenterScreen
articleHref={(id) => href(`/app/help/article/${id}`)}
categoryHref={() => href("/app/help/faq")}
contactHref={href("/contact")}
/>
),
},
{
path: "/app/help/article",
title: "Help article",
section: "Support",
nav: "/app/help",
render: () => <HelpArticleScreen helpHref={href("/app/help")} />,
},
{
path: "/app/help/article/:id",
title: "Help article",
section: "Support",
sample: "/app/help/article/sso-okta",
nav: "/app/help",
render: () => <HelpArticleScreen helpHref={href("/app/help")} />,
},
{ path: "/app/help/faq", title: "FAQ", section: "Support", render: () => <FaqScreen contactHref={href("/contact")} /> },
{ path: "/app/search", title: "Search", section: "Support", render: () => <SearchResultsScreen /> },
{ path: "/app/activity", title: "Activity", section: "Support", render: () => <TimelineScreen /> },
];
/** One app page in the shared shell, or a full dashboard. */
export function LumenAppPage({ page, route = page.path }: { page: AppPage; route?: string }) {
const { lines } = useLumenCart();
const nav = navWithCart(storeItemCount(lines));
const content = page.render({ route, nav });
if (page.dashboard) return <>{content}</>;
return (
<AppShell
logo={<LumenGlyph />}
brand={brand}
nav={nav}
currentHref={href(page.nav ?? page.path)}
title={page.title}
user={{ ...me, settingsHref: href("/app/settings"), menu: userMenu }}
sidebarFooter={usageCard}
notifications={notices(shellNotices)}
onNotificationsChange={rememberRead}
notificationsHref={notificationsHref}
>
{content}
</AppShell>
);
}"use client";
import { useSyncExternalStore, type ReactNode } from "react";
import { Faq, type FaqItem } from "@/registry/kit/blocks/faq";
import { StoreFooter, type StoreFooterColumn } from "@/registry/kit/blocks/store-footer";
import { StoreHeader, type StoreMenuItem } from "@/registry/kit/blocks/store-header";
import {
addStoreLine,
formatUsd,
sampleStoreCollections,
sampleStoreProducts,
storeItemCount,
StoreProductArt,
StoreScreen,
type StoreCartLine,
} from "@/registry/kit/screens/screen-store";
import { sampleStoreCart, StoreCartScreen, StoreMiniCart } from "@/registry/kit/screens/screen-store-cart";
import { StoreCheckoutScreen } from "@/registry/kit/screens/screen-store-checkout";
import { sampleStoreOrder, StoreConfirmationScreen, type StoreOrder } from "@/registry/kit/screens/screen-store-confirmation";
import { StoreProductScreen } from "@/registry/kit/screens/screen-store-product";
import { AuroraBackdrop } from "@/registry/kit/ui/aurora-backdrop";
import { BrandMark } from "@/registry/kit/ui/brand-mark";
import type { NotificationItem } from "@/registry/kit/ui/notification-bell";
import { buttonVariants } from "@/registry/kit/ui/button";
import { EmptyState } from "@/registry/kit/ui/states";
/*
* Lumen's public storefront, "Northwind Supply": what shoppers see, with its
* own header, cart drawer and footer and no admin chrome. The admin's Store
* menu item opens it, like "View store" in a commerce admin.
*/
const href = (route: string) => `#${route}`;
const go = (route: string) => {
window.location.hash = route;
};
/* ------------------------------------------------------------------ cart */
// One cart for the whole template: the storefront writes it, the cart drawer,
// cart and checkout read it, and the admin's Store menu item shows its count.
type CartState = { lines: StoreCartLine[]; promo: string | null; order: StoreOrder | null; drawer: boolean };
const initialCart: CartState = { lines: sampleStoreCart, promo: null, order: null, drawer: false };
let cart = initialCart;
const cartListeners = new Set<() => void>();
const cartStore = {
subscribe(listener: () => void) {
cartListeners.add(listener);
return () => {
cartListeners.delete(listener);
};
},
get: () => cart,
set(next: Partial<CartState>) {
cart = { ...cart, ...next };
cartListeners.forEach((l) => l());
},
};
export function useLumenCart() {
return useSyncExternalStore(cartStore.subscribe, cartStore.get, () => initialCart);
}
/* ---------------------------------------------------------------- chrome */
export const storeHome = "/store";
const storeHref = href(storeHome);
const cartHref = href("/store/cart");
const checkoutHref = href("/store/checkout");
const productHref = (slug: string) => href(`/store/p/${slug}`);
const collectionHref = (slug: string) => href(`/store/c/${slug}`);
const brand = {
name: "Northwind Supply",
href: storeHref,
logo: (
<BrandMark>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M12 3v18M12 3l-5 6M12 3l5 6M6 15h12" />
</svg>
</BrandMark>
),
};
// Order updates in the header bell. Read state lives here so it survives page changes.
let orderUpdates: NotificationItem[] = [
{
id: "shipped",
title: `Order ${sampleStoreOrder.number} has shipped`,
detail: "DHL Express · arrives Thursday, Oct 29",
time: "35 min ago",
tone: "success",
href: href("/store/confirmation"),
},
{
id: "restock",
title: "Trail runner, slate is back in stock",
detail: "Your size, EU 42, is available again",
time: "Yesterday",
tone: "info",
href: productHref("trail-runner-slate"),
},
{ id: "repair", title: "Your repair is on its way back", detail: "Waxed field jacket · new zip fitted", time: "Oct 22", read: true, href: href("/store/help") },
];
const byRank = (a: { rank: number }, b: { rank: number }) => a.rank - b.rank;
const menu: StoreMenuItem[] = sampleStoreCollections.map((c) => {
const picks = sampleStoreProducts.filter(c.match).sort(byRank);
const feature = picks.find((p) => p.stock > 0) ?? picks[0];
return {
label: c.label,
href: collectionHref(c.slug),
description: c.description,
highlight: c.highlight,
links: c.highlight ? undefined : picks.slice(0, 5).map((p) => ({ label: p.name, href: productHref(p.slug), meta: formatUsd(p.price) })),
feature: feature && {
title: feature.name,
description: feature.summary,
href: productHref(feature.slug),
art: <StoreProductArt kind={feature.kind} tone={feature.tone} className="aspect-[4/3]" />,
},
};
});
const footerColumns: StoreFooterColumn[] = [
{ title: "Shop", links: sampleStoreCollections.map((c) => ({ label: c.label, href: collectionHref(c.slug) })) },
{
title: "Help",
links: [
{ label: "Help and FAQ", href: href("/store/help") },
{ label: "Shipping and returns", href: href("/store/help") },
{ label: "Your cart", href: cartHref },
{ label: "Email us", href: "mailto:help@northwind.example" },
],
},
{
title: "Account",
links: [
{ label: "Sign in", href: href("/signin") },
{ label: "Create an account", href: href("/signup") },
{ label: "Store admin", href: href("/app/products") },
],
},
];
const addToCart = (line: StoreCartLine) => cartStore.set({ lines: addStoreLine(cartStore.get().lines, line) });
function StoreLayout({ current, checkout = false, children }: { current?: string; checkout?: boolean; children: ReactNode }) {
const { lines, promo, drawer } = useLumenCart();
return (
<div className="relative isolate flex min-h-dvh flex-col bg-bg text-fg">
{/* The same page backdrop as the app and marketing site; the theme's --aurora-strength sets it. */}
<AuroraBackdrop grid={false} className="fixed print:hidden" />
<StoreHeader
brand={brand}
menu={menu}
currentHref={current}
variant={checkout ? "checkout" : "full"}
backLink={{ label: "Back to cart", href: cartHref }}
notice={{ message: "You're previewing Northwind Supply as a shopper.", link: { label: "Back to admin", href: href("/app/products") } }}
onSearch={(q) => go(`/store/search?q=${encodeURIComponent(q)}`)}
account={{ label: "Sign in", href: href("/signin") }}
notifications={{
items: orderUpdates,
onItemsChange: (next) => {
orderUpdates = next;
},
}}
cartCount={storeItemCount(lines)}
cartOpen={drawer}
onCartOpenChange={(open) => cartStore.set({ drawer: open })}
cart={
<StoreMiniCart
lines={lines}
promoCode={promo}
onLinesChange={(next) => cartStore.set({ lines: next })}
cartHref={cartHref}
checkoutHref={checkoutHref}
storeHref={storeHref}
productHref={productHref}
onNavigate={() => cartStore.set({ drawer: false })}
/>
}
/>
<main id="main" tabIndex={-1} className="mx-auto w-full max-w-7xl flex-1 px-4 py-8 outline-none md:px-8 md:py-12">
{children}
</main>
{!checkout && (
<StoreFooter
brand={brand}
tagline="Clothing and goods made to be worn for a decade, then mended. Sewn in Portugal, shipped from Lisbon."
columns={footerColumns}
legal={[
{ label: "Privacy", href: href("/privacy") },
{ label: "Terms", href: href("/terms") },
]}
/>
)}
</div>
);
}
/* ---------------------------------------------------------------- routes */
const lastSegment = (route: string) => route.split("/").pop() ?? "";
const subscribeHash = (listener: () => void) => {
window.addEventListener("hashchange", listener);
return () => window.removeEventListener("hashchange", listener);
};
function SearchPage() {
const hash = useSyncExternalStore(subscribeHash, () => window.location.hash, () => "");
const q = new URLSearchParams(hash.split("?")[1] ?? "").get("q") ?? "";
return <StoreScreen key={q} title={q ? `Results for "${q}"` : "Search"} initialQuery={q} onAddToCart={addToCart} productHref={productHref} />;
}
function NotFound({ what }: { what: string }) {
return (
<div className="surface rounded-card">
<h1 className="sr-only">{what} not found</h1>
<EmptyState
title={`We couldn't find that ${what.toLowerCase()}`}
description="It may have been renamed or retired. Everything we sell right now is in the store."
action={
<a href={storeHref} className={buttonVariants({ variant: "accent" })}>
Browse the store
</a>
}
/>
</div>
);
}
function CollectionPage({ route }: { route: string }) {
const collection = sampleStoreCollections.find((c) => c.slug === lastSegment(route));
if (!collection) return <NotFound what="Collection" />;
return <StoreScreen key={collection.slug} collection={collection} onAddToCart={addToCart} productHref={productHref} />;
}
function ProductPage({ route }: { route: string }) {
const product = sampleStoreProducts.find((p) => p.slug === lastSegment(route));
if (!product) return <NotFound what="Product" />;
return (
<StoreProductScreen
key={product.slug}
product={product}
onAddToCart={(line) => {
addToCart(line);
cartStore.set({ drawer: true });
}}
storeHref={storeHref}
cartHref={cartHref}
productHref={productHref}
/>
);
}
function CartPage() {
const { lines, promo } = useLumenCart();
return (
<StoreCartScreen
lines={lines}
onLinesChange={(next) => cartStore.set({ lines: next })}
promoCode={promo}
onPromoCodeChange={(code) => cartStore.set({ promo: code })}
storeHref={storeHref}
checkoutHref={checkoutHref}
productHref={productHref}
/>
);
}
function CheckoutPage() {
const { lines, promo } = useLumenCart();
return (
<StoreCheckoutScreen
lines={lines}
promoCode={promo}
cartHref={cartHref}
storeHref={storeHref}
onPlaceOrder={(order) => {
cartStore.set({ order, lines: [], promo: null });
go("/store/confirmation");
}}
/>
);
}
function ConfirmationPage() {
const { order } = useLumenCart();
return <StoreConfirmationScreen order={order ?? undefined} storeHref={storeHref} productHref={productHref} />;
}
const helpItems: FaqItem[] = [
{ question: "How long does delivery take?", answer: "Standard shipping takes 3–5 business days and is free on orders over $150. Express arrives in 1–2 business days for $18, and next-day delivery costs $29 if you order by 2 pm." },
{ question: "How do I return something?", answer: "Returns are free within 30 days of delivery. Use the return link in your shipping email to print a label, and we refund your card within 3 business days of the parcel reaching Lisbon." },
{ question: "Can I exchange for another size?", answer: "Yes. Start a return and choose Exchange; we ship the new size as soon as the courier scans your parcel, so you're not without it for long." },
{ question: "What does repairs for life cover?", answer: "Split seams, broken zips, worn elbows and lost buttons on anything we make. Send it back with the repair form and we mend it free; you only pay postage one way." },
{ question: "Which payment methods do you take?", answer: "Visa, Mastercard, American Express, Apple Pay and PayPal. Prices are in US dollars and include duties, so nothing extra is due on delivery." },
{ question: "How do I find my size?", answer: "Each product page lists the fit. Our shirts run relaxed, so most people size down for a closer cut. Denim shrinks about 3% on the first wash and the size labels allow for it." },
{ question: "Can I change or cancel my order?", answer: "Reply to your confirmation email within an hour of ordering and we'll change or cancel it. After that it's usually packed, and a free return is quicker." },
];
export interface StoreRoute {
path: string;
title: string;
sample?: string;
render: (route: string) => ReactNode;
}
/** Every storefront route, each in its own store chrome. */
export const storeRoutes: StoreRoute[] = [
{
path: "/store",
title: "Storefront",
render: () => (
<StoreLayout current={storeHref}>
<StoreScreen onAddToCart={addToCart} productHref={productHref} collections={sampleStoreCollections} collectionHref={collectionHref} />
</StoreLayout>
),
},
{
path: "/store/c/:slug",
title: "Collection",
sample: "/store/c/men",
render: (route) => (
<StoreLayout current={href(route)}>
<CollectionPage route={route} />
</StoreLayout>
),
},
{
path: "/store/p/:slug",
title: "Product",
sample: `/store/p/${sampleStoreProducts[0]!.slug}`,
render: (route) => (
<StoreLayout>
<ProductPage route={route} />
</StoreLayout>
),
},
{
path: "/store/search",
title: "Search results",
sample: "/store/search?q=merino",
render: () => (
<StoreLayout>
<SearchPage />
</StoreLayout>
),
},
{
path: "/store/cart",
title: "Cart",
render: () => (
<StoreLayout>
<CartPage />
</StoreLayout>
),
},
{
path: "/store/checkout",
title: "Checkout",
render: () => (
<StoreLayout checkout>
<CheckoutPage />
</StoreLayout>
),
},
{
path: "/store/confirmation",
title: "Order confirmation",
render: () => (
<StoreLayout>
<ConfirmationPage />
</StoreLayout>
),
},
{
path: "/store/help",
title: "Help and FAQ",
render: () => (
<StoreLayout>
<Faq
headingLevel="h1"
title="Help and FAQ"
description="Shipping, returns, sizing and repairs. A person on our Lisbon team replies to email within one working day."
items={helpItems}
contact={{ label: "Email help@northwind.example", href: "mailto:help@northwind.example" }}
/>
</StoreLayout>
),
},
];import type { AboutMilestone, AboutStat, AboutValue } from "@/registry/kit/blocks/about";
import type { Post } from "@/registry/kit/blocks/blog-cards";
import type { PostAuthor, PostSection, RelatedPost } from "@/registry/kit/blocks/blog-post";
import type { Role } from "@/registry/kit/blocks/careers";
import type { RoleDetail } from "@/registry/kit/blocks/careers-role";
import type { CaseChapter, CaseMetric } from "@/registry/kit/blocks/case-study";
import type { Channel, Office } from "@/registry/kit/blocks/contact-page";
import type { CaseStudyCard } from "@/registry/kit/blocks/customers";
/*
* Copy for Lumen's company pages (about, careers, blog, customers, contact).
* Kept apart from the page code so the router file stays readable. Links are
* hash routes, the same as everywhere else in the template.
*/
/* ---------------------------------------------------------------- about */
export const about = {
title: "Analytics that respect the people being counted",
lead: "Lumen is a small, independent company in Berlin and Istanbul. We build product analytics that answer real questions without cookies, profiles or a consent banner.",
stats: [
{ value: 38_214, label: "sites measured" },
{ value: 2.14, decimals: 2, suffix: "B", label: "events a month" },
{ value: 99.98, decimals: 2, suffix: "%", label: "uptime over 12 months" },
{ value: 27, label: "people, fully remote" },
] satisfies AboutStat[],
story: [
"Lumen started in 2021 as a weekend fix for our own consulting clients. They wanted to know which pages sold, and they were tired of a cookie banner that half their visitors refused.",
"Today the same 1.4 kB script counts visits on 38,214 sites, from personal blogs to listed retailers. We are funded by customers, not investors, and every person here takes a turn answering support.",
],
milestones: [
{ year: "2021", text: "First version counts visits for four client sites." },
{ year: "2022", text: "Public launch. 1,206 sites in the first month." },
{ year: "2024", text: "Funnels and goals ship; servers move fully to the EU." },
{ year: "2026", text: "Funnels 2.0, retention cohorts and SOC 2 Type II." },
] satisfies AboutMilestone[],
values: [
{ title: "Count less, explain more", body: "We collect the smallest set of fields that answers the question, and spend our effort on making the answer clear." },
{ title: "No dark patterns", body: "No auto-renew surprises, no hidden limits. Pricing changes are announced 60 days ahead on the changelog." },
{ title: "Everyone does support", body: "Engineers and designers each take a support week every two months. It is the fastest way to learn what to build." },
{ title: "Calm pace", body: "Six-week cycles, two-week cool-downs and no launch nights. Analytics should be boring to run." },
] satisfies AboutValue[],
team: [
{ name: "Ada Kaya", role: "Co-founder, Growth" },
{ name: "Mateo Ruiz", role: "Co-founder, Engineering" },
{ name: "Jun Lee", role: "Head of Infrastructure" },
{ name: "Sara Okafor", role: "Developer Experience" },
{ name: "Liam Novak", role: "Security Lead" },
{ name: "Priya Shah", role: "Head of Support" },
{ name: "Tomás Herrera", role: "Data Engineer" },
{ name: "Hana Sato", role: "Product Designer" },
],
};
/* -------------------------------------------------------------- careers */
export const roles: Role[] = [
{ slug: "senior-data-engineer", title: "Senior Data Engineer", team: "Engineering", location: "Remote, Europe", type: "Full-time", summary: "Keep 2.1 billion events a month flowing through ClickHouse.", salary: "€88k – €112k", posted: "2026-10-14" },
{ slug: "frontend-engineer", title: "Frontend Engineer", team: "Engineering", location: "Berlin or remote", type: "Full-time", summary: "Make the dashboard load in under 400 ms with 90 days selected.", salary: "€76k – €96k", posted: "2026-10-03" },
{ slug: "product-designer", title: "Product Designer", team: "Design", location: "Remote, Europe", type: "Full-time", summary: "Turn funnels and cohorts into screens anyone can read.", salary: "€72k – €90k", posted: "2026-09-29" },
{ slug: "support-engineer", title: "Support Engineer", team: "Support", location: "Istanbul or remote", type: "Full-time", summary: "Answer with a fix or a query, not a ticket number.", salary: "€54k – €66k", posted: "2026-10-18" },
{ slug: "technical-writer", title: "Technical Writer", team: "Support", location: "Remote, Europe", type: "Part-time", summary: "Write the install guides for 14 frameworks.", salary: "€34k – €41k (0.6 FTE)", posted: "2026-08-27" },
];
// One description for every opening: what working on a Lumen team is like.
export const roleDetail: RoleDetail = {
about:
"Lumen is 27 people in teams of three or four. Each team owns one part of the product end to end, from the first design doc to the support replies, and ships in six-week cycles with two weeks to tidy up. Last quarter that meant Funnels 2.0, a 4.6× cheaper event store and a dashboard that loads in under 400 ms.",
responsibilities: [
"Own a part of Lumen with your team, from the first sketch to the support reply.",
"Write the short design docs that decide what we build, and the ones that decide what we don't.",
"Ship in small pieces and read the numbers afterwards. We use our own product for that.",
"Take a turn on support one week in eight, and fix what you find.",
],
requirements: [
"Several years doing this work for a product people pay for.",
"Care for the details people notice: copy, speed, edge cases.",
"Clear written English. Most of our decisions happen in documents.",
],
niceToHave: ["Experience with analytics, data tools or privacy law.", "You have run a small open-source project or community."],
process: ["30-minute intro call", "Paid take-home (€400) or a past project walkthrough", "Two 60-minute conversations with the team", "Reference calls and an offer within 10 working days"],
};
/* ----------------------------------------------------------------- blog */
export interface LumenPost {
slug: string;
tag: string;
title: string;
excerpt: string;
/** ISO date. */
date: string;
readingTime: string;
sections: PostSection[];
pullQuote: { text: string; cite: string; after: number } | null;
}
export const author: PostAuthor = {
name: "Sara Okafor",
role: "Developer Experience",
bio: "Sara leads developer experience at Lumen. Before that she ran docs at Kitebase for five years.",
href: "#/about",
};
export const blogPosts: LumenPost[] = [
{
slug: "funnels-2",
tag: "Product",
title: "Funnels 2.0: why we show the median time between steps",
excerpt: "Averages made our own onboarding look fine. The median, and the spread around it, told us where people actually stalled.",
date: "2026-10-21",
readingTime: "7 min read",
sections: [
{
id: "the-problem",
title: "Averages hid the slow step",
body: [
"Our own trial funnel said people took 6 minutes on average to go from sign-up to adding the script. That looked fine, and it was wrong.",
"Most people finished in under 2 minutes. A long tail took days, and those few dragged the average up while hiding how many got stuck.",
],
},
{
id: "what-changed",
title: "What changed in Funnels 2.0",
body: [
"Every step now shows the median time from the step before, with the 25th and 75th percentiles as a band. A wide band means people split into two groups, and that is usually the step to look at.",
"Funnels also accept any custom event, not only page views, so you can measure a step like \"copied the snippet\" that never loads a page.",
],
},
{
id: "results",
title: "What we found in our own data",
body: [
"The slow group was people on Webflow, who had to find a settings page we never linked to. One help article and a direct link cut their median from 3 days to 11 minutes.",
"Trials that added the script on day one went from 61% to 74% in the four weeks after.",
],
},
],
pullQuote: { text: "The average said six minutes. The median said two, and a third of people took days.", cite: "Sara Okafor, Developer Experience", after: 0 },
},
{
slug: "counting-without-cookies",
tag: "Engineering",
title: "Counting visitors without cookies",
excerpt: "A daily rotating salt, a hash we can't reverse, and the numbers we tested it against.",
date: "2026-10-02",
readingTime: "9 min read",
sections: [
{
id: "the-idea",
title: "One hash a day, then forget it",
body: [
"To count unique visitors we hash the site, the IP address, the browser and a salt that changes every midnight. The salt is never stored, so yesterday's hashes can't be rebuilt.",
"The IP address itself never reaches a disk. It lives in memory for the few milliseconds it takes to look up the country.",
],
},
{
id: "accuracy",
title: "How close is it?",
body: [
"We ran the method next to a cookie-based counter on 212 consenting sites for 30 days. Daily uniques matched within 2.8%, with the gap mostly from people switching networks.",
"Multi-day uniques are less exact by design. We show them as ranges and say so in the interface.",
],
},
{
id: "trade-offs",
title: "The trade-offs we accept",
body: [
"We can't follow one person across days or devices, and we don't want to. If your question needs that, you need consent, and Lumen is the wrong tool.",
],
},
],
pullQuote: null,
},
{
slug: "clickhouse-migration",
tag: "Engineering",
title: "Moving 2 billion events to ClickHouse",
excerpt: "Three migrations, one weekend of dual writes and a 4.6× smaller bill.",
date: "2026-09-12",
readingTime: "11 min read",
sections: [
{
id: "why-move",
title: "Why we moved",
body: [
"Our Postgres cluster handled 2.14 billion events a month, but a 90-day query on a busy site took 1.9 seconds at p95 and storage grew faster than revenue.",
"Columnar storage fits analytics: we read a few columns over many rows, almost never one whole row.",
],
},
{
id: "dual-writes",
title: "A weekend of dual writes",
body: [
"For 48 hours every event went to both databases. A checker compared hourly counts per site and paged us on any gap above 0.1%. It fired twice, both times for clock skew in one region.",
"We moved dashboards over one plan tier at a time, starting with Starter sites, which forgive a slow chart more easily.",
],
},
{
id: "results",
title: "Results",
body: ["p95 query time dropped to 410 ms. The storage bill fell 4.6×, which paid for the migration in under three months."],
},
],
pullQuote: { text: "Columnar storage fits analytics: we read a few columns over many rows.", cite: "Jun Lee, Head of Infrastructure", after: 0 },
},
];
const shortDate = (iso: string) => new Date(`${iso}T12:00:00Z`).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
/** Cards for the blog index. */
export const posts: Post[] = blogPosts.map((p) => ({
title: p.title,
excerpt: p.excerpt,
tag: p.tag,
date: shortDate(p.date),
readingTime: p.readingTime.replace(" read", ""),
href: `#/blog/${p.slug}`,
}));
/** The other posts, for the "Keep reading" list under a post. */
export const relatedTo = (slug: string): RelatedPost[] =>
blogPosts.filter((p) => p.slug !== slug).map((p) => ({ title: p.title, tag: p.tag, readingTime: p.readingTime.replace(" read", ""), href: `#/blog/${p.slug}` }));
/* ------------------------------------------------------------ customers */
export interface LumenCaseStudy {
slug: string;
company: string;
industry: string;
title: string;
summary: string;
metrics: CaseMetric[];
facts: { label: string; value: string }[];
chapters: CaseChapter[];
quote: { text: string; name: string; role: string };
card: Omit<CaseStudyCard, "company" | "industry" | "href">;
}
export const caseStudies: LumenCaseStudy[] = [
{
slug: "polaris",
company: "Polaris",
industry: "SaaS",
title: "How Polaris dropped its cookie banner and 11.4% of its bounces",
summary: "A 62-person SaaS company replaced Google Analytics with Lumen, removed its consent pop-up and finally got numbers that matched its sales data.",
metrics: [
{ value: 11.4, decimals: 1, suffix: "%", label: "fewer bounces in the first week" },
{ value: 23, suffix: "%", label: "more visits counted than with GA and a consent banner" },
{ value: 5, suffix: " min", label: "to install across three sites" },
],
facts: [
{ label: "Industry", value: "SaaS, scheduling" },
{ label: "Team size", value: "62 people" },
{ label: "Headquarters", value: "Copenhagen" },
{ label: "Using Lumen since", value: "February 2025" },
{ label: "Plan", value: "Business yearly" },
],
chapters: [
{
title: "The challenge",
paragraphs: [
"Polaris sells scheduling software to clinics across the Nordics. Its legal team required a consent banner for Google Analytics, and 38% of visitors declined it.",
"Marketing was making decisions on less than two thirds of its traffic, and the numbers never matched sign-ups in the billing system.",
],
},
{
title: "The approach",
paragraphs: [
"The growth team ran Lumen next to GA for two weeks to compare. Legal reviewed the data processing agreement and confirmed no consent was needed for Lumen.",
"Then they removed GA and the banner on the same day, and imported two years of history so the trend lines stayed intact.",
],
},
{
title: "The results",
paragraphs: [
"Bounce rate fell 11.4% the first week without the pop-up. Lumen counted 23% more visits than GA had, and sign-up attribution now matches billing within 2%.",
"The weekly growth meeting runs off one shared Lumen dashboard instead of three exported spreadsheets.",
],
},
],
quote: { text: "We dropped our cookie banner and our bounce rate in the same week. The numbers finally match our sales data.", name: "Ada Kowalski", role: "Head of growth, Polaris" },
card: { metric: "11.4%", metricLabel: "fewer bounces after dropping the banner", quote: "We dropped our cookie banner and our bounce rate in the same week.", person: "Ada Kowalski", role: "Head of growth" },
},
{
slug: "orbitly",
company: "Orbitly",
industry: "SaaS",
title: "How Orbitly found one confusing step and lifted trials 18%",
summary: "A product team used Lumen funnels to see where sign-ups stalled during onboarding, fixed one screen and measured the change the same week.",
metrics: [
{ value: 18, suffix: "%", label: "more trials started" },
{ value: 3, suffix: " days", label: "from first funnel to shipped fix" },
{ value: 4.1, decimals: 1, suffix: "%", label: "trial-to-paid, up from 3.3%" },
],
facts: [
{ label: "Industry", value: "SaaS, team planning" },
{ label: "Team size", value: "34 people" },
{ label: "Headquarters", value: "Lisbon" },
{ label: "Using Lumen since", value: "June 2025" },
{ label: "Plan", value: "Growth" },
],
chapters: [
{
title: "The challenge",
paragraphs: [
"Orbitly's sign-ups were growing, but trials weren't keeping up. The team had page views by the million and no idea which step people abandoned.",
],
},
{
title: "The approach",
paragraphs: [
"They built a four-step funnel from custom events: sign-up, workspace created, first plan added, teammate invited. The median time on step three was 14 minutes, against under 2 for the rest.",
"Session notes showed people stuck on an empty plan template. The team replaced it with a filled-in example.",
],
},
{
title: "The results",
paragraphs: ["The median on step three fell to 3 minutes and trials rose 18% over the next four weeks, with no change in traffic."],
},
],
quote: { text: "Funnels showed us one confusing step in onboarding. We fixed it and trials went up 18%.", name: "Jun Lee", role: "Product manager, Orbitly" },
card: { metric: "18%", metricLabel: "more trials after one onboarding fix", quote: "Funnels showed us one confusing step. We fixed it the same afternoon.", person: "Jun Lee", role: "Product manager" },
},
{
slug: "marlow-finch",
company: "Marlow & Finch",
industry: "Agencies",
title: "How Marlow & Finch replaced monthly PDF reports with live links",
summary: "A web agency moved 41 client sites to Lumen and swapped its monthly reporting day for shared dashboards clients actually open.",
metrics: [
{ value: 41, label: "client sites on one account" },
{ value: 2, suffix: " days", label: "of reporting work saved each month" },
{ value: 87, suffix: "%", label: "of clients open their link weekly" },
],
facts: [
{ label: "Industry", value: "Web and brand agency" },
{ label: "Team size", value: "19 people" },
{ label: "Headquarters", value: "Bristol" },
{ label: "Using Lumen since", value: "October 2024" },
{ label: "Plan", value: "Business" },
],
chapters: [
{
title: "The challenge",
paragraphs: [
"Every month two people spent two days exporting charts into PDF reports for 41 clients. Most clients never replied, and some asked for numbers that were already inside.",
],
},
{
title: "The approach",
paragraphs: [
"The agency moved every site to one Lumen account and gave each client a shared, read-only dashboard with a password and their own goals.",
],
},
{
title: "The results",
paragraphs: ["Reporting day is gone. 87% of clients open their link at least once a week, and account calls start from the same numbers."],
},
],
quote: { text: "Clients get a live link instead of a monthly PDF, and they actually read it.", name: "Elif Demir", role: "Managing partner, Marlow & Finch" },
card: { metric: "41", metricLabel: "client sites reporting from one account", quote: "Clients get a live link instead of a monthly PDF, and they read it.", person: "Elif Demir", role: "Managing partner" },
},
];
/** Story cards for the customers page. */
export const stories: CaseStudyCard[] = caseStudies.map((c) => ({ company: c.company, industry: c.industry, href: `#/customers/${c.slug}`, ...c.card }));
/* -------------------------------------------------------------- contact */
export const channels: Channel[] = [
{ title: "Sales", body: "Business plans, security reviews and paying by invoice.", action: "Email sales", href: "mailto:sales@lumen.example" },
{ title: "Support", body: "Stuck on setup? Median first reply is 2 hours 14 minutes.", action: "Open the help center", href: "#/app/help" },
{ title: "Press", body: "Logos, screenshots and founder bios in one download.", action: "Email press", href: "mailto:press@lumen.example" },
];
export const offices: Office[] = [
{ city: "Berlin", address: ["Linienstraße 214", "10119 Berlin, Germany"], hours: "Mon–Fri, 09:00–18:00 CET", pin: { x: 52.5, y: 33 }, mapHref: "https://maps.google.com/?q=Linienstra%C3%9Fe+214+Berlin" },
{ city: "Istanbul", address: ["Kemankeş Karamustafa Paşa Mah. 41", "34425 Beyoğlu, İstanbul, Türkiye"], hours: "Mon–Fri, 09:00–18:00 TRT", pin: { x: 58, y: 40 }, mapHref: "https://maps.google.com/?q=Karak%C3%B6y+Istanbul" },
];