docs(02-02): complete follow/block/report plan summary
- SOCL-01/02/03 and SAFE-01/SAFE-02 all implemented and tested - 21 new tests green (follows: 10, blocks: 7, reports: 7) - tsc exits 0; vitest 36 passed
This commit is contained in:
1 parent
7f969a7b33
commit
bdd676f19b
1 file changed
+163
@@ -0,0 +1,163 @@
|
||||
---
|
||||
phase: "02"
|
||||
plan: "02"
|
||||
subsystem: social-graph-safety
|
||||
tags: [trpc, prisma, follows, blocks, reports, optimistic-ui, tdd, vitest, shadcn, social-graph]
|
||||
dependency_graph:
|
||||
requires:
|
||||
- Phase 02 Plan 01 (Prisma Follow/Block/Report models, router stubs, redis singleton, feed-helpers.backfillOnFollow)
|
||||
provides:
|
||||
- followsRouter: create/delete/isFollowing/listFollowers/listFollowing (SOCL-01/02/03)
|
||||
- blocksRouter: create/list with ownership assertions (SAFE-01)
|
||||
- reportsRouter: create with enum validation (SAFE-02)
|
||||
- FollowButton: optimistic follow/unfollow with AlertDialog confirmation
|
||||
- PetListItem: row component for followers/following lists
|
||||
- BlockDialog: AlertDialog block confirmation (SAFE-01)
|
||||
- ReportSheet: Sheet with RadioGroup reason selector (SAFE-02)
|
||||
- /pets/[petId]/followers page (SOCL-03)
|
||||
- /pets/[petId]/following page (SOCL-03)
|
||||
- Pet profile stats row + ProfileActions (FollowButton + options menu)
|
||||
affects:
|
||||
- feed.getFeed (02-03+): blocksRouter.list provides blockedPetIds for feed hydration filter
|
||||
- Pet profile page: stats row links, FollowButton, BlockDialog, ReportSheet wired
|
||||
tech_stack:
|
||||
added: []
|
||||
patterns:
|
||||
- TDD RED/GREEN per task (follows tests → implements; blocks/reports tests → implements)
|
||||
- TanStack Query optimistic update: onMutate cancel+setData, onError revert+toast, onSettled invalidate
|
||||
- RSC shell + client list component pattern (FollowersList/FollowingList read ActivePetContext)
|
||||
- assertPetOwnership duplicated per router (no shared module — follows peers.ts pattern)
|
||||
- prisma.follow.delete by composite key `followerPetId_followeePetId`
|
||||
- ReportReason z.nativeEnum validation; both-null target rejection via TRPCError BAD_REQUEST
|
||||
key_files:
|
||||
created:
|
||||
- src/components/social/FollowButton.tsx
|
||||
- src/components/social/PetListItem.tsx
|
||||
- src/components/safety/BlockDialog.tsx
|
||||
- src/components/safety/ReportSheet.tsx
|
||||
- src/app/(app)/pets/[petId]/followers/page.tsx
|
||||
- src/app/(app)/pets/[petId]/followers/FollowersList.tsx
|
||||
- src/app/(app)/pets/[petId]/following/page.tsx
|
||||
- src/app/(app)/pets/[petId]/following/FollowingList.tsx
|
||||
- src/app/(app)/pets/[petId]/ProfileActions.tsx
|
||||
modified:
|
||||
- src/trpc/routers/follows.ts (stub → full implementation)
|
||||
- src/trpc/routers/blocks.ts (stub → full implementation)
|
||||
- src/trpc/routers/reports.ts (stub → full implementation)
|
||||
- src/__tests__/follows.test.ts (it.todo scaffold → 10 passing tests)
|
||||
- src/__tests__/blocks.test.ts (it.todo scaffold → 7 passing tests)
|
||||
- src/__tests__/reports.test.ts (it.todo scaffold → 7 passing tests)
|
||||
- src/app/(app)/pets/[petId]/page.tsx (Phase 1 profile → Phase 2: stats row + ProfileActions)
|
||||
decisions:
|
||||
- "assertPetOwnership duplicated in each router (follows, blocks, reports) mirroring pets.ts pattern — no shared helper module to avoid coupling"
|
||||
- "FollowButton uses useTRPC().follows.isFollowing.queryOptions() + useQuery() for follow status query"
|
||||
- "ProfileActions client component reads ActivePetContext to resolve followerPetId — RSC cannot read localStorage"
|
||||
- "FollowersList/FollowingList as separate client components to avoid making the RSC page shell a client component"
|
||||
- "Test caller context uses `prisma: mockPrisma as any` to avoid TS2322 type conflict between MockPrismaClient and PrismaClient (Rule 1 fix)"
|
||||
metrics:
|
||||
duration: "~8 minutes"
|
||||
completed: "2026-06-07T10:38:00Z"
|
||||
tasks_completed: 3
|
||||
tasks_total: 3
|
||||
files_created: 9
|
||||
files_modified: 7
|
||||
---
|
||||
|
||||
# Phase 02 Plan 02: Follow/Block/Report Summary
|
||||
|
||||
**One-liner:** followsRouter/blocksRouter/reportsRouter fully implemented with TDD (21 tests green) + optimistic FollowButton + PetListItem + BlockDialog + ReportSheet + followers/following list pages + profile stats row wired
|
||||
|
||||
## What Was Built
|
||||
|
||||
**Task 1 — followsRouter + tests (SOCL-01/02/03):**
|
||||
- `follows.create`: assertPetOwnership on followerPetId + self-follow BAD_REQUEST + `prisma.follow.create` + `backfillOnFollow(redis, followerPetId, followeePetId, prisma)`
|
||||
- `follows.delete`: assertPetOwnership + `prisma.follow.delete` by composite key `followerPetId_followeePetId`
|
||||
- `follows.isFollowing`: `prisma.follow.findUnique` → boolean (used by FollowButton)
|
||||
- `follows.listFollowers`: `where: { followeePetId: petId }` includes follower display fields (id, name, avatarKey, species.name)
|
||||
- `follows.listFollowing`: `where: { followerPetId: petId }` includes followee display fields
|
||||
- 10 tests covering: create row, self-follow rejection, ownership assertion, backfillOnFollow invoked, delete row, isFollowing true/false, listFollowers/listFollowing scoped
|
||||
|
||||
**Task 2 — blocksRouter + reportsRouter + tests (SAFE-01, SAFE-02):**
|
||||
- `blocks.create`: assertPetOwnership + self-block BAD_REQUEST + `prisma.block.create`; no Redis change (blocks authoritative in Postgres only, T-02-12)
|
||||
- `blocks.list`: assertPetOwnership + `prisma.block.findMany({ where: { blockerPetId: petId }, select: { blockedPetId: true } })` — consumed by feed hydration filter
|
||||
- `reports.create`: both-null target BAD_REQUEST + assertPetOwnership + `prisma.report.create` with nullable targetPostId/targetPetId, ReportReason enum, notes max 500
|
||||
- 7 blocks tests (create row, self-block, ownership, list returns blockedPetIds, list ownership, feed filter helper)
|
||||
- 7 reports tests (create with targetPostId, with targetPetId, both-null rejection, ownership, all 5 ReportReason enum values)
|
||||
|
||||
**Task 3 — Social + Safety UI:**
|
||||
- `FollowButton.tsx`: optimistic toggle (onMutate cancel+setData, onError revert+toast, onSettled invalidate); AlertDialog unfollow confirm with exact UI-SPEC copy; hidden when followerPetId === followeePetId
|
||||
- `PetListItem.tsx`: flex row with sm Avatar + pet name + species Badge + right-aligned FollowButton
|
||||
- `BlockDialog.tsx`: AlertDialog with exact UI-SPEC copy ("Block {pet name}?", destructive "Block", "Don't block"), success toast "You've blocked {pet name}"
|
||||
- `ReportSheet.tsx`: Sheet with 5-option RadioGroup (Spam/Harassment/Inappropriate content/False or misleading information/Something else), notes Textarea max 500, destructive "Submit Report", "Don't report" cancel
|
||||
- `followers/page.tsx`: RSC shell fetches followers count + passes to FollowersList; heading "{N} Followers"; empty state copy
|
||||
- `following/page.tsx`: RSC shell fetches following + passes to FollowingList; heading "Following {N}"; empty state copy
|
||||
- `FollowersList.tsx` + `FollowingList.tsx`: client components reading ActivePetContext for followerPetId
|
||||
- `pets/[petId]/page.tsx`: added stats row (posts/followers/following with links), ProfileActions client component
|
||||
- `ProfileActions.tsx`: FollowButton + MoreHorizontal DropdownMenu wiring BlockDialog + ReportSheet for other pets; hidden on own pet
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Test context `prisma` cast TS2322 type conflict**
|
||||
- **Found during:** Task 3, running `npx tsc --noEmit`
|
||||
- **Issue:** `prisma: mockPrisma as unknown as PrismaClient` caused `TS2322` — the `createCaller` context infers `prisma` as `MockPrismaClient` but the router type requires `PrismaClient`. TypeScript rejected the double cast.
|
||||
- **Fix:** Changed to `prisma: mockPrisma as any` in all three test helper factory functions
|
||||
- **Files modified:** `src/__tests__/follows.test.ts`, `src/__tests__/blocks.test.ts`, `src/__tests__/reports.test.ts`
|
||||
- **Commit:** 0428cf2
|
||||
|
||||
### Pre-existing Issues (Out of Scope)
|
||||
|
||||
**1. `feed.ts` TS2339 error: `prisma` does not exist on context union type**
|
||||
- **Status:** Pre-existing — confirmed by checking the error on base commit (before any 02-02 changes)
|
||||
- **Root cause:** tRPC context type inference edge case in `feed.ts` line 13 — the `assertPetOwnership` function parameter type doesn't narrow correctly
|
||||
- **Impact:** Does not affect Phase 2 plan 02-02 deliverables; feed.ts is a separate plan's concern
|
||||
- **Logged to:** `deferred-items.md` scope for feed implementation plan
|
||||
|
||||
**2. `src/__tests__/media-upload.test.ts` fails with "supabaseUrl is required"**
|
||||
- **Status:** Pre-existing failure from Phase 1 — documented in 02-01-SUMMARY.md
|
||||
- **Impact:** Does not affect this plan's 21 new tests (all green)
|
||||
|
||||
## Known Stubs
|
||||
|
||||
| File | Stub | Reason |
|
||||
|------|------|--------|
|
||||
| `src/app/(app)/pets/[petId]/page.tsx` | Posts count hardcoded to 0 | posts router not yet wired to profile; future plan adds post grid |
|
||||
|
||||
The posts count stub does not prevent this plan's goal from being achieved (SOCL-01/02/03, SAFE-01/02 all implemented and tested).
|
||||
|
||||
## Threat Flags
|
||||
|
||||
No new security-relevant surface not in the plan's threat model. All mitigations applied:
|
||||
- T-02-10: assertPetOwnership in follows.create, follows.delete, blocks.create, blocks.list, reports.create — implemented
|
||||
- T-02-11: self-follow + self-block BAD_REQUEST — implemented
|
||||
- T-02-12: blocks.list returns authoritative Postgres blockedPetIds; no Redis block state — implemented
|
||||
- T-02-13: both-null target BAD_REQUEST; ReportReason z.nativeEnum — implemented
|
||||
- T-02-14: rate limiting deferred (accepted per threat model)
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
Files verified:
|
||||
- src/trpc/routers/follows.ts: FOUND
|
||||
- src/trpc/routers/blocks.ts: FOUND
|
||||
- src/trpc/routers/reports.ts: FOUND
|
||||
- src/__tests__/follows.test.ts: FOUND
|
||||
- src/__tests__/blocks.test.ts: FOUND
|
||||
- src/__tests__/reports.test.ts: FOUND
|
||||
- src/components/social/FollowButton.tsx: FOUND
|
||||
- src/components/social/PetListItem.tsx: FOUND
|
||||
- src/components/safety/BlockDialog.tsx: FOUND
|
||||
- src/components/safety/ReportSheet.tsx: FOUND
|
||||
- src/app/(app)/pets/[petId]/followers/page.tsx: FOUND
|
||||
- src/app/(app)/pets/[petId]/followers/FollowersList.tsx: FOUND
|
||||
- src/app/(app)/pets/[petId]/following/page.tsx: FOUND
|
||||
- src/app/(app)/pets/[petId]/following/FollowingList.tsx: FOUND
|
||||
- src/app/(app)/pets/[petId]/ProfileActions.tsx: FOUND
|
||||
- src/app/(app)/pets/[petId]/page.tsx: FOUND (modified)
|
||||
|
||||
Commits verified:
|
||||
- 2e1cf45: test(02-02): add failing tests for follows router (RED)
|
||||
- 7bdb298: feat(02-02): implement followsRouter (GREEN)
|
||||
- 65b30c7: test(02-02): add failing tests for blocks + reports routers (RED)
|
||||
- f85230a: feat(02-02): implement blocksRouter + reportsRouter (GREEN)
|
||||
- 0428cf2: feat(02-02): social + safety UI (Task 3 + Rule 1 fix)
|
||||
Reference in new issue
Block a user