--- phase: 26-discovery-landing-page plan: 02 type: execute wave: 2 depends_on: [26-01] files_modified: - src/server/routes/discovery.ts - src/server/index.ts - src/client/hooks/useDiscovery.ts - tests/routes/discovery.test.ts autonomous: true requirements: [DISC-02, DISC-03, DISC-04, INFR-02] must_haves: truths: - "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" artifacts: - path: "src/server/routes/discovery.ts" provides: "Hono route handlers for three discovery endpoints" exports: ["discoveryRoutes"] - path: "src/client/hooks/useDiscovery.ts" provides: "React Query hooks for landing page data fetching" exports: ["useDiscoverySetups", "useDiscoveryItems", "useDiscoveryCategories"] - path: "tests/routes/discovery.test.ts" provides: "Route-level integration tests for discovery endpoints" min_lines: 50 key_links: - from: "src/server/routes/discovery.ts" to: "src/server/services/discovery.service.ts" via: "imports getPopularSetups, getRecentGlobalItems, getTrendingCategories" pattern: "from.*discovery\\.service" - from: "src/server/index.ts" to: "src/server/routes/discovery.ts" via: "app.route registration and auth skip" pattern: "discoveryRoutes|/api/discovery" - from: "src/client/hooks/useDiscovery.ts" to: "/api/discovery" via: "apiGet fetch calls" pattern: "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. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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): ```typescript type Env = { Variables: { db?: any } }; const app = new Hono(); app.get("/", async (c) => { ... }); export { app as globalItemRoutes }; ``` From src/server/index.ts (auth skip pattern, lines 151-170): ```typescript // 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): ```typescript 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): ```typescript 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(`/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(); ``` 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=` 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 { 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>(`/api/discovery/setups?limit=${limit}`)` - staleTime: `2 * 60 * 1000` (2 minutes) `useDiscoveryItems(limit = 8)`: - queryKey: `["discovery", "items", limit]` - queryFn: `apiGet>(`/api/discovery/items?limit=${limit}`)` - staleTime: `2 * 60 * 1000` (2 minutes) `useDiscoveryCategories(limit = 12)`: - queryKey: `["discovery", "categories", limit]` - queryFn: `apiGet(`/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 - 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 After completion, create `.planning/phases/26-discovery-landing-page/26-02-SUMMARY.md`