Public: anyone can read this; the crew can change it.
An empty /explore with no query says nothing is public yet and how to make the first app, not 'clear the line' #4121
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4110-an-empty-explore-with-no-query-says-noth → main 1 commit fork
2 files
changed +77
−5
src/web/studio/explore.ts +22 −5
| @@ -62,6 +62,27 @@ function rowHtml(row: ExploreRow): Html { | ||
| 62 | 62 | </li>`; |
| 63 | 63 | } |
| 64 | 64 | |
| 65 | /** Nothing to list: a bare line means nothing is public yet, not a miss. */ | |
| 66 | function emptyState(signedIn: boolean, query: ExploreQuery): Html { | |
| 67 | if (exploreLine(query) !== "") | |
| 68 | return empty({ | |
| 69 | icon: "search", | |
| 70 | title: "Nothing matches", | |
| 71 | note: "Clear the line to see everything you can.", | |
| 72 | }); | |
| 73 | return empty({ | |
| 74 | icon: "search", | |
| 75 | title: "Nothing public yet", | |
| 76 | note: "Make the first app and it shows up here.", | |
| 77 | action: signedIn | |
| 78 | ? btn("Make an app", { href: "/apps/new", variant: "primary" }) | |
| 79 | : btn("Sign in to make one", { | |
| 80 | href: "/signin?next=/apps/new", | |
| 81 | variant: "primary", | |
| 82 | }), | |
| 83 | }); | |
| 84 | } | |
| 85 | ||
| 65 | 86 | // GET /explore |
| 66 | 87 | export async function explorePage(ctx: Ctx): Promise<Response> { |
| 67 | 88 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); |
| @@ -123,11 +144,7 @@ export async function explorePage(ctx: Ctx): Promise<Response> { | ||
| 123 | 144 | > |
| 124 | 145 | </div> |
| 125 | 146 | ${rows.length === 0 |
| 126 | ? empty({ | |
| 127 | icon: "search", | |
| 128 | title: "Nothing matches", | |
| 129 | note: "Clear the line to see everything you can.", | |
| 130 | }) | |
| 147 | ? emptyState(ctx.auth.user !== undefined, query) | |
| 131 | 148 | : listCard(rows.map(rowHtml))}`; |
| 132 | 149 | const response = page({ ctx, title: "Explore", body }); |
| 133 | 150 | notePhase(ctx.request, "render", performance.now() - rendering); |
test/explore.test.ts +55
| @@ -267,3 +267,58 @@ describe("explore", () => { | ||
| 267 | 267 | expect(asAlice).not.toContain('href="/alice/loved/make-it-yours"'); |
| 268 | 268 | }); |
| 269 | 269 | }); |
| 270 | ||
| 271 | describe("explore on a forge with nothing public", () => { | |
| 272 | let dataDir: string; | |
| 273 | let app: App; | |
| 274 | let servers: Servers; | |
| 275 | let token: string; | |
| 276 | ||
| 277 | beforeAll(async () => { | |
| 278 | dataDir = await mkdtemp(join(tmpdir(), "forge-explore-empty-")); | |
| 279 | app = createApp({ dataDir, port: 0 }); | |
| 280 | servers = await serve(app); | |
| 281 | const made = await createUser(app.db, { | |
| 282 | origin: "seed", | |
| 283 | username: "lone", | |
| 284 | email: "[email protected]", | |
| 285 | password: "a-good-passphrase", | |
| 286 | adminIfFirst: true, | |
| 287 | }); | |
| 288 | if (!made.ok) throw new Error(made.error); | |
| 289 | token = createToken(app.db, made.value.id, "ro", "ro").token; | |
| 290 | }, 30_000); | |
| 291 | ||
| 292 | afterAll(async () => { | |
| 293 | servers.stop(); | |
| 294 | app.close(); | |
| 295 | await rm(dataDir, { recursive: true, force: true }); | |
| 296 | }); | |
| 297 | ||
| 298 | const get = async (path: string, signedIn: boolean) => | |
| 299 | ( | |
| 300 | await fetch(`${servers.url}${path}`, { | |
| 301 | headers: signedIn ? { authorization: `Bearer ${token}` } : {}, | |
| 302 | }) | |
| 303 | ).text(); | |
| 304 | ||
| 305 | test("no query says nothing is public yet and how to make the first app", async () => { | |
| 306 | const out = await get("/explore", false); | |
| 307 | expect(out).toContain("Nothing public yet"); | |
| 308 | expect(out).toContain('href="/signin?next=/apps/new"'); | |
| 309 | expect(out).not.toContain("Clear the line"); | |
| 310 | const mine = await get("/explore", true); | |
| 311 | expect(mine).toContain("Nothing public yet"); | |
| 312 | expect(mine).toContain('href="/apps/new"'); | |
| 313 | expect(mine).not.toContain("Sign in to make one"); | |
| 314 | }); | |
| 315 | ||
| 316 | test("a query with no hits still says nothing matches", async () => { | |
| 317 | for (const signedIn of [false, true]) { | |
| 318 | const out = await get(`/explore?q=${encodeURIComponent("zzz-no-such")}`, signedIn); | |
| 319 | expect(out).toContain("Nothing matches"); | |
| 320 | expect(out).toContain("Clear the line"); | |
| 321 | expect(out).not.toContain("Nothing public yet"); | |
| 322 | } | |
| 323 | }); | |
| 324 | }); |
new above
offline