Homepage Phase 1: living organism over SaaS landing — operator anchor, products, gloss
+562 / −1337 filesscreenshot pending
Files changed
- Asrc/app/(marketing)/products/[slug]/page.tsx+232 / −0
- Msrc/marketing/components/homepage.tsx+106 / −95
- Msrc/marketing/components/named-betrayal-beat.tsx+3 / −3
- Msrc/marketing/components/offer-beat.tsx+1 / −1
- Asrc/marketing/components/products-strip.tsx+115 / −0
- Msrc/marketing/components/real-human-beat.tsx+10 / −34
- Asrc/shared/products/registry.ts+95 / −0
Diff
Lines reveal in sequence as you scroll. First 20 lines per file shown — expand for the rest.
src/app/(marketing)/products/[slug]/page.tsx+232 / −0tsx| @@ -0,0 +1,232 @@ | |
| + | import Link from "next/link"; |
| + | import { notFound } from "next/navigation"; |
| + | import type { Metadata } from "next"; |
| + | import { getProductBySlug, type ProductEntry } from "@/shared/products/registry"; |
| + | import { listPublishedComparisonsByIncumbent } from "@/shared/comparisons/queries"; |
| + | import { findIncumbentBySlug } from "@/shared/comparisons/queries"; |
| + | |
| + | export const dynamic = "force-dynamic"; |
| + | |
| + | interface PageProps { |
| + | params: Promise<{ slug: string }>; |
| + | } |
| + | |
| + | export async function generateMetadata({ |
| + | params, |
| + | }: PageProps): Promise<Metadata> { |
| + | const { slug } = await params; |
| + | const product = getProductBySlug(slug); |
| + | if (!product) return { title: "Product — Our.one" }; |
src/marketing/components/homepage.tsx+106 / −95tsx| @@ -20,36 +20,29 @@ import { HomeActions } from "./home-actions"; | |
| import { NamedBetrayalBeat } from "./named-betrayal-beat"; | |
| import { RealHumanBeat } from "./real-human-beat"; | |
| import { OfferBeat } from "./offer-beat"; | |
| + | import { ProductsStrip } from "./products-strip"; |
| // COMMITMENTS rendered with their canonical numbers from the constitution. | |
| + | // ROLES section was removed from homepage in the living-organism rewrite — |
| + | // the role economics are now on /join (content/en/join.md) where they belong. |
| − | const ROLES: { name: string; what: string; how: string }[] = [ |
| − | { |
| − | name: "User", |
| − | what: "Subscribe, use, retain.", |
| − | how: "40% of every flagship's net revenue flows back to Users. The product pays you to stay.", |
| − | }, |
| − | { |
| − | name: "Ambassador", |
| − | what: "Bring anyone onto a product — consumer signups or enterprise deals.", |
| − | how: "25% of net revenue, recurring share of what your recruits generate, single-level attribution.", |
src/marketing/components/named-betrayal-beat.tsx+3 / −3tsx| @@ -45,7 +45,7 @@ export async function NamedBetrayalBeat() { | |
| if (!feature) { | |
| return ( | |
| − | <BeatShell label="Beat 1 · What we refuse"> |
| + | <BeatShell label="What we constitutionally refuse"> |
| <div className="flex flex-col gap-6 rounded-2xl border-2 border-dashed border-stone-300 bg-stone-50 px-6 py-10 md:px-8 md:py-12"> | |
| <p className="font-sans text-[11px] font-medium uppercase tracking-[0.25em] text-stone-500"> | |
| Refusals being drafted now | |
| @@ -111,8 +111,8 @@ export async function NamedBetrayalBeat() { | |
| const noSplit = isRefusal ? splitNoTitle(feature.title) : null; | |
| const sectionLabel = isRefusal | |
| − | ? "Beat 1 · What we refuse" |
| − | : "Beat 1 · What we ship"; |
| + | ? "What we constitutionally refuse" |
| + | : "What we ship this week"; |
| const scopeTag = isRefusal | |
| ? `Constitutional refusal · ${feature.productScope ?? "portfolio-wide"}` | |
| : `Our.one ships${feature.productScope ? ` · ${feature.productScope}` : " · portfolio-wide"}`; |
src/marketing/components/offer-beat.tsx+1 / −1tsx| @@ -34,7 +34,7 @@ export async function OfferBeat({ stripeCheckoutEnabled }: Props) { | |
| return ( | |
| <section> | |
| <p className="font-sans text-[10px] font-medium uppercase tracking-[0.3em] text-stone-400"> | |
| − | Beat 3 · The offer with mechanical urgency |
| + | $100 once. Lifetime. |
| </p> | |
| <div className="mt-4 rounded-2xl border-2 border-stone-900 bg-stone-900 px-6 py-10 text-[#FDFBF7] md:px-10 md:py-12"> |
src/marketing/components/products-strip.tsx+115 / −0tsx| @@ -0,0 +1,115 @@ | |
| + | import Link from "next/link"; |
| + | import { PRODUCTS, type ProductEntry } from "@/shared/products/registry"; |
| + | |
| + | const STATUS_LABEL: Record<ProductEntry["status"], string> = { |
| + | live: "Live", |
| + | "in-build": "In build", |
| + | planning: "Planning", |
| + | }; |
| + | |
| + | const STATUS_TAG_CLASS: Record<ProductEntry["status"], string> = { |
| + | live: "bg-emerald-50 text-emerald-700 border-emerald-200", |
| + | "in-build": "bg-amber-50 text-amber-800 border-amber-200", |
| + | planning: "bg-stone-100 text-stone-700 border-stone-300", |
| + | }; |
| + | |
| + | function ProductCard({ product }: { product: ProductEntry }) { |
| + | const isLive = product.status === "live"; |
| + | const isInBuild = product.status === "in-build"; |
| + | const isPlanning = product.status === "planning"; |
src/marketing/components/real-human-beat.tsx+10 / −34tsx| @@ -97,45 +97,22 @@ const dateFmt = new Intl.DateTimeFormat("en-US", { | |
| export async function RealHumanBeat() { | |
| const human = await pickRotatedHuman().catch(() => null); | |
| + | // Empty state: hide entirely. The previous "First Patron lands at Stripe |
| + | // go-live" placeholder broadcast emptiness. Better to render nothing than |
| + | // tell visitors no one has joined yet. Once Rado completes Stripe checkout |
| + | // as Patron #1, this surface activates with his name. |
| + | if (!human) { |
| + | return null; |
| + | } |
| + | |
| return ( | |
| <section> | |
| <p className="font-sans text-[10px] font-medium uppercase tracking-[0.3em] text-stone-400"> | |
| − | Beat 2 · The real human with a name |
| + | Real humans, named |
| </p> | |
| <div className="mt-4"> |