13 KiB
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 |
|
|
true |
|
|
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}` : ""}`),
});
}
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 && npx tsc --noEmit src/client/hooks/useDiscovery.ts 2>&1 || echo "Checking with bun build" && bun build src/client/hooks/useDiscovery.ts --outdir /tmp/check-discovery --external react --external @tanstack/react-query 2>&1
- 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>