docs(02-04): complete photo posts plan summary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
e4b619671f
commit
f7b09902f3
1 file changed
+184
@@ -0,0 +1,184 @@
|
||||
---
|
||||
phase: "02"
|
||||
plan: "04"
|
||||
subsystem: photo-posts
|
||||
tags: [trpc, supabase-storage, redis, fan-out, carousel, embla, tdd, shadcn, postcard]
|
||||
dependency_graph:
|
||||
requires:
|
||||
- Phase 02 Plan 01 (posts.ts stub, feed-helpers.ts fanOutPost, redis.ts, prisma schema)
|
||||
- Phase 02 Plan 02 (BlockDialog, ReportSheet from safety/)
|
||||
- Phase 02 Plan 03 (FeedList, PostCard placeholder seam)
|
||||
provides:
|
||||
- postsRouter: create+fanOut, getPresignedUrl, updateCaption, delete, byPetId
|
||||
- src/components/feed/PostCard.tsx — full carousel + caption clamp + options menu
|
||||
- src/components/post-creation/MultiImageUpload.tsx — multi-file presigned upload
|
||||
- src/components/post-creation/PhotoPostForm.tsx — caption + share CTA
|
||||
- src/components/post-creation/PostTypeSheet.tsx — D-01 bottom Sheet (3 choices)
|
||||
- PHOTO-01, PHOTO-02, PHOTO-03, PHOTO-04 requirements implemented and tested
|
||||
affects:
|
||||
- src/components/layout/Sidebar.tsx (New Post button → PostTypeSheet)
|
||||
- src/components/layout/MobileNav.tsx (+ center button → PostTypeSheet)
|
||||
- src/components/feed/FeedList.tsx (PostCard now full, not placeholder)
|
||||
tech_stack:
|
||||
added: []
|
||||
patterns:
|
||||
- TDD RED/GREEN: failing tests committed before implementation
|
||||
- postsRouter: assertPostOwnership helper (post→pet→ownerId) for edit/delete
|
||||
- posts.create: nested Prisma write (Post + PostImage[]) + synchronous fanOutPost
|
||||
- getPresignedUrl: key scoped to posts/{ownerId}/{petId}/{uuid}.ext (T-02-31)
|
||||
- imageKeys.startsWith('posts/') Zod validation — path-traversal prevention
|
||||
- imageKeys.max(10) Zod validation — carousel limit enforcement (T-02-32)
|
||||
- MultiImageUpload: per-file XHR PUT with Content-Type matching getPresignedUrl (Pitfall 4)
|
||||
- PostCard: Embla carousel via shadcn Carousel, setApi/selectedScrollSnap index tracking
|
||||
- PostCard: activePetId === post.pet.id own-vs-other determination
|
||||
- PostTypeSheet: Sheet (D-01) → Dialog (D-02) pattern, no URL navigation
|
||||
- PhotoPostForm: onSuccess invalidates feed query + router.push('/feed') (D-03)
|
||||
key_files:
|
||||
created:
|
||||
- src/components/post-creation/MultiImageUpload.tsx
|
||||
- src/components/post-creation/PhotoPostForm.tsx
|
||||
- src/components/post-creation/PostTypeSheet.tsx
|
||||
modified:
|
||||
- src/trpc/routers/posts.ts (replaced NOT_IMPLEMENTED stubs with full implementation)
|
||||
- src/__tests__/posts.test.ts (replaced it.todo stubs with 4 passing tests)
|
||||
- src/components/feed/PostCard.tsx (replaced 02-03 placeholder with full implementation)
|
||||
- src/components/layout/Sidebar.tsx (added New Post button → PostTypeSheet)
|
||||
- src/components/layout/MobileNav.tsx (wired center + button → PostTypeSheet)
|
||||
decisions:
|
||||
- "posts.create folds confirmUpload into create via imageKeys input — avoids a separate round-trip; keys validated with .startsWith('posts/') at mutation entry (T-02-31)"
|
||||
- "assertPostOwnership helper used for both updateCaption and delete — single source for post-level FORBIDDEN check"
|
||||
- "byPetId uses cursor from createdAt ISO string (not Redis) — direct Prisma cursor pagination for pet profile view, separate from Redis feed"
|
||||
- "feed cleanup on delete not implemented in Phase 2 — feed.getFeed already filters deleted post IDs since Prisma findMany won't return them"
|
||||
- "caption expand threshold heuristic: >140 chars triggers more/less toggle — avoids showing toggle for very short captions"
|
||||
- "PostCard: PostTypeSheet placed inside the outer return div in MobileNav (not outside closing div) — fixed TypeScript structural error"
|
||||
metrics:
|
||||
duration: "~25 minutes"
|
||||
completed: "2026-06-07T10:49:00Z"
|
||||
tasks_completed: 3
|
||||
tasks_total: 3
|
||||
files_created: 3
|
||||
files_modified: 5
|
||||
---
|
||||
|
||||
# Phase 02 Plan 04: Photo Posts Summary
|
||||
|
||||
**One-liner:** Posts router (create+fanOut, presigned upload, caption edit, delete) + full Embla carousel PostCard + PostTypeSheet→PhotoPostForm creation flow wiring D-01/D-02/D-03
|
||||
|
||||
## What Was Built
|
||||
|
||||
A complete vertical slice for PHOTO-01–04: server-side tRPC router, full PostCard UI, and the post-creation flow from + button to feed.
|
||||
|
||||
**Task 1 — posts router TDD RED/GREEN (PHOTO-01–04):**
|
||||
|
||||
RED phase: Wrote 4 failing tests covering:
|
||||
- PHOTO-01: `posts.create` inserts Post + PostImage with correct petId and calls `fanOutPost`
|
||||
- PHOTO-02: `posts.create` rejects >10 images via Zod `array.max(10)` (already passing from stub schema)
|
||||
- PHOTO-03: `posts.updateCaption` throws FORBIDDEN for non-owner via `assertPostOwnership`
|
||||
- PHOTO-04: `posts.delete` calls `prisma.post.delete` (PostImages cascade via schema)
|
||||
|
||||
GREEN phase: Implemented `postsRouter`:
|
||||
- `getPresignedUrl`: ownership assertion (T-02-30); key scoped to `posts/{ownerId}/{petId}/{uuid}.ext` (T-02-31); returns `{ presignedUrl, key }`
|
||||
- `create`: ownership assertion + nested Prisma write (`Post` + `PostImage[]` with position index) + `fanOutPost` (PHOTO-01); `imageKeys.max(10)` (PHOTO-02); `imageKeys.startsWith('posts/')` prevents path traversal (T-02-31)
|
||||
- `updateCaption`: `assertPostOwnership` (post→pet→ownerId, T-02-34) + Prisma update (PHOTO-03)
|
||||
- `delete`: `assertPostOwnership` + `prisma.post.delete`; PostImages cascade via schema `onDelete: Cascade` (PHOTO-04); feed cleanup deferred (feed hydration handles it via Prisma findMany)
|
||||
- `byPetId`: cursor-paginated query (ISO string cursor, not Redis) with images+pet+milestone include
|
||||
|
||||
All 4 PHOTO tests green. Full suite: 40 passing, 10 todo stubs (future plans), only pre-existing `media-upload.test.ts` failure.
|
||||
|
||||
**Task 2 — Full PostCard (PHOTO-03/04 UI, carousel, caption clamp):**
|
||||
|
||||
Replaced 02-03 placeholder with full implementation:
|
||||
- Header: Avatar (story ring seam `// TODO(02-05): StoryRing wrapper`), pet name `text-base font-semibold`, timestamp `text-xs muted`, `MoreHorizontal` options button 44px
|
||||
- Single image: `object-cover max-h-[560px]`
|
||||
- Carousel (2–10): shadcn Carousel (Embla) with `setApi/selectedScrollSnap` index indicator `"{current} / {total}"` top-right; dot indicators `bg-muted-foreground/40` inactive / `bg-foreground` active (NOT orange — accent reservation per UI-SPEC)
|
||||
- Caption: `line-clamp-3` with more/less toggle (instant, no animation per UI-SPEC); threshold >140 chars
|
||||
- Options menu (base-nova: onClick handlers, no asChild):
|
||||
- Own post (`activePetId === post.pet.id`): "Edit caption" (inline Textarea → `updateCaption` mutation, toast "Caption updated") + "Delete post" (AlertDialog → `delete` mutation, toast "Post deleted")
|
||||
- Other's post: "Report post" (opens `ReportSheet`) + separator + "Block {pet name}" (opens `BlockDialog`)
|
||||
- Exact UI-SPEC copy throughout (delete dialog, toast messages, button labels)
|
||||
- `tsc --noEmit` exits 0
|
||||
|
||||
**Task 3 — Post creation flow (D-01/D-02/D-03):**
|
||||
|
||||
`MultiImageUpload.tsx`:
|
||||
- Per-file validation: size (<10MB → "Photo must be under 10 MB."), type (jpg/png/webp → "Upload a JPG, PNG, or WebP image."), count (>10 → "You can upload up to 10 photos per post.")
|
||||
- `posts.getPresignedUrl` → XHR PUT to Supabase signed URL; `Content-Type` header MUST match exactly (Pitfall 4 prevention)
|
||||
- Thumbnail grid with remove button, upload progress bar (orange fill), status overlays
|
||||
- `onKeysChange` callback with all successfully uploaded `posts/` keys
|
||||
|
||||
`PhotoPostForm.tsx`:
|
||||
- `MultiImageUpload` + caption `Textarea` (placeholder "Write a caption…", max 500, error on overflow)
|
||||
- "Share Post" CTA (`bg-orange-500`) → `posts.create({ petId: activePetId, caption, imageKeys })`
|
||||
- `onSuccess`: `queryClient.invalidateQueries` (feed), `onClose()` (close Dialog), `router.push('/feed')` (D-03), toast "Post shared"
|
||||
- Error toast: "Couldn't share your post. Please try again."
|
||||
|
||||
`PostTypeSheet.tsx`:
|
||||
- Bottom `Sheet` titled "Create" with 3 full-width buttons: "Photo Post" (Camera), "Story" (BookOpen), "Milestone" (Trophy) (D-01)
|
||||
- Photo → full-screen `Dialog` with `PhotoPostForm` (D-02, no URL change)
|
||||
- Story: `// TODO(02-05): StoryForm` seam — Dialog with placeholder "Story creation coming soon."
|
||||
- Milestone: `// TODO(02-06): MilestoneForm` seam — Dialog with placeholder "Milestone creation coming soon."
|
||||
|
||||
`Sidebar.tsx` + `MobileNav.tsx`:
|
||||
- `Sidebar`: "New Post" button (orange-500) above owner identity → opens `PostTypeSheet`
|
||||
- `MobileNav`: center + button changed from `Link href="/pets"` to `button onClick → setPostTypeSheetOpen(true)`
|
||||
- Both pass `open/onOpenChange` props to `PostTypeSheet` (D-01)
|
||||
|
||||
`tsc --noEmit` exits 0; full suite 40 passing.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] MobileNav JSX structure error — PostTypeSheet outside closing div**
|
||||
- **Found during:** Task 3, `npx tsc --noEmit`
|
||||
- **Issue:** PostTypeSheet was placed after `</div>` closing the outer container but before `);` — TypeScript error TS1005 `')' expected`
|
||||
- **Fix:** Moved PostTypeSheet inside the outer `<div>` wrapper, before its closing tag
|
||||
- **Files modified:** `src/components/layout/MobileNav.tsx`
|
||||
- **Commit:** e4b6196 (fixed inline during task)
|
||||
|
||||
### Design Decisions (Not Deviations)
|
||||
|
||||
- `confirmUpload` is folded into `create` (imageKeys validated with `.startsWith('posts/')`) rather than being a separate procedure — avoids an extra round-trip and follows the plan spec explicitly
|
||||
- Feed cleanup (ZREM from follower sets on delete) not implemented in Phase 2 — documented in router comment; Prisma findMany already filters deleted post IDs naturally
|
||||
|
||||
## Known Stubs
|
||||
|
||||
| File | Stub | Reason |
|
||||
|------|------|--------|
|
||||
| `src/components/feed/PostCard.tsx` | `// TODO(02-05): StoryRing wrapper` | StoryRing is 02-05 scope |
|
||||
| `src/components/post-creation/PostTypeSheet.tsx` | Story dialog: "Story creation coming soon." | StoryForm is 02-05 scope |
|
||||
| `src/components/post-creation/PostTypeSheet.tsx` | Milestone dialog: "Milestone creation coming soon." | MilestoneForm is 02-06 scope |
|
||||
| `src/app/(app)/feed/page.tsx` | `TODO(02-05): StoryTray above feed` | From 02-03, still pending |
|
||||
|
||||
These stubs are intentional seams. Photo post creation (the plan goal) is fully functional. Story/Milestone seams don't block PHOTO-01–04 requirements.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
No new security-relevant surface not in the plan's threat model. All mitigations implemented:
|
||||
|
||||
| Threat ID | Status |
|
||||
|-----------|--------|
|
||||
| T-02-30 | Mitigated — `assertPetOwnership` (pet.ownerId === ctx.userId) before any post mutation |
|
||||
| T-02-31 | Mitigated — `imageKeys.startsWith('posts/')` in `create`; `getPresignedUrl` scopes key to `posts/{ownerId}/{petId}/` |
|
||||
| T-02-32 | Mitigated — `z.array(...).max(10)` on imageKeys in `posts.create` |
|
||||
| T-02-33 | Mitigated — `supabase-storage.ts` is `import "server-only"` (inherited); client receives only signed URL + public media URL |
|
||||
| T-02-34 | Mitigated — `assertPostOwnership` (post→pet→ownerId) in both `updateCaption` and `delete` |
|
||||
| T-02-35 | Mitigated — `MultiImageUpload` XHR PUT `Content-Type` header set to exact `file.type` matching `getPresignedUrl` input |
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
Files verified:
|
||||
- src/trpc/routers/posts.ts: FOUND
|
||||
- src/__tests__/posts.test.ts: FOUND
|
||||
- src/components/feed/PostCard.tsx: FOUND
|
||||
- src/components/post-creation/MultiImageUpload.tsx: FOUND
|
||||
- src/components/post-creation/PhotoPostForm.tsx: FOUND
|
||||
- src/components/post-creation/PostTypeSheet.tsx: FOUND
|
||||
- src/components/layout/Sidebar.tsx: FOUND
|
||||
- src/components/layout/MobileNav.tsx: FOUND
|
||||
|
||||
Commits verified:
|
||||
- 5e71010: test(02-04): add failing tests for posts router RED
|
||||
- 75d1e80: feat(02-04): implement postsRouter GREEN — PHOTO-01–04
|
||||
- 15f5182: feat(02-04): full PostCard — carousel + caption clamp + options menu
|
||||
- e4b6196: feat(02-04): post creation flow — MultiImageUpload, PhotoPostForm, PostTypeSheet, + button nav
|
||||
Reference in new issue
Block a user