Guidelines for AI coding agents working on this codebase.
Always use the pluralize() helper from ~/lib/utils when displaying counts with text labels. Never hardcode plural forms.
// ✅ Correct
import { pluralize } from "~/lib/utils";
<span>{count} {pluralize(count, "post")}</span>
<span>{count} {pluralize(count, "reply", "replies")}</span>
// ❌ Wrong - hardcoded plural
<span>{count} posts</span>
<span>{count} members</span>The pluralize(count, singular, plural?) function:
- Returns singular form when count === 1
- Returns plural form (or singular + "s") otherwise
- Supports irregular plurals via optional third argument
- Use
"use client"directive for interactive components - Keep server components as the default where possible
- Place shared UI components in
src/components/ui/ - Feature-specific components go in their respective folders (e.g.,
src/components/feed/)
- Use path aliases:
~/maps tosrc/ - Import order: React → Next.js → external libs → internal modules → types
- Prefer named exports over default exports for components
- Always type component props with interfaces
- Use
unknownfor JSON data from the database (e.g., Lexical editor state) - Prefer explicit types over
any
- Use tRPC for all API calls via
apifrom~/lib/trpc/client - Use React Query's infinite queries for paginated data
- Handle loading and error states explicitly
- Use Tailwind CSS classes
- Use
cn()from~/lib/utilsfor conditional class merging - Follow shadcn/ui patterns for custom components
Destructive/negative actions with red text must have proper hover/focus states to remain readable:
// ✅ Correct - red background with white text on hover/focus
<DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-destructive-foreground">
Delete item
</DropdownMenuItem>
<Button className="text-destructive hover:bg-destructive hover:text-destructive-foreground">
Sign out
</Button>
// ❌ Wrong - red text stays red on dark hover background, becomes unreadable
<DropdownMenuItem className="text-destructive focus:text-destructive">
Delete item
</DropdownMenuItem>
<Button className="text-destructive hover:text-destructive">
Sign out
</Button>Rule: Elements with text-destructive in default state should use focus:bg-destructive focus:text-destructive-foreground (for menu items) or hover:bg-destructive hover:text-destructive-foreground (for buttons) in their interactive states.
src/
├── app/ # Next.js App Router pages
│ ├── (auth)/ # Auth pages (public)
│ ├── (main)/ # Protected pages
│ └── api/ # API routes (tRPC, NextAuth)
├── components/ # React components
├── server/ # Server-side code
│ └── api/routers/ # tRPC routers
├── lib/ # Utilities and hooks
└── middleware.ts # Auth middleware
- Use Vitest for unit tests
- Place test files alongside source files with
.test.tsextension - Run tests with
npm test
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
api.post.list.useInfiniteQuery(
{ limit: 10 },
{ getNextPageParam: (lastPage) => lastPage.nextCursor }
);
const items = data?.pages.flatMap((page) => page.items) ?? [];Use formatRelativeTime() from ~/lib/utils for consistent relative dates.
Private R2 URLs need signing. Use api.upload.getDownloadUrl.useQuery() for attachments and cover images.