"use client";
import * as React from "react";
import { ActivityStream } from "@/components/ui/activity-stream";
import type { Activity } from "@/components/ui/activity-stream";
const M = 60_000;
function seed(now: number): Activity[] {
const at = (minsAgo: number) => now - minsAgo * M;
const log = (lines: string[]) => (
<pre className="m-0 whitespace-pre-wrap font-mono text-[11.5px] leading-[1.6] opacity-80">
{lines.join("\n")}
</pre>
);
return [
{
id: "e1",
at: at(3),
actor: "Mia",
kind: "deploy",
text: (
<>
deployed <em>nimbus-web</em> to Production
</>
),
status: { label: "Ready", tone: "ok" },
detail: log([
"7f3c2a1 · Move search to edge runtime",
"Built in 1m 42s · 38 routes",
"✓ nimbus.app",
]),
},
{
id: "e2",
at: at(9),
actor: "CI",
kind: "approve",
text: (
<>
checks passed on <em>PR #212</em>
</>
),
status: { label: "12 / 12", tone: "ok" },
},
{
id: "e3",
at: at(14),
actor: "Mia",
kind: "comment",
text: (
<>
commented on <em>Search architecture notes</em>
</>
),
detail: (
<p className="m-0 max-w-[52ch] leading-relaxed">
"Edge cache keys need the workspace id, otherwise the two apps will
share facets. Adding that before we flip the flag."
</p>
),
},
{
id: "e4",
at: at(16),
actor: "Mia",
kind: "status",
text: (
<>
moved <em>PRJ-212</em> from In progress to Review
</>
),
},
{
id: "e5",
at: at(24),
actor: "Mia",
kind: "upload",
groupKey: "upload-atlas",
groupText: (n) => (
<>
uploaded {n} files to <em>Atlas</em>
</>
),
text: (
<>
uploaded <em>floor-c-plan.pdf</em>
</>
),
},
{
id: "e6",
at: at(26),
actor: "Mia",
kind: "upload",
groupKey: "upload-atlas",
groupText: (n) => (
<>
uploaded {n} files to <em>Atlas</em>
</>
),
text: (
<>
uploaded <em>room-index.csv</em>
</>
),
},
{
id: "e7",
at: at(27),
actor: "Mia",
kind: "upload",
groupKey: "upload-atlas",
groupText: (n) => (
<>
uploaded {n} files to <em>Atlas</em>
</>
),
text: (
<>
uploaded <em>signage-photos.zip</em>
</>
),
},
{
id: "e8",
at: at(31),
actor: "Mia",
kind: "upload",
groupKey: "upload-atlas",
groupText: (n) => (
<>
uploaded {n} files to <em>Atlas</em>
</>
),
text: (
<>
uploaded <em>entrances.geojson</em>
</>
),
},
{
id: "e9",
at: at(214),
actor: "CI",
kind: "deploy",
text: (
<>
deployed <em>worker-mail</em> to Production
</>
),
status: { label: "Failed", tone: "bad" },
detail: log([
"2ad9cc3 · Template for invoice reminders",
"error: Cannot find module './reminder.mjml'",
"✗ Rolled back to e3a7713",
]),
},
{
id: "e10",
at: at(222),
actor: "Mia",
kind: "assign",
text: (
<>
assigned <em>PRJ-205 · Invoice reminders send twice</em> to themself
</>
),
},
{
id: "e11",
at: at(300),
actor: "Design Team",
kind: "approve",
text: (
<>
approved the <em>Q3 roadmap</em>
</>
),
status: { label: "Approved", tone: "ok" },
},
{
id: "e12",
at: at(24 * 60 + 40),
actor: "Mia",
kind: "status",
text: (
<>
closed <em>PRJ-41 · Room search index</em>
</>
),
status: { label: "Done", tone: "neutral" },
},
{
id: "e13",
at: at(24 * 60 + 55),
actor: "CI",
kind: "deploy",
text: (
<>
deployed <em>nimbus-api</em> to Staging
</>
),
status: { label: "Ready", tone: "ok" },
},
{
id: "e14",
at: at(24 * 60 + 300),
actor: "Mia",
kind: "comment",
text: (
<>
commented on <em>Draft v4 — methods</em>
</>
),
detail: (
<p className="m-0 max-w-[52ch] leading-relaxed">
"Section 3.2 still reads like a list. Rewriting it as a narrative of
the survey rounds."
</p>
),
},
];
}
const INCOMING: Omit<Activity, "id" | "at">[] = [
{
actor: "CI",
kind: "approve",
text: (
<>
checks passed on <em>PR #214</em>
</>
),
status: { label: "12 / 12", tone: "ok" },
},
{
actor: "Mia",
kind: "comment",
text: (
<>
replied in <em>PRJ-209 · Deploy previews</em>
</>
),
detail: (
<p className="m-0 leading-relaxed">
"Reproduced on the monorepo fixture — it's the 10 MB manifest limit."
</p>
),
},
{
actor: "Mia",
kind: "deploy",
text: (
<>
deployed <em>search-edge</em> to Preview
</>
),
status: { label: "Building", tone: "warn" },
},
];
export default function ActivityStreamDemo() {
const [now, setNow] = React.useState(() => Date.now());
const base = React.useMemo(() => seed(now), []); // eslint-disable-line react-hooks/exhaustive-deps
const [events, setEvents] = React.useState<Activity[]>(base);
const [n, setN] = React.useState(0);
const push = () => {
const t = Date.now();
setNow(t);
setEvents((e) => [
{ ...INCOMING[n % INCOMING.length], id: `new${n}`, at: t },
...e,
]);
setN((x) => x + 1);
};
React.useEffect(() => {
const t = window.setInterval(() => setNow(Date.now()), 30_000);
return () => window.clearInterval(t);
}, []);
return (
<div
className="flex h-full min-h-[640px] w-full justify-center overflow-auto bg-[#fbfaf6] px-5 py-8 text-[#1b1a17]"
style={{ fontFamily: "Geist, ui-sans-serif, system-ui" }}
>
<div className="w-full max-w-[720px]">
<div className="mb-5 flex flex-wrap items-center gap-2">
<h1 className="m-0 mr-auto text-[20px] font-semibold tracking-[-0.02em]">
Project activity
</h1>
<button
type="button"
onClick={push}
className="h-8 rounded-lg px-3 text-[12.5px] ring-1 ring-black/10"
>
Simulate new event
</button>
</div>
<ActivityStream
theme="paper"
label="Project activity"
now={now}
events={events}
/>
</div>
</div>
);
}