-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Refactor Prisma Schema and Update PostGallery Mock Data Structure #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…ify PostGallery component
WalkthroughMoves per-post summaries and metadata into a new PostGroup model, replaces Subpost with Post (individual posts) linked to PostGroup, updates Prisma schema and migrations with backfill and NOT NULL enforcement, updates seed to create PostGroups with nested Posts, and adapts frontend components/types to PostGroup→Post. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Migr as migration.sql
participant DB as Database
Note over Migr,DB: Schema migration + data backfill to introduce PostGroup and promote Posts
Migr->>DB: ALTER TABLE Post ADD COLUMN postGroupId, content, createdAt, updatedAt, sentiment, source, categories, subcategories, link
Migr->>DB: BACKFILL new columns (content from title/defaults, timestamps, sentiment=NEUTRAL, source=REDDIT, arrays=[])
Migr->>DB: CREATE TABLE PostGroup (id,title,totalposts,summaries...)
Migr->>DB: UPDATE Post SET postGroupId = <generated> -- associate existing rows to groups
Migr->>DB: ALTER TABLE Post ALTER COLUMN ... SET NOT NULL
Migr->>DB: DROP TABLE Subpost
Migr->>DB: ADD FK Post.postGroupId → PostGroup.id ON DELETE RESTRICT ON UPDATE CASCADE
DB-->>Migr: Migration completed
sequenceDiagram
autonumber
participant Seed as prisma/seed.ts
participant DB as Database
Note over Seed,DB: Seed creates PostGroups with nested Posts
Seed->>DB: deleteMany(PostGroup)
loop per generated postGroup
Seed->>DB: create(PostGroup { title, totalposts, bullishSummary..., posts: { createMany: [...] } })
end
DB-->>Seed: Created PostGroups + Posts
sequenceDiagram
autonumber
participant UI as PostsGallery
participant API as Data (PostGroup[])
participant Card as PostCard
Note over UI,Card: Frontend consumes PostGroup and aggregates group-level info
UI->>API: fetch postGroups
loop for each postGroup
UI->>Card: render postGroup (title, totalposts, summaries, posts[])
Card->>Card: aggregate sentiments, sources, categories across posts[]
Card-->>UI: rendered card
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/PostComponents/PostGallery.tsx (1)
9-9
: Don’t import Prisma client into a client component; use a type-only import.Importing
@prisma/client
at runtime in"use client"
code bloats or breaks the bundle. Use a type-only import (or a local union type).-import { Sentiment } from "@prisma/client"; +import type { Sentiment } from "@prisma/client";Alternatively:
type Sentiment = "BULLISH" | "NEUTRAL" | "BEARISH";
🧹 Nitpick comments (12)
prisma/schema.prisma (3)
51-59
: Renametotalposts
to idiomatictotalPosts
without another migration using @Map.Prisma field names are typically camelCase. Keep the DB column as-is and expose a camelCased field via
@map
for clarity and future DX.model PostGroup { id String @id @default(uuid()) title String posts Post[] - totalposts Int @default(0) + totalPosts Int @default(0) @map("totalposts") bullishSummary String? bearishSummary String? neutralSummary String? }Follow-ups:
- Update usage sites (e.g., seed.ts) from
totalposts
→totalPosts
.
69-71
: Set explicit referential action to avoid manual child cleanup.You’re already deleting Posts before PostGroups in seed due to
RESTRICT
. If the product semantics allow, switch toonDelete: Cascade
so parent deletions remove children automatically.- postGroup PostGroup @relation(fields: [postGroupId], references: [id]) + postGroup PostGroup @relation(fields: [postGroupId], references: [id], onDelete: Cascade)If you must keep
RESTRICT
in prod, consider a seed-only flag or keep current order. Please confirm intended behavior.
61-73
: Add an index on the foreign key for query performance.Typical access patterns fetch Posts by
postGroupId
. Add an index to avoid seq scans.model Post { id String @id @default(uuid()) content String sentiment Sentiment source Source categories String[] @default([]) subcategories String[] @default([]) link String? postGroupId String postGroup PostGroup @relation(fields: [postGroupId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([postGroupId]) }prisma/migrations/20250917075310_update_post_group_model/migration.sql (1)
8-9
: Prefer column rename over drop+add to preserve data.If this runs against a non-empty DB, current SQL discards counts. Use
RENAME COLUMN
to retain values.-ALTER TABLE "public"."PostGroup" DROP COLUMN "totalSubposts", -ADD COLUMN "totalposts" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "public"."PostGroup" RENAME COLUMN "totalSubposts" TO "totalposts";Confirm no prod/staging data would be lost if keeping the drop.
prisma/seed.ts (2)
11-15
: Wrap destructive deletes in an ordered interactive transaction.Ensures atomicity and preserves the required delete order under
RESTRICT
.- // Delete existing data to prevent stacking - await prisma.post.deleteMany({}); - await prisma.postGroup.deleteMany({}); - await prisma.user.deleteMany({}); + // Delete existing data to prevent stacking (atomic & ordered) + await prisma.$transaction(async (tx) => { + await tx.post.deleteMany({}); + await tx.postGroup.deleteMany({}); + await tx.user.deleteMany({}); + });
17-21
: Clarify constant naming.
MAX_POSTGROUP
suggests a single group; this is a count. Prefer plural or an explicit count name.- const MAX_POSTGROUP = 10; + const MAX_POST_GROUPS = 10;Also update the loop to use
MAX_POST_GROUPS
.src/components/PostComponents/PostGallery.tsx (5)
11-15
: Avoid naming collision with Prisma’sSource
enum.Rename the UI helper type to reduce confusion with backend
Source
.-type Source = { +type SourceCount = { name: string; count: number; };
16-25
: Align UI type with naming and future backend mapping.
- Pluralize
source
since it’s an array.- Consider
summary
instead ofcontent
for group-level text (optional).type PostGroup = { title: string; - content: string; + content: string; // consider: summary sentiment: Sentiment; sentimentBar: { bullish: number; neutral: number; bearish: number }; postCount: number; - source: Source[]; + sources: SourceCount[]; categories: string[]; subcategories?: string[]; };Remember to update uses below.
28-29
: Rename mock data to reflect groups and avoid drift with PR description.The PR summary mentions
mockPostGroup
; since it’s an array, prefermockPostGroups
. Update default prop usage.-const mockPostsData: PostGroup[] = [ +const mockPostGroups: PostGroup[] = [ ... -type PostsGalleryProps = { - posts?: PostGroup[]; -}; +type PostsGalleryProps = { posts?: PostGroup[] }; ... export default function PostsGallery({ - posts = mockPostsData, + posts = mockPostGroups, }: PostsGalleryProps) {Also applies to: 95-97, 190-192
151-176
: Keep naming consistent with the type change (sources
).If you adopt the plural rename, update usage here.
- {post.source.map((src, idx) => { + {post.sources.map((src, idx) => {
195-197
: Prefer a stable key over array index.Use a deterministic key (e.g., group id or title). Index keys hurt reconciler behavior on reorder.
- <SinglePostCard key={index} post={post} /> + <SinglePostCard key={post.title} post={post} />Ensure
title
is unique across the list; otherwise add a synthetic id in the mock/type.prisma/migrations/20250917074232_rename_to_post_group_and_post_model/migration.sql (1)
51-52
: Confirm referential action choice.FK uses
ON DELETE RESTRICT
. If PostGroups are routinely pruned, considerCASCADE
to simplify maintenance. Otherwise document the required deletion order.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
prisma/migrations/20250917074232_rename_to_post_group_and_post_model/migration.sql
(1 hunks)prisma/migrations/20250917075310_update_post_group_model/migration.sql
(1 hunks)prisma/schema.prisma
(1 hunks)prisma/seed.ts
(2 hunks)src/components/PostComponents/PostGallery.tsx
(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
prisma/seed.ts (1)
src/lib/prisma.ts (1)
prisma
(13-14)
🔇 Additional comments (1)
prisma/seed.ts (1)
34-68
: Seed: fix Faker array-generation; nested createMany is fine; keeptotalposts
or map it if you change the schema
faker.helpers.arrayElements accepts a number or a { min, max } range (v10) — your call is valid. (fakerjs.dev)
However, you supply only two source words, so asking for { min: 2, max: 5 } will be capped at 2; generate variable-length arrays instead, e.g.:
faker.helpers.multiple(() => faker.lorem.word(), { count: faker.number.int({ min: 2, max: 5 }) }). (v8.fakerjs.dev)Prisma ^6.15.0 supports nested createMany for 1→N relations (so posts: { createMany: { data: [...] } } is supported), but createMany cannot nest additional relation writes per item and doesn’t return nested records — use nested create if you need deeper per-post relations. (prisma.io)
Schema currently defines totalposts (lowercase); keep using totalposts in the seed, or change the schema to totalPosts and add @Map("totalposts") if you prefer camelCase.
Likely an incorrect or invalid review comment.
prisma/migrations/20250917074232_rename_to_post_group_and_post_model/migration.sql
Outdated
Show resolved
Hide resolved
…emove obsolete columns
…nce sentiment display, and improve data handling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (6)
src/components/PostComponents/PostGallery.tsx (6)
149-149
: Tighten typing of sourceCounts (avoid stringly-typed keys).Apply this diff:
- }, {} as Record<string, number>); + }, {} as Partial<Record<Source, number>>);
171-176
: Show the computed count; avoid desync with posts vs. totalposts.Apply this diff:
- {postGroup.totalposts} posts + {totalPosts} postsIf you need to display a server-aggregated count, consider
postGroup.totalposts ?? totalPosts
.
199-214
: Reduce duplication in source pill styling/icon selection.Apply this replacement inside the map:
- {Object.entries(sourceCounts).map(([source, count]) => { - let pillClass = - "px-2 py-1 rounded-full flex items-center gap-1 border text-xs font-medium"; - let icon = null; - if (source === "TWITTER") { - pillClass += " bg-blue-50 text-blue-600 border-blue-200"; - icon = <FaTwitter className="w-3 h-3" />; - } else if (source === "LINKEDIN") { - pillClass += " bg-blue-50 text-blue-600 border-blue-200"; - icon = <FaLinkedin className="w-3 h-3" />; - } else if (source === "REDDIT") { - pillClass += " bg-orange-50 text-orange-600 border-orange-200"; - icon = <FaReddit className="w-3 h-3" />; - } else { - pillClass += " bg-gray-50 text-gray-600 border-gray-200"; - } - return ( - <span key={source} className={pillClass}> + {Object.entries(sourceCounts).map(([source, count]) => { + const { pillClass, icon } = + SOURCE_META[source as keyof typeof SOURCE_META] ?? SOURCE_META.DEFAULT; + return ( + <span key={source} className={pillClass}> {icon} {source} <span className="font-semibold">({count})</span> </span> ); })}Add this helper once above the component (adjust classes as you prefer):
const BASE_PILL = "px-2 py-1 rounded-full flex items-center gap-1 border text-xs font-medium"; const SOURCE_META = { TWITTER: { pillClass: `${BASE_PILL} bg-blue-50 text-blue-600 border-blue-200`, icon: <FaTwitter className="w-3 h-3" />, }, LINKEDIN: { pillClass: `${BASE_PILL} bg-blue-50 text-blue-600 border-blue-200`, icon: <FaLinkedin className="w-3 h-3" />, }, REDDIT: { pillClass: `${BASE_PILL} bg-orange-50 text-orange-600 border-orange-200`, icon: <FaReddit className="w-3 h-3" />, }, DEFAULT: { pillClass: `${BASE_PILL} bg-gray-50 text-gray-600 border-gray-200`, icon: null as JSX.Element | null, }, } as const;
240-240
: Non-standard Tailwind spacing token.Apply this diff (or use an existing scale token like ml-8):
- <div className="flex flex-row justify-start ml-30 gap-6 p-2"> + <div className="flex flex-row justify-start ml-[30px] gap-6 p-2">
241-243
: Use a stable key.Apply this diff:
- {postGroups.map((postGroup, index) => ( - <PostCard key={index} postGroup={postGroup} /> + {postGroups.map((postGroup) => ( + <PostCard key={postGroup.id} postGroup={postGroup} /> ))}
123-124
: Prop is required but you also provide a default. Make it optional or drop the default.Apply this diff:
-type PostsGalleryProps = { - postGroups: PostGroup[]; -}; +type PostsGalleryProps = { + postGroups?: PostGroup[]; +};Also applies to: 237-238
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/components/PostComponents/PostGallery.tsx
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/PostComponents/PostGallery.tsx (1)
src/components/PostComponents/TagRenderer.tsx (1)
TagRenderer
(13-49)
…update TagRenderer for improved styling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (2)
src/components/PostComponents/PostGallery.tsx (2)
10-10
: Don’t runtime‑import @prisma/client in a client component. Use type‑only.
This can pull Prisma into the browser bundle.-import { Sentiment, Source } from "@prisma/client"; +import type { Sentiment, Source } from "@prisma/client";#!/bin/bash # Find "use client" TSX files that import @prisma/client at value level rg -n --type=tsx '^\s*"use client";' -l \ | xargs -I{} rg -nP '^\s*import(?!\s+type).*@prisma/client' {}
139-144
: Guard division by zero in sentiment bar (avoid NaN).
If a group has 0 posts, flex values become NaN and break layout.- const sentimentBar = { - bullish: (sentimentCounts.bullish / totalPosts) * 100, - neutral: (sentimentCounts.neutral / totalPosts) * 100, - bearish: (sentimentCounts.bearish / totalPosts) * 100, - }; + const sentimentBar = + totalPosts > 0 + ? { + bullish: (sentimentCounts.bullish / totalPosts) * 100, + neutral: (sentimentCounts.neutral / totalPosts) * 100, + bearish: (sentimentCounts.bearish / totalPosts) * 100, + } + : { bullish: 0, neutral: 0, bearish: 0 };
🧹 Nitpick comments (6)
src/components/PostComponents/TagRenderer.tsx (1)
43-45
: Match “+N more” pill styling with tag pills.
For visual consistency, mirror the flex + padding used above.- <span className="px-1 py-0.5 text-[7px] font-medium text-gray-700 bg-gray-100 rounded-full"> + <span className="flex items-center justify-center px-1 py-1 text-[7px] font-medium text-gray-700 bg-gray-100 rounded-full">src/components/PostComponents/PostGallery.tsx (5)
221-224
: Use the computed totalPosts to keep UI consistent with the bar.
Avoid mismatch between displayed count and computed distribution.- {postGroup.totalposts} posts + {totalPosts} posts
12-20
: Verify naming: totalposts vs totalPosts.
CamelCase is typical in TS; confirm schema alignment or rename locally.- totalposts: number; + totalPosts: number;If DB uses totalposts, map it at the boundary and keep UI types camelCase.
200-202
: Fallback when group summary is null/undefined.
Prevents empty description blocks.- {dominantSentiment.description} + {dominantSentiment.description ?? "No summary available."}
288-288
: Invalid Tailwind class ml-30.
Tailwind doesn’t ship ml-30 by default; use an arbitrary value or a supported token.- <div className="flex flex-row justify-start ml-30 gap-6 p-2"> + <div className="flex flex-row justify-start gap-6 p-2 ml-[7.5rem]">
289-291
: Avoid index keys; use stable IDs.
Prevents key churn and unnecessary re-renders.- {postGroups.map((postGroup, index) => ( - <PostCard key={index} postGroup={postGroup} /> + {postGroups.map((postGroup) => ( + <PostCard key={postGroup.id} postGroup={postGroup} /> ))}
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/components/PostComponents/PostGallery.tsx
(2 hunks)src/components/PostComponents/TagRenderer.tsx
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/PostComponents/PostGallery.tsx (1)
src/components/PostComponents/TagRenderer.tsx (1)
TagRenderer
(13-49)
🔇 Additional comments (1)
src/components/PostComponents/TagRenderer.tsx (1)
33-37
: Centering pill content: LGTM.
Change to flex-centering is correct and safe.
…e zero posts scenario
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good work @ginzahatemi the code is really well written!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/components/PostComponents/PostGallery.tsx (1)
139-147
: 0‑post guard on sentimentBar — good catch.Prevents NaN/invalid flex when there are no posts.
🧹 Nitpick comments (12)
src/components/PostComponents/PostGallery.tsx (12)
10-10
: Type-only Prisma import in a client component — good.This avoids bundling Prisma into the browser. Optional: consider a small shared “domain-types” module (string unions mirroring Prisma enums) so client code doesn’t depend on @prisma/client at all.
224-227
: Use posts.length for the displayed count (avoid drift with totalposts).Rendering from the array ensures accuracy if a cached/denormalized total diverges.
Apply this diff:
- {postGroup.totalposts} posts + {postGroup.posts.length} posts
16-16
: Inconsistent casing: totalposts → totalPosts (or drop the field).If you keep a denormalized count, prefer camelCase. Otherwise, remove it and rely on posts.length to avoid dual sources of truth.
Apply this diff to align casing:
- totalposts: number; + totalPosts: number;If the schema doesn’t expose this field (or it’s optional), consider removing it from the type entirely.
39-39
: Denormalized count in mock data — consider removing.If you switch the UI to posts.length, this becomes redundant and can be dropped.
Apply this diff:
- totalposts: 3, + // totalPosts: 3, // (optional) keep only if present in API shape
88-88
: Same as above for group2.Apply this diff:
- totalposts: 2, + // totalPosts: 2, // (optional) keep only if present in API shape
157-171
: Tie-handling for dominant sentiment could be clearer.Currently a 2‑way tie chooses the first branch (bullish) implicitly; consider making ties explicit (e.g., any tie on max → NEUTRAL) for predictability.
Apply this diff:
- const getDominantSentiment = () => { + const getDominantSentiment = () => { const max = Math.max( sentimentCounts.bullish, sentimentCounts.neutral, sentimentCounts.bearish ); - - if ( - sentimentCounts.bullish === sentimentCounts.neutral && - sentimentCounts.neutral === sentimentCounts.bearish - ) { - return { sentiment: "NEUTRAL", description: postGroup.neutralSummary }; - } + const maxTies = + (sentimentCounts.bullish === max ? 1 : 0) + + (sentimentCounts.neutral === max ? 1 : 0) + + (sentimentCounts.bearish === max ? 1 : 0); + if (maxTies > 1) { + return { sentiment: "NEUTRAL", description: postGroup.neutralSummary }; + }
183-187
: Tighten typing of sourceCounts.Use the Source enum type for keys to catch unknown sources at compile time.
Apply this diff:
- const sourceCounts = postGroup.posts.reduce((acc, post) => { - acc[post.source] = (acc[post.source] || 0) + 1; - return acc; - }, {} as Record<string, number>); + const sourceCounts = postGroup.posts.reduce((acc, post) => { + acc[post.source] = (acc[post.source] ?? 0) + 1; + return acc; + }, {} as Partial<Record<Source, number>>);
203-205
: Fallback when no summary is available.Prevents an empty description area if the chosen summary is null/undefined.
Apply this diff:
- {dominantSentiment.description} + {dominantSentiment.description || "No summary available."}
213-220
: Mark decorative sentiment icons as aria-hidden.Screen readers already get the adjacent text; hide redundant SVGs.
Apply this diff:
- <IoIosTrendingUp className="text-green-600 w-4 h-4" /> + <IoIosTrendingUp aria-hidden className="text-green-600 w-4 h-4" /> @@ - <IoIosTrendingDown className="text-red-600 w-4 h-4" /> + <IoIosTrendingDown aria-hidden className="text-red-600 w-4 h-4" /> @@ - <FiMinus className="text-gray-600 w-4 h-4" /> + <FiMinus aria-hidden className="text-gray-600 w-4 h-4" />
291-291
: Tailwind class ml-30 is not in the default spacing scale.Use an arbitrary value or a valid token so it doesn’t get purged.
Apply this diff:
- <div className="flex flex-row justify-start ml-30 gap-6 p-2"> + <div className="flex flex-row justify-start ml-[30px] gap-6 p-2">
292-294
: Use a stable key (postGroup.id) instead of the array index.Avoids unnecessary remounts/reconciliation issues on reordering/insertions.
Apply this diff:
- {postGroups.map((postGroup, index) => ( - <PostCard key={index} postGroup={postGroup} /> + {postGroups.map((postGroup) => ( + <PostCard key={postGroup.id} postGroup={postGroup} /> ))}
287-289
: Defaulting to mock data in the exported component can leak to prod.Prefer passing data from the parent; keep mocks in stories/dev‑only paths.
Apply this diff:
-export default function PostsGallery({ - postGroups = mockPostGroup, -}: PostsGalleryProps) { +export default function PostsGallery({ postGroups }: PostsGalleryProps) {If you need a local preview, add a Storybook story or a dev-only wrapper that injects mockPostGroup.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/components/PostComponents/PostGallery.tsx
(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/PostComponents/PostGallery.tsx (1)
src/components/PostComponents/TagRenderer.tsx (1)
TagRenderer
(13-49)
PR Description:
PostGroup
andPost
naming and relationships inschema.prisma
.mockPostsData
tomockPostGroup
for consistency with schema changes.Summary by CodeRabbit
New Features
Refactor
Chores
Style