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

Merged opened this

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

Merged

scout/forge:scout/4110-an-empty-explore-with-no-query-says-noth → main 1 commit fork

  • Merged. scout/4110-an-empty-explore-with-no-query-says-noth was merged into main as b253608.

Review

Sign in to review this change.
2 files changed +77 −5
src/web/studio/explore.ts +22 −5
@@ -62,6 +62,27 @@ function rowHtml(row: ExploreRow): Html {
6262 </li>`;
6363 }
6464
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
6586 // GET /explore
6687 export async function explorePage(ctx: Ctx): Promise<Response> {
6788 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
@@ -123,11 +144,7 @@ export async function explorePage(ctx: Ctx): Promise<Response> {
123144 >
124145 </div>
125146 ${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)
131148 : listCard(rows.map(rowHtml))}`;
132149 const response = page({ ctx, title: "Explore", body });
133150 notePhase(ctx.request, "render", performance.now() - rendering);
test/explore.test.ts +55
@@ -267,3 +267,58 @@ describe("explore", () => {
267267 expect(asAlice).not.toContain('href="/alice/loved/make-it-yours"');
268268 });
269269 });
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs