Appearance
Epic 16 — Publication Feed
Talents post media (photo or video) enriched with text overlays, filters, and manual edits. Producers and other users see a scrollable feed with live content at the top and regular posts below.
BE-FEED-001 — Post, PostLike, PostComment, PostShare models
- [x] Implemented
Files:
- Edit:
prisma/schema.prisma— addPost,PostLike,PostComment,PostSharemodels - Run:
pnpm db:migrate
Schema:
prisma
model Post {
id String @id @default(cuid())
authorId String
author TalentProfile @relation(fields: [authorId], references: [id], onDelete: Cascade)
mediaUrl String
mediaType MediaType
thumbnailUrl String?
caption String?
textOverlay Json? // { text, fontId, position: { x, y } }
editParams Json? // { exposure, contrast, saturation, temperature, hue }
filterId String?
viewCount Int @default(0)
status PostStatus @default(PUBLISHED)
likes PostLike[]
comments PostComment[]
shares PostShare[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posts")
}
enum MediaType { PHOTO VIDEO }
enum PostStatus { PUBLISHED DRAFT REMOVED }
model PostLike {
id String @id @default(cuid())
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
userId String
createdAt DateTime @default(now())
@@unique([postId, userId])
@@map("post_likes")
}
model PostComment {
id String @id @default(cuid())
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
authorId String
body String
createdAt DateTime @default(now())
@@map("post_comments")
}
model PostShare {
id String @id @default(cuid())
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
sharedById String
createdAt DateTime @default(now())
@@map("post_shares")
}BE-FEED-002 — Media upload + processing pipeline
- [x] Implemented
Files:
- Edit:
src/services/media/index.ts— adduploadPostMedia(userId, file, type)returning signed URL + processed URLs - Create:
src/workers/mediaProcessor.ts— BullMQ worker: generate thumbnail, transcode video to HLS, extract dominant color - Edit:
src/graphql/schema/index.ts— adduploadPostMedia(file: Upload!, type: MediaType!): PostMediaUpload! - Create:
src/__tests__/services/media.test.ts
Description: Upload raw file to S3 (castyou-media/posts/{userId}/{id}.{ext}). Background job generates thumbnail and (for video) HLS playlist. Returns { mediaUrl, thumbnailUrl, dominantColor }. Max size: 50 MB photo, 200 MB video.
BE-FEED-003 — Feed query + Post mutations
- [x] Implemented
Files:
- Edit:
src/graphql/schema/index.ts— addPosttype,feedquery,createPost/deletePost/likePost/unlikePost/createCommentmutations - Edit:
src/graphql/resolvers/index.ts - Create:
src/graphql/resolvers/feed.ts - Create:
src/services/feed/index.ts - Create:
src/__tests__/resolvers/feed.test.ts
GraphQL:
graphql
type Post {
id: ID!
author: TalentProfile!
mediaUrl: String!
mediaType: String!
thumbnailUrl: String
caption: String
textOverlay: JSON
editParams: JSON
filterId: String
viewCount: Int!
likeCount: Int!
commentCount: Int!
likedByMe: Boolean!
status: String!
createdAt: DateTime!
}
type PostConnection { edges: [Post!]! pageInfo: PageInfo! }
# Queries
feed(first: Int, after: String): PostConnection!
post(id: ID!): Post
# Mutations
createPost(input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
likePost(postId: ID!): Post!
unlikePost(postId: ID!): Post!
createComment(postId: ID!, body: String!): PostComment!
input CreatePostInput {
mediaUrl: String!
mediaType: String!
thumbnailUrl: String
caption: String
textOverlay: JSON
editParams: JSON
filterId: String
}Feed ordering: recency (createdAt DESC) for MVP. Pagination: cursor-based (after = last post ID).
FE-FEED-001 — Feed screen (post cards + infinite scroll)
- [x] Implemented
Files:
- Create:
apps/app/src/pages/feed/FeedPage.tsx - Create:
apps/app/src/components/feed/PostCard.tsx - Create:
apps/app/src/hooks/useFeed.ts - Edit:
apps/app/src/App.tsx— add/feedroute - Edit:
packages/design-system/src/components/ui/PostCard.tsx— DS component - Edit:
packages/design-system/src/index.ts
Description: Scrollable feed page. Sections:
- "Live for you" horizontal strip — placeholder card for now (live streaming epic TBD).
- "To keep you in the loop" vertical list of
PostCardcomponents:- Avatar + display name + role label (top-left)
- Share icon (top-right)
- Full-bleed media: photo renders statically; video loops muted with a play/pause toggle
- Like count + comment count (bottom-right)
- "View full profile" CTA →
/talent/:id - Logged-out variant: replace CTA with "Sign in to apply" →
/login
Infinite scroll via IntersectionObserver, cursor-based pagination calling feed(first: 10, after).
FE-FEED-002 — Media picker screen
- [x] Implemented
Files:
- Create:
apps/app/src/pages/feed/MediaPickerPage.tsx - Edit:
packages/design-system/src/components/ui/MediaGallery.tsx - Edit:
packages/design-system/src/index.ts
Description:
- "Select file" dropzone at top (accepts image/, video/)
- Gallery grid below showing device media (via
<input type="file" accept="image/*,video/*" multiple>on web) - Single-select: tapping a thumbnail moves it to the preview slot at top
- "Continue" button (disabled until selection made) navigates to composer
FE-FEED-003 — Post composer (Text / Filters / Edit tabs)
- [x] Implemented
Files:
- Create:
apps/app/src/pages/feed/ComposerPage.tsx - Create:
apps/app/src/pages/feed/tabs/TextTab.tsx - Create:
apps/app/src/pages/feed/tabs/FiltersTab.tsx - Create:
apps/app/src/pages/feed/tabs/EditTab.tsx - Create:
apps/app/src/hooks/useCreatePost.ts - Edit:
packages/design-system/src/components/ui/RangeSlider.tsx— if not already in DS - Edit:
packages/design-system/src/index.ts
Description: Shared layout: media preview (cropped square) + 3-tab bar + "Post" button.
| Tab | Features |
|---|---|
| Text | Font picker carousel (shows "Aa Bb Cc…" sample per font), text input, overlay rendered on the media preview at a draggable position |
| Filters | Horizontal strip of preset filter thumbnails (applied via CSS filter); tap to apply with live preview |
| Edit | Sliders: Exposure, Contrast, Saturation, Temperature, Hue — applied via CSS filter on photo; values stored as editParams JSON |
"Post" button calls uploadPostMedia then createPost, then navigates back to /feed.