Skip to content

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 — add Post, PostLike, PostComment, PostShare models
  • 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 — add uploadPostMedia(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 — add uploadPostMedia(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 — add Post type, feed query, createPost / deletePost / likePost / unlikePost / createComment mutations
  • 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 /feed route
  • Edit: packages/design-system/src/components/ui/PostCard.tsx — DS component
  • Edit: packages/design-system/src/index.ts

Description: Scrollable feed page. Sections:

  1. "Live for you" horizontal strip — placeholder card for now (live streaming epic TBD).
  2. "To keep you in the loop" vertical list of PostCard components:
    • 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.

TabFeatures
TextFont picker carousel (shows "Aa Bb Cc…" sample per font), text input, overlay rendered on the media preview at a draggable position
FiltersHorizontal strip of preset filter thumbnails (applied via CSS filter); tap to apply with live preview
EditSliders: 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.