feat(sp6): Inbox page + useInboxLanes hook

This commit is contained in:
Tyler
2026-06-20 18:41:44 -06:00
parent 610e580844
commit b607e4c459
4 changed files with 162 additions and 6 deletions
+33
View File
@@ -0,0 +1,33 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, waitFor } from "@testing-library/react";
import Inbox from "./Inbox";
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
});
describe("Inbox page", () => {
it("renders the four lane headers", async () => {
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
rejected: [],
candidates: [],
unmatched: [],
done_today: [],
}),
}),
);
const { container } = render(<Inbox />);
await waitFor(() => {
expect(container.textContent).toContain("REJECTED");
expect(container.textContent).toContain("CANDIDATES");
expect(container.textContent).toContain("UNMATCHED");
expect(container.textContent).toContain("DONE");
});
});
});
+43 -6
View File
@@ -2,19 +2,56 @@
// Inbox page (sub-project 6).
//
// Full-bleed dark working surface for the four-lane triage workflow.
// Real implementation lands in T14-T17; this placeholder exists so the
// /inbox route resolves during T13.
// The page itself is a thin wrapper: data via useInboxLanes, layout via
// Lane / InboxHeader. Bulk actions are wired in T18.
// ---------------------------------------------------------------------------
import { Lane, type LaneRow } from "@/components/inbox/Lane";
import { InboxHeader } from "@/components/inbox/InboxHeader";
import { useInboxLanes } from "@/hooks/useInboxLanes";
export default function Inbox() {
const { lanes, loading, error } = useInboxLanes();
if (loading) {
return (
<div
className="min-h-screen p-8 font-mono"
style={{ background: "var(--tt-bg)", color: "var(--tt-ink-dim)" }}
>
loading
</div>
);
}
if (error) {
return (
<div
className="min-h-screen p-8 font-mono"
style={{ background: "var(--tt-bg)", color: "var(--tt-oxblood)" }}
>
error: {error.message}
</div>
);
}
const needEyes =
lanes.rejected.length + lanes.candidates.length + lanes.unmatched.length;
const noop = (_r: LaneRow) => {};
return (
<div
className="min-h-screen p-8"
className="min-h-screen"
style={{ background: "var(--tt-bg)", color: "var(--tt-ink)" }}
>
<p className="font-mono text-sm" style={{ color: "var(--tt-ink-dim)" }}>
Inbox coming up in T14T17
</p>
<InboxHeader needEyesCount={needEyes} doneTodayCount={lanes.done_today.length} />
<main className="p-4 flex gap-4 items-start flex-wrap">
<Lane name="REJECTED" accent="oxblood" rows={lanes.rejected} onRowClick={noop} />
<Lane name="CANDIDATES" accent="amber" rows={lanes.candidates} onRowClick={noop} />
<Lane name="UNMATCHED" accent="ink-blue" rows={lanes.unmatched} onRowClick={noop} />
<Lane name="DONE" accent="muted" rows={lanes.done_today} onRowClick={noop} />
</main>
</div>
);
}