Public: anyone can read this; the crew can change it.

The /contribute page speaks the home page's language #9843

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/9824-the-contribute-page-speaks-the-home-page → main 3 commits fork 2 threads · 2 unresolved

  • Merged. scout/9824-the-contribute-page-speaks-the-home-page was merged into main as e8b5b76.

Review

Sign in to review this change.
2 files changed +76 −13
src/web/pages/contribute.ts +12 −13
@@ -9,7 +9,6 @@ import type { Ctx } from "../../http/context.ts";
99 import { welcome } from "../../system/telemetry.ts";
1010 import { html } from "../html.ts";
1111 import { page } from "../layout.ts";
12 import { card } from "../ui.ts";
1312
1413 // GET /contribute
1514 export function contributePage(ctx: Ctx): Response {
@@ -22,20 +21,20 @@ export function contributePage(ctx: Ctx): Response {
2221 <p class="muted">${w.forge}</p>
2322 </div>
2423 <p id="bottleneck"><strong>Where it hurts now:</strong> ${w.bottleneck.says}</p>
25 ${card(
26 w.issues.length === 0
27 ? html`<div class="card-body"><p class="muted">No open issue carries the “${w.welcomeLabel}” label right now.</p></div>`
24 <section class="stack" aria-labelledby="welcome-h">
outdated

Principle 3 (generous rhythm): these sections carry no spacing from the design system, whereas the DESIGN.md worked example uses <section class="stack">. Give the two sections the existing stack rhythm (a wrapping <div class="stack"> or class on each) rather than relying on default margins.

25 <h2 id="welcome-h">Issues that welcome outside work</h2>
26 ${w.issues.length === 0
27 ? html`<p class="muted">No open issue carries the “${w.welcomeLabel}” label right now.</p>`
2828 : html`<ul class="list">${w.issues.map(
2929 (i) => html`<li class="row"><a href="${i.path}">${i.repo}#${String(i.number)}</a> ${i.title}</li>`,
30 )}</ul>`,
31 { head: html`<h2>Issues that welcome outside work</h2>`, pad: false },
32 )}
33 ${card(
34 html`<div class="card-body"><ol id="propose">${w.propose.map((s) => html`<li>${s}</li>`)}</ol>
35 <p class="muted small">${w.telemetry}</p>
36 <p class="muted small">For agents: <span class="mono">GET /api/v1/contribute</span>, or the MCP tool <span class="mono">contribute</span>.</p></div>`,
37 { head: html`<h2>How to propose</h2>` },
38 )}`;
30 )}</ul>`}
31 </section>
32 <section class="stack" aria-labelledby="propose-h">
outdated

Same as above (principle 3): the How to propose section needs the stack rhythm too.

33 <h2 id="propose-h">How to propose</h2>
34 <ol id="propose">${w.propose.map((s) => html`<li>${s}</li>`)}</ol>
35 <p class="muted small">${w.telemetry}</p>
36 <p class="muted small">For agents: <span class="mono">GET /api/v1/contribute</span>, or the MCP tool <span class="mono">contribute</span>.</p>
37 </section>`;
3938 const response = page({ ctx, title: "Contribute", body });
4039 notePhase(ctx.request, "render", performance.now() - rendering);
4140 return response;
test/contribute-language.test.ts added +64
@@ -0,0 +1,64 @@
1 /** /contribute speaks the home page's language (#9824): one heading, plain sections, no card. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createRepo } from "../src/actions/repos.ts";
7 import { createApp } from "../src/app.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { serve } from "../src/http/server.ts";
10
11 let dir = "";
12 let app: ReturnType<typeof createApp>;
13 let servers: Awaited<ReturnType<typeof serve>>;
14
15 beforeAll(async () => {
16 dir = await mkdtemp(join(tmpdir(), "forge-contribute-lang-"));
17 app = createApp({ dataDir: dir, port: 0 });
18 servers = await serve(app);
19 });
20 afterAll(async () => {
21 servers.stop();
22 app.close();
23 await rm(dir, { recursive: true, force: true });
24 });
25
26 const main = (page: string): string => page.slice(page.indexOf("<main"), page.indexOf("</main>"));
27
28 async function contribute(): Promise<string> {
29 const res = await fetch(servers.url + "/contribute");
30 expect(res.status).toBe(200);
31 return main(await res.text());
32 }
33
34 function plain(page: string): void {
35 expect(page.match(/<h1[\s>]/g)?.length).toBe(1);
36 expect(page).not.toContain('class="card');
37 expect(page).not.toContain("card-body");
38 expect(page).toContain('<ol id="propose">');
39 expect(page).toContain("GET /api/v1/contribute");
40 }
41
42 test("signed out with no welcomed issue: the empty sentence and the steps, no card", async () => {
43 const page = await contribute();
44 plain(page);
45 expect(page).toContain("No open issue carries");
46 });
47
48 test("signed out with a welcomed issue: its link and the steps, no card", async () => {
49 const u = await createUser(app.db, {
50 origin: "seed", trusted: true, username: "alice", email: "[email protected]", password: "a-good-passphrase", adminIfFirst: true,
51 });
52 if (!u.ok) throw new Error(u.error);
53 const repo = await createRepo(app, u.value, { name: "garden", visibility: "public" });
54 if (!repo.ok) throw new Error(String(repo.error));
55 const rid = repo.value.id;
56 app.db.query("INSERT INTO label (id, repo_id, name) VALUES ('LBLW', ?1, 'help wanted')").run(rid);
57 app.db.query("INSERT INTO issue (id, repo_id, number, kind, title, author_id) VALUES ('ISSW', ?1, 1, 'issue', 'tidy the herbs', ?2)").run(rid, u.value.id);
58 app.db.query("INSERT INTO issue_label (issue_id, label_id) VALUES ('ISSW', 'LBLW')").run();
59 const page = await contribute();
60 plain(page);
61 expect(page).toContain('href="/alice/garden/issues/1"');
62 expect(page).toContain("tidy the herbs");
63 expect(page).not.toContain("No open issue carries");
64 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs