Skip to content

Conversation

@arjunkomath
Copy link
Member

@arjunkomath arjunkomath commented Oct 25, 2025

Summary by CodeRabbit

  • New Features

    • Posts management per project: Published and My Drafts tabs, create/edit/delete flow, categories (Announcement/FYI/Question), inline editor, preview, comments, and a "New" action when permitted
    • Search now returns Posts
    • Project navigation adds a Posts link
  • Chores

    • Database schema and types added for Posts and relations
    • Backend APIs, indexing, activity logging, and mention notifications updated to support Posts

@vercel
Copy link

vercel bot commented Oct 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
manage Ready Ready Preview Oct 25, 2025 11:08am

@coderabbitai
Copy link

coderabbitai bot commented Oct 25, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds a Post feature: DB Post table and types, TRPC posts router with CRUD/indexing/mentions, UI for listing/creating/editing posts (page, list, form, tabs), search indexing support for posts, navbar link, Radix tabs dependency, and migration/meta and DB connection configuration changes.

Changes

Cohort / File(s) Summary
Pages & Project UI
app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx, components/project/posts/posts-list.tsx
New posts page with Published/My Drafts tabs and conditional New panel. PostsList renders items, detail panel, edit/delete actions, author controls, and integrates comments and panels for view/edit.
Post Form
components/form/post.tsx
New PostForm component (create/edit) with title, category, content editor, TRPC upsert, draft handling, and query invalidation.
Navigation
components/layout/navbar.tsx
Added "Posts" project nav link with active detection.
UI primitives & deps
components/ui/tabs.tsx, package.json
New Radix Tabs wrapper components (Tabs, TabsList, TabsTrigger, TabsContent). Added dependency @radix-ui/react-tabs.
Search integration
components/core/search-panel.tsx, lib/search/helpers.ts, lib/search/index.ts, trpc/routers/search.ts
Added post as searchable type, icons/labels, grouping/filtering updates, indexPost helpers, and inclusion of posts in indexAllContent and search types.
TRPC router
trpc/routers/posts.ts, trpc/routers/_app.ts
New postsRouter with list, listAll, myDrafts, get, delete, upsert; enforces permissions, logs activity, indexes posts, sends mention notifications; router registered on appRouter.
Database schema & types
drizzle/0008_soft_serpent_society.sql, drizzle/meta/0008_snapshot.json, drizzle/meta/_journal.json, drizzle/schema.ts, drizzle/types.ts
New Post table, FK constraints, snapshot and journal entry; drizzle schema and TS types (Post, PostWithCreator); project relations include posts.
Search helpers & activity
lib/search/helpers.ts, lib/activity/index.ts, lib/utils/mentionNotifications.ts
Added helpers to index posts; extended logActivity and mention notification context to accept "post".
Scripts (DB connections)
scripts/migrate-all-tenants.ts, scripts/post-upgrade-maintenance.ts
Replaced sslmode-in-URL usage with explicit per-connection SSL configuration objects and unified ops/tenant connection usage.
Misc (types, meta)
drizzle/meta/..., drizzle/types.ts
Snapshot, journal, and type exports updated to reflect Post table.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant U as User
    participant Page as Posts Page
    participant List as PostsList
    participant Form as PostForm
    participant API as TRPC postsRouter
    participant DB as Database
    participant Search as Search Service
    participant Activity as Activity Log

    U->>Page: Open Posts page
    Page->>API: list / myDrafts (projectId)
    API->>DB: Query posts
    DB-->>API: Posts data
    API-->>Page: Return posts
    Page->>List: Render posts

    U->>Page: Click "New"
    Page->>Form: Open create panel (create=true)
    U->>Form: Submit (publish/draft)
    Form->>API: upsert(post payload)
    API->>API: Permission checks (canEditProject)
    API->>DB: Insert/Update Post (transaction)
    DB-->>API: Post record
    API->>Search: indexPost(post, project)
    API->>Activity: logActivity("post", ...)
    Search-->>API: Indexed
    Activity-->>API: Logged
    API-->>Form: Success
    Form->>Page: Close & refresh lists
    Page->>List: Re-fetch posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Review hotspots:
    • trpc/routers/posts.ts — permission logic, transactional upsert, side effects (indexing, mention notifications, activity).
    • components/project/posts/posts-list.tsx & components/form/post.tsx — state flows (view/edit/create), author-only actions, mutation error/edge cases.
    • lib/search/* and components/core/search-panel.tsx — consistency of post in types, grouping, filtering and UI labels.
    • drizzle migration/schema/snapshot — FK cascades and snapshot accuracy.
    • Scripts altering DB connections — verify SSL behavior and environment compatibility.

Possibly related PRs

  • feat: Permissions #160 — related: introduces permission primitives and ProjectPermission schema used by the posts router for canViewProject/canEditProject checks.
  • Performance improvements #183 — related: modifies components/layout/navbar.tsx and may overlap with the navbar changes here.
  • Migrate from Turso to neon #98 — related: earlier DB/schema/migration work touching schema snapshots and migrations used by this PR.

Poem

🐰 I hopped to add a post or two,
A title, draft, and carrot stew.
I tunneled through schema, API, and view,
Indexed the tale and logged it too.
Hooray — the meadow's got posts anew! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: Posts" directly corresponds to the main change in this changeset, which comprehensively adds a new Posts feature across the application. The title follows conventional commit format with the "feat:" prefix and clearly identifies the primary change. While the title is minimal, it is not vague or generic—it specifically refers to the concrete feature being implemented (posts management, CRUD operations, search integration, UI components, and database schema). A teammate scanning the version history would immediately understand that this PR adds the Posts feature to the project management application.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34da2f3 and 4d44fb8.

📒 Files selected for processing (1)
  • components/project/posts/posts-list.tsx (1 hunks)

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
scripts/post-upgrade-maintenance.ts (1)

6-8: Sanitize database names to safe identifier charset before embedding in raw SQL

You later embed databaseName into sql.raw. Replace non [a‑z0‑9_] to prevent identifier injection/parse issues.

-function getDatabaseName(ownerId: string): string {
-  return ownerId.toLowerCase().replace(/ /g, "_");
-}
+function getDatabaseName(ownerId: string): string {
+  return ownerId.toLowerCase().replace(/[^a-z0-9_]/g, "_");
+}
scripts/migrate-all-tenants.ts (2)

24-32: Use opsSchema for the ops DB client

getOpsDatabase currently registers tenant schema, which is inconsistent and loses typed helpers for ops tables.

 async function getOpsDatabase() {
   return drizzle({
     connection: {
       url: `${process.env.DATABASE_URL}/manage`,
       ssl: process.env.DATABASE_SSL === "true",
     },
-    schema,
+    schema: opsSchema,
   });
 }

Based on learnings.


8-10: Harden getDatabaseName to avoid unsafe identifier content

Same reasoning as the maintenance script.

-function getDatabaseName(ownerId: string): string {
-  return ownerId.toLowerCase().replace(/ /g, "_");
-}
+function getDatabaseName(ownerId: string): string {
+  return ownerId.toLowerCase().replace(/[^a-z0-9_]/g, "_");
+}
lib/search/index.ts (1)

90-100: Wrap all Upstash calls with runAndLogError (service boundary).

Guideline requires runAndLogError for search code under lib/search/**. Wrap upsert/delete/deleteIndex/reset (and optionally search) to standardize logging and avoid noisy console logs.

Apply this pattern (example for indexPost and deleteItem; replicate in other methods):

+import { runAndLogError } from "@/lib/search/helpers";
@@
-		await index.upsert({
-			id: searchableItem.id,
-			content,
-			metadata: {
-				url: searchableItem.url,
-				createdAt: searchableItem.createdAt.toISOString(),
-				category: searchableItem.metadata?.category,
-				isDraft: searchableItem.metadata?.isDraft,
-				publishedAt: searchableItem.metadata?.publishedAt,
-				createdByUser: searchableItem.metadata?.createdByUser,
-			},
-		});
+		await runAndLogError("indexing post", async () => {
+			await index.upsert({
+				id: searchableItem.id,
+				content,
+				metadata: {
+					url: searchableItem.url,
+					createdAt: searchableItem.createdAt.toISOString(),
+					category: searchableItem.metadata?.category,
+					isDraft: searchableItem.metadata?.isDraft,
+					publishedAt: searchableItem.metadata?.publishedAt,
+					createdByUser: searchableItem.metadata?.createdByUser,
+				},
+			});
+		});
@@
-	async deleteItem(id: string) {
-		await this.index.delete(id);
-	}
+	async deleteItem(id: string) {
+		await runAndLogError(`removing ${id} from search index`, async () => {
+			await this.index.delete(id);
+		});
+	}
@@
-	async deleteTenantIndex() {
-		try {
-			await this.index.deleteIndex();
-		} catch (error) {
-			console.log(
-				`Index for tenant ${this.tenant} does not exist or already deleted`,
-				error,
-			);
-		}
-	}
+	async deleteTenantIndex() {
+		await runAndLogError(`deleting index for tenant ${this.tenant}`, async () => {
+			await this.index.deleteIndex();
+		});
+	}
@@
-	async clearIndex() {
-		try {
-			await this.index.reset();
-		} catch (error) {
-			console.log(
-				`Index for tenant ${this.tenant} could not be cleared:`,
-				error,
-			);
-		}
-	}
+	async clearIndex() {
+		await runAndLogError(`clearing index for tenant ${this.tenant}`, async () => {
+			await this.index.reset();
+		});
+	}

Based on learnings.

Also applies to: 137-149, 183-193, 226-238, 272-284, 375-377, 387-395, 397-405

🧹 Nitpick comments (13)
scripts/post-upgrade-maintenance.ts (1)

58-64: Avoid duplicating opsDb creation — reuse getOpsDatabase()

Reduces config drift and keeps SSL/schema consistent.

-    const opsDb = drizzle({
-      connection: {
-        url: `${process.env.DATABASE_URL}/manage`,
-        ssl: process.env.DATABASE_SSL === "true",
-      },
-      schema: opsSchema,
-    });
+    const opsDb = await getOpsDatabase();
scripts/migrate-all-tenants.ts (1)

60-66: Reuse getOpsDatabase() instead of inlining drizzle config

Prevents drift and keeps SSL/schema logic centralized.

-    const opsDb = drizzle({
-      connection: {
-        url: `${process.env.DATABASE_URL}/manage`,
-        ssl: process.env.DATABASE_SSL === "true",
-      },
-      schema: opsSchema,
-    });
+    const opsDb = await getOpsDatabase();
drizzle/schema.ts (2)

285-301: Add indexes for common Post queries (projectId, isDraft, publishedAt/updatedAt)

List/listAll/myDrafts/published lists will benefit from indexes. Without them, large tables will full-scan.

Two options:

  • Add a follow-up migration with indexes (preferred for deployed envs).
  • Or amend the current migration if not yet applied anywhere.

Example SQL (new migration):

CREATE INDEX IF NOT EXISTS idx_post_project ON "Post"("projectId");
CREATE INDEX IF NOT EXISTS idx_post_project_isdraft ON "Post"("projectId","isDraft");
CREATE INDEX IF NOT EXISTS idx_post_project_published_at ON "Post"("projectId","publishedAt" DESC) WHERE "isDraft" = false;
CREATE INDEX IF NOT EXISTS idx_post_project_updated_at ON "Post"("projectId","updatedAt" DESC);

285-301: Consider a pgEnum or CHECK constraint for category

Router constrains category to a small set; enforce at DB level to prevent invalid writes.

Example:

DO $$ BEGIN
  CREATE TYPE post_category AS ENUM ('announcement','fyi','question');
EXCEPTION WHEN duplicate_object THEN NULL END $$;
ALTER TABLE "Post" ALTER COLUMN "category" TYPE post_category USING "category"::post_category;
drizzle/0008_soft_serpent_society.sql (1)

1-16: Add indexes to support Post queries

Improve query plans for filters by projectId/isDraft and ordering by publishedAt/updatedAt.

If safe to amend this migration, append:

CREATE INDEX IF NOT EXISTS idx_post_project ON "Post"("projectId");
CREATE INDEX IF NOT EXISTS idx_post_project_isdraft ON "Post"("projectId","isDraft");
CREATE INDEX IF NOT EXISTS idx_post_project_published_at ON "Post"("projectId","publishedAt" DESC) WHERE "isDraft" = false;
CREATE INDEX IF NOT EXISTS idx_post_project_updated_at ON "Post"("projectId","updatedAt" DESC);

Otherwise, create a new 0009_* migration with the above statements.

trpc/routers/search.ts (1)

66-69: Use TRPCError with FORBIDDEN instead of generic Error

Aligns with tRPC conventions and consistent error handling.

-    if (!ctx.isOrgAdmin) {
-      throw new Error("Only organization admins can reindex content");
-    }
+    if (!ctx.isOrgAdmin) {
+      throw new TRPCError({ code: "FORBIDDEN", message: "Only organization admins can reindex content" });
+    }

Add import:

- import type { inferRouterOutputs } from "@trpc/server";
+ import type { inferRouterOutputs } from "@trpc/server";
+ import { TRPCError } from "@trpc/server";

As per coding guidelines.

lib/search/helpers.ts (1)

158-169: Reduce duplication across fetch-and-index helpers.

indexTaskListWithProjectFetch, indexEventWithProjectFetch, and indexPostWithProjectFetch share structure. Consider a small generic util to fetch project and invoke a provided indexer to cut repetition.

app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx (1)

65-94: Optional: prefetch both tab queries to avoid first-switch latency.

You can prefetch the inactive tab in background (keep enabled gating) using queryClient.prefetchQuery on mount to eliminate the loading hit when switching.

components/form/post.tsx (2)

112-116: Tighten category typing.

Avoid any; constrain onValueChange to the union type to prevent invalid values.

- onValueChange={(value: any) => setCategory(value)}
+ onValueChange={(value: "announcement" | "fyi" | "question") => setCategory(value)}

152-176: Optional: disable actions while saving.

Prevent double submissions and reflect pending state by disabling buttons when upsertPost.isPending is true.

- <Button
+ <Button
   type="button"
   variant="outline"
+  disabled={upsertPost.isPending}
   ...
 />
- <Button
+ <Button
   type="button"
+  disabled={upsertPost.isPending}
   ...
 >
   {item ? "Update" : "Publish"}
 </Button>
lib/search/index.ts (1)

252-253: Deep-link post results to the specific post.

Align with task/event URLs by including the post id; otherwise users land on the generic posts page.

Apply this change:

-			url: `/${this.orgSlug}/projects/${projectData.id}/posts`,
+			url: `/${this.orgSlug}/projects/${projectData.id}/posts?post=${postData.id}`,
trpc/routers/posts.ts (1)

306-316: Add a "post" case in mention notifications; link to the specific post.

sendMentionNotifications lacks a "post" case; fallback message/target is generic. Add a branch and deep-link.

In lib/utils/mentionNotifications.ts:

 	switch (context.type) {
+		case "post":
+			message = `${fromUserName} mentioned you in post "${context.entityName}"`;
+			target = `/${context.orgSlug}/projects/${context.projectId}/posts?post=${context.entityId}`;
+			break;

Based on learnings.

Also applies to: 341-351

components/core/search-panel.tsx (1)

176-189: Hide status filter for posts.

Posts don’t have a status; offering status options is confusing. Treat like events (no statuses).

Apply:

 		switch (typeFilter) {
 			case "project":
 				return ["active", "archived"];
 			case "task":
 				return ["todo", "done"];
 			case "tasklist":
 				return ["active", "archived"];
 			case "event":
 				return []; // Events don't have status
+			case "post":
+				return []; // Posts don't have status
 			default:
 				return ["active", "archived", "todo", "done"]; // All statuses
 		}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a22e9c and d0b1885.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx (1 hunks)
  • components/core/search-panel.tsx (12 hunks)
  • components/form/post.tsx (1 hunks)
  • components/layout/navbar.tsx (1 hunks)
  • components/project/posts/posts-list.tsx (1 hunks)
  • components/ui/tabs.tsx (1 hunks)
  • drizzle/0008_soft_serpent_society.sql (1 hunks)
  • drizzle/meta/0008_snapshot.json (1 hunks)
  • drizzle/meta/_journal.json (1 hunks)
  • drizzle/schema.ts (2 hunks)
  • lib/search/helpers.ts (2 hunks)
  • lib/search/index.ts (4 hunks)
  • package.json (2 hunks)
  • scripts/migrate-all-tenants.ts (1 hunks)
  • scripts/post-upgrade-maintenance.ts (1 hunks)
  • trpc/routers/_app.ts (2 hunks)
  • trpc/routers/posts.ts (1 hunks)
  • trpc/routers/search.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add comments that merely restate what the code does; avoid obvious, redundant comments
Write comments to explain WHY, not WHAT; add context such as constraints, decisions, performance, gotchas, or references
Remove or update outdated comments; keep comments synchronized with code changes
Prefer self-documenting code over comments: better naming, smaller functions, constants over magic values, and expressive types
Use TypeScript types/annotations to document data structures and contracts; JSDoc for public API documentation where needed

Files:

  • app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx
  • components/ui/tabs.tsx
  • trpc/routers/_app.ts
  • components/project/posts/posts-list.tsx
  • components/core/search-panel.tsx
  • lib/search/helpers.ts
  • drizzle/schema.ts
  • trpc/routers/posts.ts
  • components/layout/navbar.tsx
  • components/form/post.tsx
  • scripts/migrate-all-tenants.ts
  • trpc/routers/search.ts
  • scripts/post-upgrade-maintenance.ts
  • lib/search/index.ts
app/{(api),(dashboard),(legal)}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Organize Next.js routes within the defined App Router groups: (api), (dashboard), and (legal)

Files:

  • app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx
components/ui/**

📄 CodeRabbit inference engine (CLAUDE.md)

UI components are excluded from linting as configured; do not rely on linter coverage in components/ui

Files:

  • components/ui/tabs.tsx
trpc/routers/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use protectedProcedure for authenticated tRPC endpoints

Files:

  • trpc/routers/_app.ts
  • trpc/routers/posts.ts
  • trpc/routers/search.ts
{lib/search/**/*.ts,trpc/routers/search.ts}

📄 CodeRabbit inference engine (CLAUDE.md)

Use runAndLogError utility for error handling in search-related code

Files:

  • lib/search/helpers.ts
  • trpc/routers/search.ts
  • lib/search/index.ts
lib/search/helpers.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Keep shared search indexing and helper utilities in lib/search/helpers.ts

Files:

  • lib/search/helpers.ts
drizzle/schema.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Keep the main application database schema in drizzle/schema.ts (Drizzle ORM)

Files:

  • drizzle/schema.ts
lib/search/index.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Implement centralized search operations in lib/search/index.ts (SearchService)

Files:

  • lib/search/index.ts
🧠 Learnings (3)
📚 Learning: 2025-10-24T11:41:23.518Z
Learnt from: CR
PR: techulus/manage#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-24T11:41:23.518Z
Learning: Applies to lib/search/helpers.ts : Keep shared search indexing and helper utilities in lib/search/helpers.ts

Applied to files:

  • lib/search/helpers.ts
  • lib/search/index.ts
📚 Learning: 2025-10-24T11:41:23.518Z
Learnt from: CR
PR: techulus/manage#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-24T11:41:23.518Z
Learning: Applies to ops/drizzle/schema.ts : Keep the operations database schema in ops/drizzle/schema.ts (separate database)

Applied to files:

  • scripts/post-upgrade-maintenance.ts
📚 Learning: 2025-10-24T11:41:23.518Z
Learnt from: CR
PR: techulus/manage#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-24T11:41:23.518Z
Learning: Applies to lib/search/index.ts : Implement centralized search operations in lib/search/index.ts (SearchService)

Applied to files:

  • lib/search/index.ts
🧬 Code graph analysis (8)
app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx (5)
components/layout/page-title.tsx (1)
  • PageTitle (16-63)
components/core/section.tsx (1)
  • PageSection (3-48)
components/project/posts/posts-list.tsx (1)
  • PostsList (56-237)
components/core/empty-state.tsx (1)
  • EmptyState (3-38)
components/core/panel.tsx (1)
  • Panel (8-39)
trpc/routers/_app.ts (1)
trpc/routers/posts.ts (1)
  • postsRouter (14-356)
components/project/posts/posts-list.tsx (8)
lib/utils/error.ts (1)
  • displayMutationError (5-26)
drizzle/schema.ts (2)
  • post (285-301)
  • user (12-23)
components/core/user-avatar.tsx (1)
  • UserAvatar (5-25)
components/core/panel.tsx (1)
  • Panel (8-39)
components/layout/page-title.tsx (1)
  • PageTitle (16-63)
components/form/button.tsx (1)
  • DeleteButton (11-69)
components/core/html-view.tsx (1)
  • HtmlPreview (8-23)
components/project/comment/comments-section.tsx (1)
  • CommentsSection (12-46)
lib/search/helpers.ts (4)
lib/search/index.ts (2)
  • indexPost (240-284)
  • search (286-373)
drizzle/schema.ts (2)
  • post (285-301)
  • project (34-45)
lib/error.ts (1)
  • runAndLogError (1-9)
drizzle/types.ts (1)
  • Database (15-15)
trpc/routers/posts.ts (6)
trpc/init.ts (2)
  • createTRPCRouter (51-51)
  • protectedProcedure (55-76)
lib/permissions/index.ts (2)
  • canViewProject (68-86)
  • canEditProject (48-66)
drizzle/schema.ts (1)
  • post (285-301)
lib/activity/index.ts (1)
  • logActivity (17-46)
lib/search/helpers.ts (2)
  • deleteSearchItem (113-121)
  • indexPostWithProjectFetch (158-169)
lib/utils/mentionNotifications.ts (1)
  • sendMentionNotifications (19-82)
components/form/post.tsx (1)
lib/utils/error.ts (1)
  • displayMutationError (5-26)
trpc/routers/search.ts (3)
trpc/init.ts (2)
  • createTRPCRouter (51-51)
  • protectedProcedure (55-76)
drizzle/schema.ts (1)
  • post (285-301)
lib/error.ts (1)
  • runAndLogError (1-9)
lib/search/index.ts (1)
drizzle/schema.ts (2)
  • post (285-301)
  • project (34-45)
🪛 Biome (2.1.2)
components/project/posts/posts-list.tsx

[error] 103-106: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 103-106: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)

🔇 Additional comments (14)
package.json (2)

43-43: UI tabs dependency looks good

Adopting @radix-ui/react-tabs is fine and consistent with your UI stack. No concerns.


86-86: Canary dependency for resend — confirm intentionality and add test coverage

The canary version introduces pre-release stability risk, though the main v4→v6 breaking change (attachment field renaming) doesn't apply to current usage, which relies on react and text parameters. Confirm this canary version is intentional and consider pinning to a stable release. Additionally, add test coverage for the email sending functionality in:

  • app/(api)/api/jobs/account/mark-for-deletion/route.ts
  • app/(api)/api/jobs/daily-summary/route.ts
  • app/(api)/api/jobs/user/mark-for-deletion/route.ts
  • app/(api)/api/webhook/auth/route.ts
drizzle/meta/_journal.json (1)

60-67: Journal entry addition looks consistent

New entry aligns with 0008 migration tag. No action.

trpc/routers/_app.ts (1)

5-5: Exposing posts router in appRouter — good integration

Routes are now accessible under appRouter.posts. Matches the new CRUD router.

Also applies to: 18-19

drizzle/schema.ts (1)

303-312: Relations look correct

postRelations and projectRelations.posts wiring is coherent.

Also applies to: 55-56

trpc/routers/search.ts (2)

133-146: Post indexing path looks correct

Non-draft posts fetched with project relation; errors wrapped via runAndLogError; counts included.

Also applies to: 149-156


56-60: Access filter now covers posts — good

Filtering by accessibleProjectIds for non-project types is correct and includes posts.

components/layout/navbar.tsx (1)

97-103: Posts nav item matches existing route patterns; LGTM.

Active-state detection and href mirror Tasks/Events. No issues.

lib/search/helpers.ts (1)

148-156: Search helper aligns with error-handling guidance; LGTM.

Uses runAndLogError and typed inputs correctly.

Based on learnings

drizzle/meta/0008_snapshot.json (1)

514-633: Post table snapshot looks consistent with schema; LGTM.

PK, defaults, and FKs match drizzle/schema.ts definitions.

components/ui/tabs.tsx (1)

10-55: Radix Tabs wrappers are clean and idiomatic; LGTM.

Forwarded refs + class merging look good.

lib/search/index.ts (2)

2-2: "post" type wiring looks consistent.

Type unions and allowedTypes now include "post". Good parity with other entities.

Also applies to: 8-8, 289-289, 309-313, 358-360


309-313: Filter field scope is correct—no changes needed.

The verification confirms that filters targeting content fields (like type) should use unprefixed field names, exactly as implemented. Document indexing stores type, projectId, and status in the content object (not metadata), and the filter expressions type = '${options.type}' and similar are correctly scoped to those content fields. No migration to metadata is required.

components/core/search-panel.tsx (1)

40-41: UI additions for "post" look good.

Icon, labels, type unions, placeholder, grouping, and filter option are consistent with other types.

Also applies to: 60-62, 77-79, 112-114, 191-195, 211-215, 230-231, 320-323, 456-457

Comment on lines 80 to 93
const handleSubmit = (isDraft: boolean) => (formData: FormData) => {
const content = formData.get("content") as string;
const metadata = formData.get("metadata");

upsertPost.mutate({
title,
content,
metadata: metadata ? JSON.parse(metadata as string) : undefined,
category,
projectId: +projectId!,
id: item?.id ?? undefined,
isDraft,
});
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard JSON.parse to prevent runtime crashes on invalid metadata.

JSON parsing can throw; add a try/catch and fail fast instead of crashing the handler.

-      const metadata = formData.get("metadata");
-
-      upsertPost.mutate({
+      const metadataRaw = formData.get("metadata");
+      let parsedMetadata: unknown | undefined = undefined;
+      if (typeof metadataRaw === "string" && metadataRaw.trim()) {
+        try {
+          parsedMetadata = JSON.parse(metadataRaw);
+        } catch (e) {
+          console.error("Invalid metadata JSON", e);
+          return; // optionally show a toast here
+        }
+      }
+
+      upsertPost.mutate({
         title,
         content,
-        metadata: metadata ? JSON.parse(metadata as string) : undefined,
+        metadata: parsedMetadata,
         category,
         projectId: +projectId!,
         id: item?.id ?? undefined,
         isDraft,
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSubmit = (isDraft: boolean) => (formData: FormData) => {
const content = formData.get("content") as string;
const metadata = formData.get("metadata");
upsertPost.mutate({
title,
content,
metadata: metadata ? JSON.parse(metadata as string) : undefined,
category,
projectId: +projectId!,
id: item?.id ?? undefined,
isDraft,
});
};
const handleSubmit = (isDraft: boolean) => (formData: FormData) => {
const content = formData.get("content") as string;
const metadataRaw = formData.get("metadata");
let parsedMetadata: unknown | undefined = undefined;
if (typeof metadataRaw === "string" && metadataRaw.trim()) {
try {
parsedMetadata = JSON.parse(metadataRaw);
} catch (e) {
console.error("Invalid metadata JSON", e);
return; // optionally show a toast here
}
}
upsertPost.mutate({
title,
content,
metadata: parsedMetadata,
category,
projectId: +projectId!,
id: item?.id ?? undefined,
isDraft,
});
};
🤖 Prompt for AI Agents
In components/form/post.tsx around lines 80 to 93, the handler calls JSON.parse
on metadata without guarding against invalid JSON which can throw; wrap the
metadata parse in a try/catch, validate metadata is a string before parsing, and
on parse failure fail fast by returning early and surfacing an error (e.g., set
a form error or show an alert) instead of calling upsertPost.mutate; ensure
successful parsing passes the parsed object to mutate and that the catch avoids
throwing so the handler doesn't crash the app.

Comment on lines +68 to 73
const checkDb = await opsDb.execute(
sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
);

if (checkDb.count === 0) {
return { success: true, skipped: true };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

DB existence check is incorrect (uses .count) — may mis-handle missing DBs

Same bug as in maintenance script; check array length/rowCount.

-    const checkDb = await opsDb.execute(
-      sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
-    );
-
-    if (checkDb.count === 0) {
+    const checkDb = await opsDb.execute(
+      sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
+    );
+    const dbExists =
+      Array.isArray(checkDb) ? checkDb.length > 0 : (checkDb as any)?.rowCount > 0;
+    if (!dbExists) {
       return { success: true, skipped: true };
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const checkDb = await opsDb.execute(
sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
);
if (checkDb.count === 0) {
return { success: true, skipped: true };
const checkDb = await opsDb.execute(
sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
);
const dbExists =
Array.isArray(checkDb) ? checkDb.length > 0 : (checkDb as any)?.rowCount > 0;
if (!dbExists) {
return { success: true, skipped: true };
🤖 Prompt for AI Agents
In scripts/migrate-all-tenants.ts around lines 68 to 73, the DB existence check
incorrectly inspects checkDb.count which is not provided by the query result;
change the check to use the actual row count (e.g., checkDb.rowCount or
checkDb.rows.length depending on the DB client) and return skipped when that
count is 0. Ensure you use the correct property for the opsDb.execute result
type, and update any typing if necessary.

Comment on lines +66 to 71
const checkDb = await opsDb.execute(
sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
);

if (checkDb.count === 0) {
return { success: true, skipped: true };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

DB existence check is incorrect (uses non-existent .count) — can skip wrongly or always proceed

drizzle.execute for postgres-js returns rows (array), not an object with count. Use length/rowCount to determine existence.

Apply:

-    const checkDb = await opsDb.execute(
-      sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
-    );
-
-    if (checkDb.count === 0) {
+    const checkDb = await opsDb.execute(
+      sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
+    );
+    const dbExists =
+      Array.isArray(checkDb) ? checkDb.length > 0 : (checkDb as any)?.rowCount > 0;
+    if (!dbExists) {
       return { success: true, skipped: true };
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const checkDb = await opsDb.execute(
sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
);
if (checkDb.count === 0) {
return { success: true, skipped: true };
const checkDb = await opsDb.execute(
sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
);
const dbExists =
Array.isArray(checkDb) ? checkDb.length > 0 : (checkDb as any)?.rowCount > 0;
if (!dbExists) {
return { success: true, skipped: true };
🤖 Prompt for AI Agents
In scripts/post-upgrade-maintenance.ts around lines 66 to 71, the code checks
database existence using checkDb.count but drizzle.execute for postgres-js
returns an array of rows (not an object with count), so the condition can be
wrong; replace the .count check with a proper rows length check (e.g., treat the
result as an array and use checkDb.length === 0 or destructure rows and check
rows.length) and return skipped when length is 0, otherwise proceed.

Comment on lines +275 to +276
publishedAt: isDraft ? null : new Date(),
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t overwrite publishedAt on every edit. Only set on draft→published; clear on published→draft.

Current logic resets publish date on any non-draft update, losing original publish time.

Apply this update-branch change:

-			const postData = {
+			// Build base data; publishedAt patched below for updates
+			const postData = {
 				title,
 				content,
 				metadata,
 				category,
 				isDraft,
 				projectId,
-				publishedAt: isDraft ? null : new Date(),
+				publishedAt: isDraft ? null : new Date(), // used for create only
 			};

And in the update path (inside if (id) { ... }):

-				const updatedPost = await ctx.db
+				// Preserve/transition publish time
+				const nextPublishedAt =
+					existing.isDraft && !isDraft
+						? new Date()
+						: !existing.isDraft && isDraft
+						? null
+						: existing.publishedAt;
+
+				const updatedPost = await ctx.db
 					.update(post)
-					.set(postData)
+					.set({ ...postData, publishedAt: nextPublishedAt })
 					.where(eq(post.id, id))
 					.returning();

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In trpc/routers/posts.ts around lines 275-276, the current assignment always
sets publishedAt to new Date() for any non-draft save which overwrites the
original publish time; change this so that when creating/updating you only set
publishedAt to now if the post is transitioning from draft→published, preserve
existing.publishedAt for edits that keep it published, and set publishedAt to
null if transitioning from published→draft. In the update branch (inside if (id)
{ ... }) fetch the existing post, compare existing.isDraft vs incoming isDraft,
then: if existing.isDraft && !isDraft set publishedAt = new Date(); else if
(!existing.isDraft && isDraft) set publishedAt = null; else keep publishedAt =
existing.publishedAt.

Comment on lines +303 to +316
if (updatedPost?.[0] && !isDraft) {
await indexPostWithProjectFetch(ctx.db, ctx.search, updatedPost[0]);

if (content) {
await sendMentionNotifications(content, {
type: "post",
entityName: title,
entityId: id,
projectId,
orgSlug: ctx.orgSlug,
fromUserId: ctx.userId,
});
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

De-index when unpublishing (published → draft).

When a published post becomes draft, remove it from the search index to avoid stale public results.

Apply this after the update/insert operations:

-				if (updatedPost?.[0] && !isDraft) {
+				if (updatedPost?.[0] && !isDraft) {
 					await indexPostWithProjectFetch(ctx.db, ctx.search, updatedPost[0]);
@@
-				}
+				} else if (updatedPost?.[0] && isDraft) {
+					await deleteSearchItem(ctx.search, `post-${updatedPost[0].id}`, "post");
+				}
-				if (newPost?.[0] && !isDraft) {
+				if (newPost?.[0] && !isDraft) {
 					await indexPostWithProjectFetch(ctx.db, ctx.search, newPost[0]);
-				}
+				} // drafts are never indexed

Also applies to: 338-351

🤖 Prompt for AI Agents
In trpc/routers/posts.ts around lines 303-316 (and similarly 338-351), the code
indexes posts when updated but does not remove posts from the search index when
they transition from published to draft; update the post handling so that after
the update/insert you check the previous published state vs the new isDraft
flag: if the post was previously published and is now a draft, call the search
de-index function (e.g., removePostFromIndex or the counterpart to
indexPostWithProjectFetch) with the post id/project context to remove it from
the index; if the post is published (previously draft → published or still
published) keep the existing indexPostWithProjectFetch call; ensure this logic
is applied in both code paths at lines 303-316 and 338-351 so unpublished posts
are removed from search results.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
components/project/posts/posts-list.tsx (1)

85-88: A11y issue previously flagged.

This accessibility issue (interactive div should be a button) was already identified in a previous review. Please refer to the earlier comment for the proposed fix.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d0b1885 and 2e460a8.

📒 Files selected for processing (5)
  • app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx (1 hunks)
  • components/project/posts/posts-list.tsx (1 hunks)
  • drizzle/types.ts (3 hunks)
  • lib/activity/index.ts (1 hunks)
  • lib/utils/mentionNotifications.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add comments that merely restate what the code does; avoid obvious, redundant comments
Write comments to explain WHY, not WHAT; add context such as constraints, decisions, performance, gotchas, or references
Remove or update outdated comments; keep comments synchronized with code changes
Prefer self-documenting code over comments: better naming, smaller functions, constants over magic values, and expressive types
Use TypeScript types/annotations to document data structures and contracts; JSDoc for public API documentation where needed

Files:

  • drizzle/types.ts
  • app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx
  • lib/activity/index.ts
  • components/project/posts/posts-list.tsx
  • lib/utils/mentionNotifications.ts
app/{(api),(dashboard),(legal)}/**

📄 CodeRabbit inference engine (CLAUDE.md)

Organize Next.js routes within the defined App Router groups: (api), (dashboard), and (legal)

Files:

  • app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx
🧬 Code graph analysis (3)
drizzle/types.ts (1)
drizzle/schema.ts (1)
  • post (285-301)
app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx (5)
components/layout/page-title.tsx (1)
  • PageTitle (16-63)
components/core/section.tsx (1)
  • PageSection (3-48)
components/project/posts/posts-list.tsx (1)
  • PostsList (40-232)
components/core/empty-state.tsx (1)
  • EmptyState (3-38)
components/core/panel.tsx (1)
  • Panel (8-39)
components/project/posts/posts-list.tsx (9)
drizzle/types.ts (1)
  • PostWithCreator (73-75)
lib/utils/error.ts (1)
  • displayMutationError (5-26)
drizzle/schema.ts (2)
  • post (285-301)
  • user (12-23)
components/core/user-avatar.tsx (1)
  • UserAvatar (5-25)
components/core/panel.tsx (1)
  • Panel (8-39)
components/layout/page-title.tsx (1)
  • PageTitle (16-63)
components/form/button.tsx (1)
  • DeleteButton (11-69)
components/core/html-view.tsx (1)
  • HtmlPreview (8-23)
components/project/comment/comments-section.tsx (1)
  • CommentsSection (12-46)
🪛 Biome (2.1.2)
components/project/posts/posts-list.tsx

[error] 85-88: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 85-88: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)

🔇 Additional comments (2)
lib/activity/index.ts (1)

26-26: LGTM!

The addition of "post" to the activity type union correctly extends activity logging to support the new Post entity.

drizzle/types.ts (1)

9-9: LGTM!

The Post type definitions follow the established pattern used for other entities in the codebase and correctly align with the new post schema.

Also applies to: 26-26, 73-75


const { data: project } = useQuery(
trpc.projects.getProjectById.queryOptions({
id: +projectId!,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate projectId before using non-null assertions.

The pattern +projectId! is used repeatedly (lines 31, 37, 44, 74, 86) to coerce and assert the projectId parameter. This bypasses TypeScript's safety checks and could lead to runtime errors if projectId is undefined or not a valid numeric string.

Consider validating the parameter early in the component:

 export default function Posts() {
 	const { projectId, tenant } = useParams();
+	
+	if (!projectId || Array.isArray(projectId)) {
+		throw new Error("Invalid project ID");
+	}
+	
+	const numericProjectId = Number(projectId);
+	if (Number.isNaN(numericProjectId)) {
+		throw new Error("Project ID must be a number");
+	}
+	
 	const [create, setCreate] = useQueryState(
 		"create",
 		parseAsBoolean.withDefault(false),
 	);

Then use numericProjectId throughout instead of +projectId!.

Also applies to: 37-37, 44-44, 74-74, 86-86

🤖 Prompt for AI Agents
In app/(dashboard)/[tenant]/projects/[projectId]/posts/page.tsx around lines
31-86, the code uses the non-null assertion/coercion pattern `+projectId!` in
multiple places which bypasses TypeScript safety; validate and normalize
projectId once at the top (e.g., check that projectId exists, parse it to a
number with Number() or parseInt, verify it is not NaN and positive) and assign
to a local const like numericProjectId; if validation fails, return an
appropriate fallback (throw, return notFound(), or render an error) so runtime
errors are avoided, then replace all `+projectId!` occurrences with the
validated numericProjectId.

<div className="flex items-center gap-2">
<Badge
className={
categoryColors[post.category as keyof typeof categoryColors]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against undefined category colors.

The type assertions at lines 108 and 198 assume all post categories exist in categoryColors. If a post has a category not defined in the categoryColors object (lines 28-33), the lookup will return undefined, resulting in missing styles.

Consider adding a fallback color:

+const defaultCategoryColor = "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200";
+
 const categoryColors = {
 	announcement: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
 	fyi: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
 	question:
 		"bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200",
 };

Then use:

 <Badge
 	className={
-		categoryColors[post.category as keyof typeof categoryColors]
+		categoryColors[post.category as keyof typeof categoryColors] ?? defaultCategoryColor
 	}
 	variant="secondary"
 >

Apply the same pattern at line 197-199.

Also applies to: 197-199

🤖 Prompt for AI Agents
In components/project/posts/posts-list.tsx around lines 108 and 197-199, the
code assumes every post.category exists in categoryColors which can return
undefined and drop styles; update both lookups to use a safe fallback (e.g.,
const color = categoryColors[post.category as keyof typeof categoryColors] ??
fallbackColor) and then use that color variable in place of the direct lookup so
undefined categories fall back to a default color.

Copy link

@coderabbitai coderabbitai bot left a 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 (5)
lib/utils/mentionNotifications.ts (1)

68-71: LGTM! Post mention handling implemented correctly.

The post case follows the established pattern for other entity types, with appropriate message formatting and target URL construction.

components/form/post.tsx (2)

87-87: LGTM! Form ref implementation is correct.

The component properly uses a ref instead of document.querySelector, which improves reliability and testability. The ref is correctly typed and null-checked before use.

Also applies to: 105-105, 167-167, 179-179


89-102: Guard JSON.parse to prevent runtime crashes.

JSON parsing can throw on invalid input, causing the mutation to fail without proper error handling. Add try-catch to handle malformed metadata gracefully.

Apply this diff to add error handling:

 const savePost = (formData: FormData, isDraft = false) => {
   const content = formData.get("content") as string;
-  const metadata = formData.get("metadata");
+  const metadataRaw = formData.get("metadata");
+  let parsedMetadata: unknown | undefined = undefined;
+  
+  if (typeof metadataRaw === "string" && metadataRaw.trim()) {
+    try {
+      parsedMetadata = JSON.parse(metadataRaw);
+    } catch (e) {
+      console.error("Invalid metadata JSON", e);
+      displayMutationError({
+        message: "Invalid metadata format",
+      } as any);
+      return;
+    }
+  }
 
   upsertPost.mutate({
     title,
     content,
-    metadata: metadata ? JSON.parse(metadata as string) : undefined,
+    metadata: parsedMetadata,
     category,
     projectId: +projectId!,
     id: item?.id ?? undefined,
     isDraft,
   });
 };
components/project/posts/posts-list.tsx (2)

86-89: LGTM! Accessibility issue resolved.

The interactive element is now a semantic <button> with proper type, styling, and keyboard accessibility. This addresses the a11y concerns from the previous review.


108-115: Add fallback for undefined category colors.

If a post has a category not defined in categoryColors (lines 29-34), the lookup returns undefined, resulting in missing styles. Add a fallback to handle unexpected categories.

Apply this diff:

+const defaultCategoryColor = "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200";
+
 const categoryColors = {
   announcement: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
   fyi: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
   question:
     "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200",
 };

Then update both usages:

 <Badge
   className={
-    categoryColors[post.category as keyof typeof categoryColors]
+    categoryColors[post.category as keyof typeof categoryColors] ?? defaultCategoryColor
   }
   variant="secondary"
 >

Also apply the same pattern at lines 197-206.

🧹 Nitpick comments (1)
components/form/post.tsx (1)

121-134: Replace any with proper type for better type safety.

The onValueChange handler uses any, which bypasses type checking. Use the defined category union type instead.

Apply this diff:

 <Select
   name="category"
   value={category}
-  onValueChange={(value: any) => setCategory(value)}
+  onValueChange={(value: "announcement" | "fyi" | "question") => setCategory(value)}
 >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2e460a8 and 34da2f3.

📒 Files selected for processing (4)
  • components/form/post.tsx (1 hunks)
  • components/project/posts/posts-list.tsx (1 hunks)
  • lib/utils/mentionNotifications.ts (2 hunks)
  • trpc/routers/posts.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • trpc/routers/posts.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never add comments that merely restate what the code does; avoid obvious, redundant comments
Write comments to explain WHY, not WHAT; add context such as constraints, decisions, performance, gotchas, or references
Remove or update outdated comments; keep comments synchronized with code changes
Prefer self-documenting code over comments: better naming, smaller functions, constants over magic values, and expressive types
Use TypeScript types/annotations to document data structures and contracts; JSDoc for public API documentation where needed

Files:

  • components/project/posts/posts-list.tsx
  • components/form/post.tsx
  • lib/utils/mentionNotifications.ts
🧬 Code graph analysis (2)
components/project/posts/posts-list.tsx (9)
drizzle/types.ts (1)
  • PostWithCreator (73-75)
lib/utils/error.ts (1)
  • displayMutationError (5-26)
drizzle/schema.ts (2)
  • post (285-301)
  • user (12-23)
components/core/user-avatar.tsx (1)
  • UserAvatar (5-25)
components/core/panel.tsx (1)
  • Panel (8-39)
components/layout/page-title.tsx (1)
  • PageTitle (16-63)
components/form/button.tsx (1)
  • DeleteButton (11-69)
components/core/html-view.tsx (1)
  • HtmlPreview (8-23)
components/project/comment/comments-section.tsx (1)
  • CommentsSection (12-46)
components/form/post.tsx (1)
lib/utils/error.ts (1)
  • displayMutationError (5-26)
🔇 Additional comments (1)
components/project/posts/posts-list.tsx (1)

41-234: Well-structured component with proper state management.

The component demonstrates good practices:

  • Proper React Query usage with conditional fetching
  • Correct cache invalidation on mutations
  • Authorization checks before allowing edit/delete
  • Clean separation between viewing and editing modes

@arjunkomath arjunkomath merged commit bf29bd7 into main Oct 25, 2025
3 of 5 checks passed
@arjunkomath arjunkomath deleted the feat/posts branch October 25, 2025 11:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants