Files
GearBox/.planning/phases/26-discovery-landing-page/26-02-PLAN.md

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
26-discovery-landing-page 02 execute 2
26-01
src/server/routes/discovery.ts
src/server/index.ts
src/client/hooks/useDiscovery.ts
tests/routes/discovery.test.ts
true
DISC-02
DISC-03
DISC-04
INFR-02
truths artifacts key_links
GET /api/discovery/setups returns popular setups for anonymous users
GET /api/discovery/items returns recent catalog items for anonymous users
GET /api/discovery/categories returns trending categories for anonymous users
All discovery endpoints accept limit and cursor query params
Discovery endpoints are rate-limited with browseTier
path provides exports
src/server/routes/discovery.ts Hono route handlers for three discovery endpoints
discoveryRoutes
path provides exports
src/client/hooks/useDiscovery.ts React Query hooks for landing page data fetching
useDiscoverySetups
useDiscoveryItems
useDiscoveryCategories
path provides min_lines
tests/routes/discovery.test.ts Route-level integration tests for discovery endpoints 50
from to via pattern
src/server/routes/discovery.ts src/server/services/discovery.service.ts imports getPopularSetups, getRecentGlobalItems, getTrendingCategories from.*discovery.service
from to via pattern
src/server/index.ts src/server/routes/discovery.ts app.route registration and auth skip discoveryRoutes|/api/discovery
from to via pattern
src/client/hooks/useDiscovery.ts /api/discovery apiGet fetch calls apiGet.*api/discovery
Wire the discovery service to HTTP endpoints and create client-side React Query hooks. Register routes in server/index.ts with public access (auth skip) and browseTier rate limiting.

Purpose: Makes the discovery feed data accessible to the landing page UI via three REST endpoints and three React Query hooks. Output: Working endpoints at /api/discovery/{setups,items,categories}, matching client hooks, route-level tests.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/26-discovery-landing-page/26-CONTEXT.md @.planning/phases/26-discovery-landing-page/26-RESEARCH.md @.planning/phases/26-discovery-landing-page/26-01-SUMMARY.md From src/server/services/discovery.service.ts: ```typescript export async function getPopularSetups(db: Db, limit?: number, cursor?: string): Promise<{ items: PopularSetup[], nextCursor: string | null, hasMore: boolean }> export async function getRecentGlobalItems(db: Db, limit?: number, cursor?: string): Promise<{ items: GlobalItemRow[], nextCursor: string | null, hasMore: boolean }> export async function getTrendingCategories(db: Db, limit?: number): Promise<{ name: string, itemCount: number }[]> ```

From src/server/routes/global-items.ts (pattern reference):

type Env = { Variables: { db?: any } };
const app = new Hono<Env>();
app.get("/", async (c) => { ... });
export { app as globalItemRoutes };

From src/server/index.ts (auth skip pattern, lines 151-170):

// Skip public global-items endpoint (GET /api/global-items)
if (c.req.path.startsWith("/api/global-items") && c.req.method === "GET")
  return next();

From src/server/index.ts (rate limit pattern, lines 126-134):

app.use("/api/global-items", async (c, next) => {
  if (c.req.method === "GET" && !c.req.path.match(/^\/api\/global-items\/\d+$/))
    return browseTier(c, next);
  return next();
});

From src/client/hooks/useGlobalItems.ts (hook pattern):

import { useQuery } from "@tanstack/react-query";
import { apiGet } from "../lib/api";
export function useGlobalItems(query?: string, tags?: string[]) {
  return useQuery({
    queryKey: ["global-items", query ?? "", tags ?? []],
    queryFn: () => apiGet<GlobalItem[]>(`/api/global-items${qs ? `?${qs}` : ""}`),
  });
}
Task 1: Discovery routes, server registration, and route tests src/server/routes/discovery.ts, src/server/index.ts, tests/routes/discovery.test.ts - src/server/routes/global-items.ts (exact Hono route pattern to replicate) - src/server/index.ts (auth skip list at lines 151-170, rate limit setup at lines 120-148, route registration at lines 173-183) - tests/routes/global-items.test.ts (route test pattern: createTestApp, middleware setup, request format) - src/server/services/discovery.service.ts (function signatures from plan 01) **Create `src/server/routes/discovery.ts`:**
Follow the exact pattern of `global-items.ts`:
```typescript
import { Hono } from "hono";
import { getPopularSetups, getRecentGlobalItems, getTrendingCategories } from "../services/discovery.service.ts";

type Env = { Variables: { db?: any } };
const app = new Hono<Env>();
```

Three GET handlers:

`app.get("/setups", ...)`:
- Parse query params: `limit` (parseInt, default 6, max 50), `cursor` (string, optional)
- Call `getPopularSetups(db, limit, cursor)`
- Return `c.json(result)` — result already has `{ items, nextCursor, hasMore }` shape

`app.get("/items", ...)`:
- Parse query params: `limit` (parseInt, default 8, max 50), `cursor` (string, optional)
- Call `getRecentGlobalItems(db, limit, cursor)`
- Return `c.json(result)`

`app.get("/categories", ...)`:
- Parse query params: `limit` (parseInt, default 12, max 50)
- Call `getTrendingCategories(db, limit)`
- Return `c.json(result)` — result is array directly

Export: `export { app as discoveryRoutes };`

**Modify `src/server/index.ts`:**

1. Add import at top (after line 14, near other route imports):
   `import { discoveryRoutes } from "./routes/discovery.ts";`

2. Add rate limiting for discovery endpoints (after line 134, in the browse tier section):
   ```typescript
   app.use("/api/discovery/*", async (c, next) => {
     if (c.req.method === "GET") return browseTier(c, next);
     return next();
   });
   ```

3. Add auth skip in the auth middleware block (after line 167, before the requireAuth call):
   ```typescript
   // Skip public discovery endpoints (GET /api/discovery/*)
   if (c.req.path.startsWith("/api/discovery") && c.req.method === "GET")
     return next();
   ```

4. Add route registration (after line 183, near other app.route calls):
   `app.route("/api/discovery", discoveryRoutes);`

**Create `tests/routes/discovery.test.ts`:**

Follow the exact test pattern from `global-items.test.ts`:
```typescript
import { beforeEach, describe, expect, it } from "bun:test";
import { Hono } from "hono";
import { globalItems, items, setups, setupItems } from "../../src/db/schema.ts";
import { discoveryRoutes } from "../../src/server/routes/discovery.ts";
import { createTestDb } from "../helpers/db.ts";

async function createTestApp() {
  const { db, userId } = await createTestDb();
  const app = new Hono();
  app.use("*", async (c, next) => {
    c.set("db", db);
    await next();
  });
  // Note: NO userId set — discovery endpoints don't need auth
  app.route("/api/discovery", discoveryRoutes);
  return { app, db, userId };
}
```

Tests (minimum 6):
1. `GET /api/discovery/setups` returns 200 with `{ items, nextCursor, hasMore }` shape
2. `GET /api/discovery/items` returns 200 with `{ items, nextCursor, hasMore }` shape
3. `GET /api/discovery/categories` returns 200 with array shape
4. `GET /api/discovery/setups?limit=1` respects limit param
5. `GET /api/discovery/items?limit=1&cursor=<timestamp>` pagination works
6. `GET /api/discovery/categories?limit=2` respects limit param

For each test, seed appropriate data using db.insert() then make fetch requests via `app.request("/api/discovery/...")`.
cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/routes/discovery.test.ts - src/server/routes/discovery.ts contains `app.get("/setups"` and `app.get("/items"` and `app.get("/categories"` - src/server/routes/discovery.ts contains `export { app as discoveryRoutes }` - src/server/index.ts contains `import { discoveryRoutes }` from `"./routes/discovery.ts"` - src/server/index.ts contains `app.route("/api/discovery", discoveryRoutes)` - src/server/index.ts contains `c.req.path.startsWith("/api/discovery")` in auth skip section - src/server/index.ts contains `"/api/discovery/*"` in rate limit section with `browseTier` - tests/routes/discovery.test.ts contains at least 6 `it(` calls - `bun test tests/routes/discovery.test.ts` exits 0 Three discovery endpoints respond to GET requests with correct JSON shapes, anonymous access works (no auth required), rate limiting is applied, and route tests pass. Task 2: Client-side React Query hooks for discovery data src/client/hooks/useDiscovery.ts - src/client/hooks/useGlobalItems.ts (hook pattern: useQuery, apiGet, interface definitions, queryKey structure) - src/client/lib/api.ts (apiGet signature) Create `src/client/hooks/useDiscovery.ts` with three named exports.
**Type definitions** at top of file:
```typescript
export interface DiscoverySetup {
  id: number;
  name: string;
  createdAt: string;
  itemCount: number;
  creatorName: string | null;
}

export interface DiscoveryCategory {
  name: string;
  itemCount: number;
}

interface CursorPage<T> {
  items: T[];
  nextCursor: string | null;
  hasMore: boolean;
}
```

For GlobalItem type, import from useGlobalItems or re-define inline matching the existing `GlobalItem` interface in `useGlobalItems.ts` (id, brand, model, category, weightGrams, priceCents, imageUrl, description, sourceUrl, imageCredit, imageSourceUrl, createdAt — all as they appear in that file).

**Three hooks:**

`useDiscoverySetups(limit = 6)`:
- queryKey: `["discovery", "setups", limit]`
- queryFn: `apiGet<CursorPage<DiscoverySetup>>(`/api/discovery/setups?limit=${limit}`)`
- staleTime: `2 * 60 * 1000` (2 minutes)

`useDiscoveryItems(limit = 8)`:
- queryKey: `["discovery", "items", limit]`
- queryFn: `apiGet<CursorPage<GlobalItem>>(`/api/discovery/items?limit=${limit}`)`
- staleTime: `2 * 60 * 1000` (2 minutes)

`useDiscoveryCategories(limit = 12)`:
- queryKey: `["discovery", "categories", limit]`
- queryFn: `apiGet<DiscoveryCategory[]>(`/api/discovery/categories?limit=${limit}`)`
- staleTime: `5 * 60 * 1000` (5 minutes — categories change rarely)

Import `useQuery` from `@tanstack/react-query` and `apiGet` from `../lib/api`.
cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -20 - src/client/hooks/useDiscovery.ts contains `export function useDiscoverySetups(` - src/client/hooks/useDiscovery.ts contains `export function useDiscoveryItems(` - src/client/hooks/useDiscovery.ts contains `export function useDiscoveryCategories(` - src/client/hooks/useDiscovery.ts contains `export interface DiscoverySetup` - src/client/hooks/useDiscovery.ts contains `export interface DiscoveryCategory` - src/client/hooks/useDiscovery.ts contains `staleTime: 2 * 60 * 1000` (for setups and items) - src/client/hooks/useDiscovery.ts contains `staleTime: 5 * 60 * 1000` (for categories) - src/client/hooks/useDiscovery.ts contains `queryKey: ["discovery",` for all three hooks - src/client/hooks/useDiscovery.ts contains `apiGet` import from `../lib/api` Three React Query hooks export correctly with proper types, query keys, stale times, and API endpoint URLs matching the server routes. - `bun test tests/routes/discovery.test.ts` — route tests pass - `bun test` — full suite green - `bun run build` — client builds without TypeScript errors

<success_criteria>

  • Three GET endpoints at /api/discovery/{setups,items,categories} respond to anonymous requests
  • Endpoints are rate-limited with browseTier
  • Three React Query hooks ready for consumption by the landing page
  • Route-level tests verify response shapes and status codes </success_criteria>
After completion, create `.planning/phases/26-discovery-landing-page/26-02-SUMMARY.md`