docs(26): create phase plan
This commit is contained in:
182
.planning/phases/26-discovery-landing-page/26-01-PLAN.md
Normal file
182
.planning/phases/26-discovery-landing-page/26-01-PLAN.md
Normal file
@@ -0,0 +1,182 @@
|
||||
---
|
||||
phase: 26-discovery-landing-page
|
||||
plan: 01
|
||||
type: tdd
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/server/services/discovery.service.ts
|
||||
- tests/services/discovery.service.test.ts
|
||||
autonomous: true
|
||||
requirements: [DISC-02, DISC-03, DISC-04, INFR-02]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "getPopularSetups returns public setups ordered by item count descending"
|
||||
- "getRecentGlobalItems returns items ordered by createdAt descending"
|
||||
- "getTrendingCategories returns categories ordered by item count, excluding nulls"
|
||||
- "Cursor pagination returns next page without duplicates"
|
||||
artifacts:
|
||||
- path: "src/server/services/discovery.service.ts"
|
||||
provides: "Discovery feed queries with cursor pagination"
|
||||
exports: ["getPopularSetups", "getRecentGlobalItems", "getTrendingCategories"]
|
||||
- path: "tests/services/discovery.service.test.ts"
|
||||
provides: "Unit tests for all three discovery service functions"
|
||||
min_lines: 100
|
||||
key_links:
|
||||
- from: "src/server/services/discovery.service.ts"
|
||||
to: "src/db/schema.ts"
|
||||
via: "Drizzle query builders using globalItems, setups, setupItems, users tables"
|
||||
pattern: "from\\(globalItems\\)|from\\(setups\\)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the discovery service layer with three query functions: getPopularSetups, getRecentGlobalItems, and getTrendingCategories. All functions use cursor-based pagination per INFR-02 (except categories which use simple limit).
|
||||
|
||||
Purpose: Provides the data layer for the discovery landing page feed sections. TDD approach ensures correct ordering, filtering, and pagination before wiring to routes.
|
||||
Output: `discovery.service.ts` with three exported functions, fully tested.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
@src/db/schema.ts
|
||||
@tests/helpers/db.ts
|
||||
@tests/services/global-item.service.test.ts (pattern reference for test structure)
|
||||
@src/server/services/global-item.service.ts (pattern reference for service structure)
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Discovery service with TDD — popular setups, recent items, trending categories</name>
|
||||
<files>src/server/services/discovery.service.ts, tests/services/discovery.service.test.ts</files>
|
||||
<read_first>
|
||||
- src/db/schema.ts (table definitions: globalItems, setups, setupItems, users)
|
||||
- tests/helpers/db.ts (createTestDb pattern)
|
||||
- tests/services/global-item.service.test.ts (test file structure, insertGlobalItem helper pattern)
|
||||
- src/server/services/global-item.service.ts (service function patterns — how db param is typed, import style)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- getPopularSetups: returns only public setups (isPublic=true), ordered by setupItems count descending then by id descending. Each result includes id, name, createdAt, itemCount (number), creatorName (string|null from users.displayName). Private setups are excluded.
|
||||
- getPopularSetups cursor: given cursor "5_42" (itemCount=5, id=42), returns setups where (itemCount < 5) OR (itemCount === 5 AND id < 42). hasMore is true when rows exceed limit.
|
||||
- getRecentGlobalItems: returns globalItems ordered by createdAt descending. Each result includes all globalItems columns.
|
||||
- getRecentGlobalItems cursor: given cursor ISO timestamp, returns items where createdAt < cursor timestamp. hasMore is true when rows exceed limit.
|
||||
- getTrendingCategories: returns { name: string, itemCount: number }[] ordered by itemCount descending. Excludes rows where globalItems.category IS NULL. No cursor pagination (simple limit).
|
||||
- getTrendingCategories empty: returns empty array when no items have a category set.
|
||||
</behavior>
|
||||
<action>
|
||||
**RED phase — write tests first in `tests/services/discovery.service.test.ts`:**
|
||||
|
||||
Use the same test structure as `global-item.service.test.ts`:
|
||||
- Import `{ beforeEach, describe, expect, it }` from `"bun:test"`
|
||||
- Import schema tables: `globalItems, setups, setupItems, users` from `../../src/db/schema.ts`
|
||||
- Import `createTestDb` from `../helpers/db.ts`
|
||||
- Import service functions from `../../src/server/services/discovery.service.ts`
|
||||
- Type `TestDb = Awaited<ReturnType<typeof createTestDb>>`
|
||||
|
||||
Helper functions needed in test file:
|
||||
```typescript
|
||||
async function insertGlobalItem(db, data: { brand: string; model: string; category?: string }) {
|
||||
const [row] = await db.insert(globalItems).values({ brand: data.brand, model: data.model, category: data.category ?? null }).returning();
|
||||
return row;
|
||||
}
|
||||
async function insertPublicSetup(db, userId: number, name: string, itemIds: number[]) {
|
||||
const [setup] = await db.insert(setups).values({ name, userId, isPublic: true }).returning();
|
||||
// Insert items into the items table first, then setupItems
|
||||
for (const itemId of itemIds) {
|
||||
await db.insert(setupItems).values({ setupId: setup.id, itemId });
|
||||
}
|
||||
return setup;
|
||||
}
|
||||
```
|
||||
|
||||
Note: `setupItems.itemId` references the `items` table, not `globalItems`. So tests need to insert real `items` rows first (use `db.insert(items).values({ name: "Test", categoryId: 1, userId })`) before creating setupItems.
|
||||
|
||||
Write tests for:
|
||||
1. `getPopularSetups` — seed 2 public setups with different item counts, verify order is by count desc
|
||||
2. `getPopularSetups` — seed 1 private setup, verify it's excluded
|
||||
3. `getPopularSetups` — cursor pagination: seed 3 setups, fetch limit=1, verify hasMore=true and nextCursor returned, fetch page 2 with cursor, verify different setup returned
|
||||
4. `getPopularSetups` — includes creatorName from users.displayName (seed user with displayName, verify it appears)
|
||||
5. `getRecentGlobalItems` — seed 3 items with different createdAt, verify order is newest first
|
||||
6. `getRecentGlobalItems` — cursor pagination: fetch limit=1, verify hasMore, fetch page 2 with cursor
|
||||
7. `getTrendingCategories` — seed items in 3 categories with different counts, verify order by count desc
|
||||
8. `getTrendingCategories` — seed item with null category, verify it's excluded from results
|
||||
|
||||
**GREEN phase — create `src/server/services/discovery.service.ts`:**
|
||||
|
||||
Import from drizzle-orm: `count, desc, eq, lt, sql, and, isNotNull`
|
||||
Import schema: `globalItems, setups, setupItems, users`
|
||||
Import types: infer Db type the same way as `global-item.service.ts` does
|
||||
|
||||
Three exported functions:
|
||||
|
||||
`getPopularSetups(db: Db, limit = 6, cursor?: string)`:
|
||||
- Query: SELECT setups.id, setups.name, setups.createdAt, COUNT(setupItems.id) AS itemCount, users.displayName AS creatorName
|
||||
- FROM setups LEFT JOIN setupItems ON setupItems.setupId = setups.id LEFT JOIN users ON users.id = setups.userId
|
||||
- WHERE setups.isPublic = true
|
||||
- GROUP BY setups.id, setups.name, setups.createdAt, users.displayName
|
||||
- ORDER BY itemCount DESC, setups.id DESC
|
||||
- LIMIT limit + 1
|
||||
|
||||
For cursor: parse "itemCount_id" format. Use SQL HAVING or WHERE with subquery. Since Drizzle groupBy with cursor is tricky, use the post-filter approach from RESEARCH.md:
|
||||
- Fetch more rows (limit * 2 + 1 if cursor provided)
|
||||
- Filter in JS: keep rows where (itemCount < cursorCount) OR (itemCount === cursorCount AND id < cursorId)
|
||||
- Slice to limit + 1
|
||||
|
||||
Return `{ items: T[], nextCursor: string | null, hasMore: boolean }` shape:
|
||||
- hasMore = rows.length > limit
|
||||
- items = hasMore ? rows.slice(0, limit) : rows
|
||||
- nextCursor = hasMore ? `${items[items.length-1].itemCount}_${items[items.length-1].id}` : null
|
||||
|
||||
`getRecentGlobalItems(db: Db, limit = 8, cursor?: string)`:
|
||||
- Query: SELECT * FROM globalItems WHERE (cursor ? createdAt < new Date(cursor) : true) ORDER BY createdAt DESC LIMIT limit + 1
|
||||
- Return `{ items, nextCursor, hasMore }` — nextCursor is ISO string of last item's createdAt
|
||||
|
||||
`getTrendingCategories(db: Db, limit = 12)`:
|
||||
- Query: SELECT category AS name, COUNT(id) AS itemCount FROM globalItems WHERE category IS NOT NULL GROUP BY category ORDER BY COUNT(id) DESC LIMIT limit
|
||||
- Return array directly (no cursor pagination per RESEARCH.md open question 3)
|
||||
|
||||
**REFACTOR:** Ensure all functions handle edge cases (empty results, no cursor). Extract shared `buildCursorResponse` helper if patterns are identical.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/services/discovery.service.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- tests/services/discovery.service.test.ts contains `describe("getPopularSetups"` and `describe("getRecentGlobalItems"` and `describe("getTrendingCategories"`
|
||||
- tests/services/discovery.service.test.ts contains at least 8 `it(` calls
|
||||
- src/server/services/discovery.service.ts contains `export async function getPopularSetups(`
|
||||
- src/server/services/discovery.service.ts contains `export async function getRecentGlobalItems(`
|
||||
- src/server/services/discovery.service.ts contains `export async function getTrendingCategories(`
|
||||
- src/server/services/discovery.service.ts contains `isNotNull(globalItems.category)` (null category exclusion)
|
||||
- src/server/services/discovery.service.ts contains `eq(setups.isPublic, true)` (public-only filter)
|
||||
- src/server/services/discovery.service.ts contains `nextCursor` and `hasMore` in return shapes
|
||||
- `bun test tests/services/discovery.service.test.ts` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>All three discovery service functions pass their tests: correct ordering, cursor pagination works for setups and items, categories exclude nulls, and hasMore/nextCursor response shape is correct.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `bun test tests/services/discovery.service.test.ts` — all tests pass
|
||||
- `bun test` — full suite still green (no regressions)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Three exported service functions exist with cursor pagination (setups, items) and simple limit (categories)
|
||||
- All tests pass covering ordering, filtering, cursor, and edge cases
|
||||
- Service functions are pure (take db instance, no HTTP awareness)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/26-discovery-landing-page/26-01-SUMMARY.md`
|
||||
</output>
|
||||
315
.planning/phases/26-discovery-landing-page/26-02-PLAN.md
Normal file
315
.planning/phases/26-discovery-landing-page/26-02-PLAN.md
Normal file
@@ -0,0 +1,315 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
|
||||
<interfaces>
|
||||
<!-- From plan 01: discovery service exports -->
|
||||
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<Env>();
|
||||
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<GlobalItem[]>(`/api/global-items${qs ? `?${qs}` : ""}`),
|
||||
});
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Discovery routes, server registration, and route tests</name>
|
||||
<files>src/server/routes/discovery.ts, src/server/index.ts, tests/routes/discovery.test.ts</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<action>
|
||||
**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/...")`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/routes/discovery.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Client-side React Query hooks for discovery data</name>
|
||||
<files>src/client/hooks/useDiscovery.ts</files>
|
||||
<read_first>
|
||||
- src/client/hooks/useGlobalItems.ts (hook pattern: useQuery, apiGet, interface definitions, queryKey structure)
|
||||
- src/client/lib/api.ts (apiGet signature)
|
||||
</read_first>
|
||||
<action>
|
||||
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`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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`
|
||||
</acceptance_criteria>
|
||||
<done>Three React Query hooks export correctly with proper types, query keys, stale times, and API endpoint URLs matching the server routes.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `bun test tests/routes/discovery.test.ts` — route tests pass
|
||||
- `bun test` — full suite green
|
||||
- `bun run build` — client builds without TypeScript errors
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/26-discovery-landing-page/26-02-SUMMARY.md`
|
||||
</output>
|
||||
477
.planning/phases/26-discovery-landing-page/26-03-PLAN.md
Normal file
477
.planning/phases/26-discovery-landing-page/26-03-PLAN.md
Normal file
@@ -0,0 +1,477 @@
|
||||
---
|
||||
phase: 26-discovery-landing-page
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [26-01]
|
||||
files_modified:
|
||||
- src/client/routes/index.tsx
|
||||
- src/client/components/PublicSetupCard.tsx
|
||||
- src/client/routes/users/$userId.tsx
|
||||
autonomous: false
|
||||
requirements: [DISC-01, DISC-02, DISC-03, DISC-04, DISC-05]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Root URL shows a hero section with a catalog search bar"
|
||||
- "Clicking the search bar opens CatalogSearchOverlay"
|
||||
- "Popular setups section shows setup cards with item counts"
|
||||
- "Recently added items section shows GlobalItemCard components"
|
||||
- "Trending categories section shows category names with item counts"
|
||||
- "Authenticated users see Go to Collection link in hero"
|
||||
- "Anonymous users see the page without login redirect"
|
||||
artifacts:
|
||||
- path: "src/client/routes/index.tsx"
|
||||
provides: "Landing page with hero, popular setups, recent items, trending categories"
|
||||
min_lines: 80
|
||||
- path: "src/client/components/PublicSetupCard.tsx"
|
||||
provides: "Enhanced setup card with optional itemCount and creatorName"
|
||||
contains: "itemCount"
|
||||
key_links:
|
||||
- from: "src/client/routes/index.tsx"
|
||||
to: "src/client/hooks/useDiscovery.ts"
|
||||
via: "imports useDiscoverySetups, useDiscoveryItems, useDiscoveryCategories"
|
||||
pattern: "from.*useDiscovery"
|
||||
- from: "src/client/routes/index.tsx"
|
||||
to: "src/client/stores/uiStore.ts"
|
||||
via: "openCatalogSearch trigger from hero"
|
||||
pattern: "openCatalogSearch"
|
||||
- from: "src/client/routes/index.tsx"
|
||||
to: "src/client/hooks/useAuth.ts"
|
||||
via: "auth state for conditional Go to Collection CTA"
|
||||
pattern: "useAuth"
|
||||
- from: "src/client/routes/index.tsx"
|
||||
to: "src/client/components/GlobalItemCard.tsx"
|
||||
via: "renders catalog items in recent items section"
|
||||
pattern: "GlobalItemCard"
|
||||
- from: "src/client/routes/index.tsx"
|
||||
to: "src/client/components/PublicSetupCard.tsx"
|
||||
via: "renders setup cards in popular setups section"
|
||||
pattern: "PublicSetupCard"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Rewrite the landing page at `/` from a personal dashboard to a public discovery page. Enhance PublicSetupCard with itemCount and creatorName. Build hero section with search trigger, three content feed sections, and conditional auth CTA.
|
||||
|
||||
Purpose: This is the user-facing deliverable — the page visitors see first. Composes existing components with new discovery hooks into the layout specified by D-01 through D-11.
|
||||
Output: Complete landing page at `/`, enhanced PublicSetupCard.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
|
||||
<interfaces>
|
||||
<!-- From plan 02: client hooks -->
|
||||
From src/client/hooks/useDiscovery.ts:
|
||||
```typescript
|
||||
export interface DiscoverySetup {
|
||||
id: number;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
itemCount: number;
|
||||
creatorName: string | null;
|
||||
}
|
||||
export interface DiscoveryCategory {
|
||||
name: string;
|
||||
itemCount: number;
|
||||
}
|
||||
export function useDiscoverySetups(limit?: number): UseQueryResult
|
||||
export function useDiscoveryItems(limit?: number): UseQueryResult
|
||||
export function useDiscoveryCategories(limit?: number): UseQueryResult
|
||||
```
|
||||
|
||||
From src/client/hooks/useAuth.ts:
|
||||
```typescript
|
||||
interface AuthState {
|
||||
user: { id: string; email?: string } | null;
|
||||
authenticated: boolean;
|
||||
}
|
||||
export function useAuth(): UseQueryResult<AuthState>
|
||||
```
|
||||
|
||||
From src/client/stores/uiStore.ts:
|
||||
```typescript
|
||||
openCatalogSearch: (mode: "collection" | "thread") => void;
|
||||
// Access via: const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
|
||||
```
|
||||
|
||||
From src/client/components/GlobalItemCard.tsx:
|
||||
```typescript
|
||||
interface GlobalItemCardProps {
|
||||
id: number; brand: string; model: string; category: string | null;
|
||||
weightGrams: number | null; priceCents: number | null; imageUrl: string | null;
|
||||
}
|
||||
export function GlobalItemCard(props: GlobalItemCardProps): JSX.Element
|
||||
```
|
||||
|
||||
From src/client/components/PublicSetupCard.tsx (current — will be enhanced):
|
||||
```typescript
|
||||
interface PublicSetupCardProps {
|
||||
setup: { id: number; name: string; createdAt: string; };
|
||||
}
|
||||
export function PublicSetupCard({ setup }: PublicSetupCardProps): JSX.Element
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Enhance PublicSetupCard with itemCount and creatorName</name>
|
||||
<files>src/client/components/PublicSetupCard.tsx, src/client/routes/users/$userId.tsx</files>
|
||||
<read_first>
|
||||
- src/client/components/PublicSetupCard.tsx (current component — full content)
|
||||
- src/client/routes/users/$userId.tsx (existing usage of PublicSetupCard — check what shape is passed)
|
||||
</read_first>
|
||||
<action>
|
||||
**Modify `src/client/components/PublicSetupCard.tsx`:**
|
||||
|
||||
Update the `PublicSetupCardProps` interface to add optional fields (per Pitfall 3 — keep optional to avoid breaking existing usages):
|
||||
```typescript
|
||||
interface PublicSetupCardProps {
|
||||
setup: {
|
||||
id: number;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
itemCount?: number; // NEW — optional for backward compat
|
||||
creatorName?: string | null; // NEW — optional
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Update the component JSX to display the new fields when present:
|
||||
|
||||
After the existing `<h3>` (setup name) and before/replacing the `<p>` date line, render:
|
||||
- If `setup.creatorName` is truthy: `<p className="text-xs text-gray-400 mt-1">by {setup.creatorName}</p>`
|
||||
- If `setup.itemCount` is defined and > 0: `<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400">{setup.itemCount} items</span>`
|
||||
- Keep the existing date display
|
||||
|
||||
Layout for the bottom area of the card (below the name):
|
||||
```tsx
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{setup.itemCount != null && setup.itemCount > 0 && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-400">
|
||||
{setup.itemCount} {setup.itemCount === 1 ? "item" : "items"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400">{formattedDate}</p>
|
||||
</div>
|
||||
{setup.creatorName && (
|
||||
<p className="text-xs text-gray-400 mt-1">by {setup.creatorName}</p>
|
||||
)}
|
||||
```
|
||||
|
||||
Add `cursor-pointer` to the Link className (folded todo from CONTEXT.md).
|
||||
|
||||
**Verify `src/client/routes/users/$userId.tsx`:**
|
||||
Read the file to confirm the existing `PublicSetupCard` usage still compiles. Since `itemCount` and `creatorName` are optional, the existing usage passing `{ id, name, createdAt }` will continue to work without changes. No modification needed to this file unless it already passes extra props.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- src/client/components/PublicSetupCard.tsx contains `itemCount?: number`
|
||||
- src/client/components/PublicSetupCard.tsx contains `creatorName?: string | null`
|
||||
- src/client/components/PublicSetupCard.tsx contains `setup.itemCount` (conditional rendering)
|
||||
- src/client/components/PublicSetupCard.tsx contains `setup.creatorName` (conditional rendering)
|
||||
- src/client/components/PublicSetupCard.tsx contains `cursor-pointer`
|
||||
- `bun run build` succeeds without TypeScript errors
|
||||
</acceptance_criteria>
|
||||
<done>PublicSetupCard renders item count and creator name when provided, existing usages compile without changes, cursor-pointer applied.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Rewrite index.tsx as discovery landing page</name>
|
||||
<files>src/client/routes/index.tsx</files>
|
||||
<read_first>
|
||||
- src/client/routes/index.tsx (current dashboard — will be completely rewritten)
|
||||
- src/client/components/GlobalItemCard.tsx (props interface for rendering items)
|
||||
- src/client/components/PublicSetupCard.tsx (enhanced props from Task 1)
|
||||
- src/client/hooks/useDiscovery.ts (hook signatures from plan 02)
|
||||
- src/client/hooks/useAuth.ts (useAuth hook pattern)
|
||||
- src/client/stores/uiStore.ts (openCatalogSearch usage pattern)
|
||||
- src/client/routes/__root.tsx (isDashboard detection — do NOT modify per Pitfall 2)
|
||||
</read_first>
|
||||
<action>
|
||||
**Completely rewrite `src/client/routes/index.tsx`** (per D-03, retire DashboardPage entirely):
|
||||
|
||||
Remove ALL existing imports (DashboardCard, useFormatters, useSetups, useThreads, useTotals).
|
||||
|
||||
New imports:
|
||||
```typescript
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { Search } from "lucide-react";
|
||||
import { GlobalItemCard } from "../components/GlobalItemCard";
|
||||
import { PublicSetupCard } from "../components/PublicSetupCard";
|
||||
import { useAuth } from "../hooks/useAuth";
|
||||
import { useDiscoverySetups, useDiscoveryItems, useDiscoveryCategories } from "../hooks/useDiscovery";
|
||||
import { useUIStore } from "../stores/uiStore";
|
||||
```
|
||||
|
||||
Note: Use `lucide-react` for the Search icon. The project already uses lucide-react (check existing imports). If the project uses the custom `LucideIcon` component instead, use `<LucideIcon name="search" className="w-5 h-5 text-gray-400" />` from `../lib/iconData`.
|
||||
|
||||
**Route export** (same file route, new component):
|
||||
```typescript
|
||||
export const Route = createFileRoute("/")({
|
||||
component: LandingPage,
|
||||
});
|
||||
```
|
||||
|
||||
**LandingPage function** (per D-01, D-02):
|
||||
```typescript
|
||||
function LandingPage() {
|
||||
const { data: auth } = useAuth();
|
||||
const isAuthenticated = !!auth?.user;
|
||||
const openCatalogSearch = useUIStore((s) => s.openCatalogSearch);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<HeroSection isAuthenticated={isAuthenticated} onSearchFocus={() => openCatalogSearch("collection")} />
|
||||
<PopularSetupsSection />
|
||||
<RecentItemsSection />
|
||||
<TrendingCategoriesSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**HeroSection** (per D-01, D-04, D-10, D-11):
|
||||
```typescript
|
||||
function HeroSection({ isAuthenticated, onSearchFocus }: { isAuthenticated: boolean; onSearchFocus: () => void }) {
|
||||
return (
|
||||
<div className="text-center mb-16">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold text-gray-900 mb-2">Discover Gear</h1>
|
||||
<p className="text-gray-500 mb-8">Browse what other people carry</p>
|
||||
<div
|
||||
onClick={onSearchFocus}
|
||||
onKeyDown={(e) => e.key === "Enter" && onSearchFocus()}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="max-w-xl mx-auto flex items-center gap-3 px-4 py-3 bg-white rounded-xl border border-gray-200 hover:border-gray-300 cursor-pointer shadow-sm transition-all"
|
||||
>
|
||||
<Search className="w-5 h-5 text-gray-400 shrink-0" />
|
||||
<span className="text-sm text-gray-400 flex-1 text-left">Search the catalog...</span>
|
||||
</div>
|
||||
{isAuthenticated && (
|
||||
<div className="mt-4">
|
||||
<Link to="/collection" className="text-sm text-gray-600 hover:text-gray-900 underline underline-offset-2 cursor-pointer">
|
||||
Go to Collection
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**PopularSetupsSection** (per D-02, D-05):
|
||||
```typescript
|
||||
function PopularSetupsSection() {
|
||||
const { data, isLoading } = useDiscoverySetups(6);
|
||||
const setups = data?.items ?? [];
|
||||
|
||||
if (!isLoading && setups.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="mb-12">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Popular Setups</h2>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<SectionSkeleton count={6} aspect="none" />
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{setups.map((setup) => (
|
||||
<PublicSetupCard key={setup.id} setup={setup} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**RecentItemsSection** (per D-02, D-06):
|
||||
```typescript
|
||||
function RecentItemsSection() {
|
||||
const { data, isLoading } = useDiscoveryItems(8);
|
||||
const items = data?.items ?? [];
|
||||
|
||||
if (!isLoading && items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="mb-12">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Recently Added</h2>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<SectionSkeleton count={8} aspect="[4/3]" />
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{items.map((item) => (
|
||||
<GlobalItemCard
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
brand={item.brand}
|
||||
model={item.model}
|
||||
category={item.category}
|
||||
weightGrams={item.weightGrams}
|
||||
priceCents={item.priceCents}
|
||||
imageUrl={item.imageUrl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**TrendingCategoriesSection** (per D-02, D-07):
|
||||
```typescript
|
||||
function TrendingCategoriesSection() {
|
||||
const { data, isLoading } = useDiscoveryCategories(12);
|
||||
const categories = data ?? [];
|
||||
|
||||
if (!isLoading && categories.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="mb-12">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Trending Categories</h2>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="h-8 w-24 bg-gray-100 rounded-full animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map((cat) => (
|
||||
<span
|
||||
key={cat.name}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium bg-gray-50 text-gray-700 border border-gray-100 hover:border-gray-200 hover:bg-gray-100 transition-all cursor-pointer"
|
||||
>
|
||||
{cat.name}
|
||||
<span className="text-xs text-gray-400">{cat.itemCount}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**SectionSkeleton** helper (matches CatalogSearchOverlay animate-pulse pattern):
|
||||
```typescript
|
||||
function SectionSkeleton({ count, aspect }: { count: number; aspect: string }) {
|
||||
return (
|
||||
<div className={`grid ${aspect === "none" ? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3" : "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4"} gap-4`}>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<div key={i} className="bg-white rounded-xl border border-gray-100 overflow-hidden animate-pulse">
|
||||
{aspect !== "none" && <div className={`aspect-${aspect} bg-gray-100`} />}
|
||||
<div className="p-4 space-y-2">
|
||||
<div className="h-3 bg-gray-100 rounded w-16" />
|
||||
<div className="h-4 bg-gray-100 rounded w-32" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Ensure all clickable elements have `cursor-pointer` (folded todo from CONTEXT.md).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- src/client/routes/index.tsx does NOT contain `DashboardPage` or `DashboardCard` or `useTotals` (per D-03)
|
||||
- src/client/routes/index.tsx contains `function LandingPage()`
|
||||
- src/client/routes/index.tsx contains `function HeroSection(`
|
||||
- src/client/routes/index.tsx contains `openCatalogSearch("collection")` (per D-04)
|
||||
- src/client/routes/index.tsx contains `useDiscoverySetups` and `useDiscoveryItems` and `useDiscoveryCategories`
|
||||
- src/client/routes/index.tsx contains `"Go to Collection"` (per D-10)
|
||||
- src/client/routes/index.tsx contains `!!auth?.user` or `auth?.user` for auth check (per anti-pattern: do not use auth?.authenticated)
|
||||
- src/client/routes/index.tsx contains `GlobalItemCard` import
|
||||
- src/client/routes/index.tsx contains `PublicSetupCard` import
|
||||
- src/client/routes/index.tsx contains `cursor-pointer` on the search bar div
|
||||
- src/client/routes/index.tsx contains `animate-pulse` (loading skeletons)
|
||||
- `bun run build` succeeds without errors
|
||||
</acceptance_criteria>
|
||||
<done>Landing page renders hero with search trigger, three feed sections with loading skeletons and empty-state handling, authenticated CTA, and all clickable elements have cursor-pointer. Build succeeds.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Visual verification of discovery landing page</name>
|
||||
<files>src/client/routes/index.tsx</files>
|
||||
<action>
|
||||
No code changes — this is a visual verification checkpoint. The executor should start the dev server with `bun run dev` and present the verification steps to the user.
|
||||
</action>
|
||||
<what-built>
|
||||
Complete discovery landing page replacing the personal dashboard at /. Features:
|
||||
- Hero section with "Discover Gear" heading and catalog search bar trigger
|
||||
- Popular Setups section with enhanced cards (item count, creator name)
|
||||
- Recently Added Items section with GlobalItemCard components
|
||||
- Trending Categories section with category pills
|
||||
- Conditional "Go to Collection" link for authenticated users
|
||||
- Loading skeletons for all sections
|
||||
- Empty state handling (sections hide when no data)
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Run `bun run dev` and open http://localhost:5173/ in a browser
|
||||
2. Verify the hero section shows "Discover Gear" heading with search bar
|
||||
3. Click the search bar — it should open the full CatalogSearchOverlay
|
||||
4. Verify sections appear below: Popular Setups, Recently Added, Trending Categories
|
||||
5. If there is seed data, verify cards show correct information (item counts, creator names, images)
|
||||
6. If no data exists, verify empty sections are hidden gracefully (no broken/empty grids)
|
||||
7. Log in and verify "Go to Collection" link appears in hero area
|
||||
8. Click "Go to Collection" — should navigate to /collection
|
||||
9. Check responsive behavior: resize browser to mobile width, verify single-column layout
|
||||
10. Verify all clickable elements show pointer cursor on hover
|
||||
</how-to-verify>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>User has visually verified the landing page renders correctly with all sections, search trigger works, auth CTA appears for logged-in users, and responsive layout is correct.</done>
|
||||
<resume-signal>Type "approved" or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `bun run build` — builds without errors
|
||||
- Visual inspection of landing page at localhost:5173
|
||||
- CatalogSearchOverlay opens from hero search bar
|
||||
- Authenticated user sees "Go to Collection" link
|
||||
- Anonymous user sees content immediately
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Root URL shows discovery landing page (not personal dashboard)
|
||||
- Hero search bar triggers CatalogSearchOverlay on click
|
||||
- Three content sections render with real data or hide when empty
|
||||
- PublicSetupCard displays item count and creator name
|
||||
- Authenticated users see "Go to Collection" CTA in hero
|
||||
- All clickable elements have cursor-pointer
|
||||
- Build succeeds
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/26-discovery-landing-page/26-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user