diff --git a/site/src/components/CRMMock.astro b/site/src/components/CRMMock.astro new file mode 100644 index 00000000..afe9f41f --- /dev/null +++ b/site/src/components/CRMMock.astro @@ -0,0 +1,390 @@ +--- +/** + * Pixel-faithful mock of the Warmbly /app/crm/deals screen (the CRM kanban board). + * + * Maps 1:1 to the real components: + * - web/src/app/app/crm/deals/page.tsx (the board page: PageTopbar / StatStrip / SectionBar / Board) + * - web/src/components/layout/Page.tsx (PageTopbar h-12, StatStrip, Stat, SectionBar, TopbarAction) + * - the Board / Column / DealCard primitives inside deals/page.tsx + * + * Renders only the in-app screen content (page topbar + stat strip + section bar + * + the horizontal kanban). The marketing page wraps this in its own browser + * frame, so there is no global sidebar / app header / window chrome here. The + * board overflows the frame on purpose; the parent crops with overflow-hidden, + * so the load-bearing content sits top-left. + * + * Tokens copied verbatim from the real source: + * - PageTopbar: h-12 px-5 border-b border-slate-200, eyebrow text-[10px] uppercase + * tracking-[0.14em] text-slate-400, h-4 w-px divider, subtitle text-[12.5px] + * - TopbarAction: h-7 px-2.5 rounded-md bg-sky-600 hover:bg-sky-700 text-white text-[12px] + * - Stat: label text-[10px] uppercase tracking-[0.14em], value text-[26px] font-light + * - Column head: h-9 px-3, size-1.5 color dot, name text-[11px] uppercase tracking-[0.1em] + * font-semibold text-slate-700, count font-mono text-[10.5px] + * - DealCard: rounded-lg bg-white border border-slate-200, name text-[12.5px] font-medium, + * value font-mono text-[12px] text-slate-700 + */ + +// Money formatter, mirrors formatMoney() in deals/page.tsx. +const money = (n: number) => + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }).format(n); + +// ── Sample dataset: one pipeline, four stages, eight deals ───────── +type Tag = { color: string; label: string }; +type Deal = { + name: string; + value: number; + contact: string; + owner: string; // initials for the avatar + gradient: string; // tailwind gradient classes for the owner avatar + status?: "won"; + close?: string; + tags: Tag[]; +}; + +type Stage = { + name: string; + color: string; // hex dot, mirrors STAGE_COLORS in pipelines/page.tsx + won?: boolean; + deals: Deal[]; +}; + +const stages: Stage[] = [ + { + name: "Lead", + color: "#94a3b8", // slate-400 + deals: [ + { + name: "Acme Co", + value: 12000, + contact: "dana@acme.co", + owner: "MO", + gradient: "from-violet-400 to-indigo-500", + close: "Jun 18", + tags: [{ color: "#3b82f6", label: "Inbound" }], + }, + { + name: "Northwind", + value: 8500, + contact: "ravi@northwind.dev", + owner: "JS", + gradient: "from-sky-400 to-blue-500", + close: "Jun 21", + tags: [{ color: "#f59e0b", label: "Outbound" }], + }, + { + name: "Spruce Bio", + value: 6200, + contact: "lena@sprucebio.com", + owner: "AM", + gradient: "from-rose-400 to-pink-500", + tags: [{ color: "#3b82f6", label: "Inbound" }], + }, + ], + }, + { + name: "Qualified", + color: "#0ea5e9", // sky-500 + deals: [ + { + name: "Helio", + value: 24000, + contact: "founder@helio.gen", + owner: "MO", + gradient: "from-violet-400 to-indigo-500", + close: "Jul 02", + tags: [ + { color: "#8b5cf6", label: "Enterprise" }, + { color: "#3b82f6", label: "Inbound" }, + ], + }, + { + name: "Tidewater Labs", + value: 15500, + contact: "ops@tidewater.io", + owner: "CR", + gradient: "from-teal-400 to-emerald-500", + close: "Jul 09", + tags: [{ color: "#f59e0b", label: "Outbound" }], + }, + ], + }, + { + name: "Proposal", + color: "#8b5cf6", // violet-500 + deals: [ + { + name: "Brightfold", + value: 31000, + contact: "priya@brightfold.com", + owner: "JS", + gradient: "from-sky-400 to-blue-500", + close: "Jul 15", + tags: [ + { color: "#8b5cf6", label: "Enterprise" }, + { color: "#ef4444", label: "Hot" }, + ], + }, + { + name: "Maple & Co", + value: 9800, + contact: "sam@mapleco.com", + owner: "AM", + gradient: "from-rose-400 to-pink-500", + close: "Jul 20", + tags: [{ color: "#3b82f6", label: "Inbound" }], + }, + ], + }, + { + name: "Won", + color: "#10b981", // emerald-500 + won: true, + deals: [ + { + name: "Cobalt Group", + value: 42000, + contact: "ceo@cobaltgroup.com", + owner: "MO", + gradient: "from-violet-400 to-indigo-500", + status: "won", + close: "Jun 03", + tags: [ + { color: "#8b5cf6", label: "Enterprise" }, + { color: "#10b981", label: "Closed" }, + ], + }, + ], + }, +]; + +// ── Derived strip values, mirrors the page's reduce()s ───────────── +const allDeals = stages.flatMap((s) => s.deals); +const totalValue = allDeals.reduce((a, d) => a + d.value, 0); +const openCount = allDeals.filter((d) => d.status !== "won").length; +const wonCount = allDeals.filter((d) => d.status === "won").length; +const stageTotal = (s: Stage) => s.deals.reduce((a, d) => a + d.value, 0); + +const stats = [ + { label: "Open", value: String(openCount), sub: "active deals", last: false }, + { label: "Pipeline value", value: money(totalValue), sub: "all filtered", last: false }, + { label: "Won", value: String(wonCount), sub: "this view", last: false }, + { label: "Stages", value: String(stages.length), sub: "on this pipeline", last: true }, +]; +--- +