← All commits
AI · claude-opus-4-7radosukala071825dOur-One/our-one

Homepage Phase 2C+2D: refusal cards strip + AI terminal panel

+835 / 126 filesscreenshot pending

Files changed

Diff

Lines reveal in sequence as you scroll. First 20 lines per file shown — expand for the rest.

new filesrc/marketing/components/ai-terminal.tsx+132 / 0tsx
@@ -0,0 +1,132 @@
+"use client";
+ 
+import Link from "next/link";
+import { useEffect, useRef, useState } from "react";
+import type { AiBuildEntry } from "@/shared/movement/ai-build-queries";
+ 
+interface Props {
+ entries: AiBuildEntry[];
+}
+ 
+function timeAgo(date: Date | string): string {
+ const d = typeof date === "string" ? new Date(date) : date;
+ const ms = Date.now() - d.getTime();
+ const mins = Math.floor(ms / 60000);
+ if (mins < 1) return "just now";
+ if (mins < 60) return `${mins}m ago`;
+ const hrs = Math.floor(mins / 60);
+ if (hrs < 24) return `${hrs}h ago`;
+ const days = Math.floor(hrs / 24);
modifiedsrc/marketing/components/homepage.tsx+4 / 12tsx
@@ -17,12 +17,13 @@ import { CountersStrip } from "./counters-strip";
import { PulseTicker } from "./pulse-ticker";
import { PatronWall } from "./patron-wall";
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";
import { CommitTicker } from "./commit-ticker";
import { RecentShipsStrip } from "./recent-ships-strip";
+import { RefusalCardsStrip } from "./refusal-cards-strip";
+import { LiveBuildPanel } from "./live-build-panel";
 
// COMMITMENTS rendered with their canonical numbers from the constitution.
// ROLES section was removed from homepage in the living-organism rewrite —
@@ -120,7 +121,8 @@ export async function Homepage() {
<div id="products">
<ProductsStrip />
</div>
<FeaturedRefusalSection />
new filesrc/marketing/components/live-build-panel.tsx+58 / 0tsx
@@ -0,0 +1,58 @@
+import Link from "next/link";
+import { getRecentAiBuildEntries } from "@/shared/movement/ai-build-queries";
+import { countAiCommitsEver } from "@/shared/commits";
+import { AiTerminal } from "./ai-terminal";
+ 
+export async function LiveBuildPanel() {
+ const [entries, totalAi] = await Promise.all([
+ getRecentAiBuildEntries(5).catch(() => []),
+ countAiCommitsEver().catch(() => 0),
+ ]);
+ 
+ if (entries.length === 0) {
+ return null;
+ }
+ 
+ return (
+ <section className="bg-[#FDFBF7] px-6 py-20 md:py-24">
+ <div className="mx-auto w-full max-w-[76rem]">
+ <p className="font-sans text-[11px] font-medium uppercase tracking-[0.25em] text-stone-500">
new filesrc/marketing/components/refusal-cards-strip.tsx+268 / 0tsx
@@ -0,0 +1,268 @@
+import Link from "next/link";
+import { listRefusalsWithChains } from "@/shared/movement/queries";
+import { listComparisonsForFeature } from "@/shared/comparisons/queries";
+import { listCommitmentTiesForFeature } from "@/shared/features/queries";
+import type { Feature } from "@/shared/db/schema/features";
+import type { FeatureChain } from "@/shared/movement/queries";
+import { HorizontalScroll } from "./horizontal-scroll";
+ 
+function splitNoTitle(title: string): { rest: string } | null {
+ const match = title.match(/^No\.?\s+(.+)$/i);
+ if (!match) return null;
+ return { rest: match[1] };
+}
+ 
+interface RefusalCardData {
+ feature: Feature;
+ chain: FeatureChain;
+ incumbentSlug: string | null;
+ comparisonSlug: string | null;
new filesrc/shared/movement/ai-build-queries.ts+67 / 0typescript
@@ -0,0 +1,67 @@
+import { and, desc, eq, lte, isNotNull } from "drizzle-orm";
+import { getReadDb } from "@/shared/db/client";
+import { commits } from "@/shared/db/schema/commits";
+ 
+export interface AiBuildEntry {
+ id: string;
+ sha: string;
+ shortMessage: string;
+ /** First non-empty paragraph of the reasoning trailer, ~200-400 chars. */
+ reasoningExcerpt: string;
+ /** Full reasoning markdown — used by the typewriter when expanded. */
+ reasoningFull: string;
+ aiProvider: string | null;
+ committedAt: Date;
+}
+ 
+const EXCERPT_MAX = 600;
+ 
+function firstParagraph(reasoning: string): string {
new filesrc/shared/movement/queries.ts+306 / 0typescript
@@ -0,0 +1,306 @@
+/**
+ * Movement queries — joins across the public chain.
+ *
+ * The homepage's living-organism surfaces (refusal cards with chain inline,
+ * recent NOs, recent YESes) all need to traverse from a Feature back to the
+ * humans who caused it: which wishes originated it, how many members reacted
+ * supportively, which ADRs locked it. These joins are non-trivial and reused
+ * across multiple components, so they live here.
+ */
+ 
+import { and, asc, count, desc, eq, inArray, sql } from "drizzle-orm";
+import { getReadDb } from "@/shared/db/client";
+import { features, type Feature } from "@/shared/db/schema/features";
+import { featureWishes } from "@/shared/db/schema/feature-wishes";
+import { wishes } from "@/shared/db/schema/wishes";
+import { wishReactions } from "@/shared/db/schema/wish-reactions";
+import { featureAdrs } from "@/shared/db/schema/feature-adrs";
+import { adrs } from "@/shared/db/schema/adrs";
+import { users } from "@/shared/db/schema/auth";