Skip to content

Split plaintiffs from people directory#48

Merged
mikepsinn merged 2 commits into
mainfrom
feature/plaintiffs-people-directory
May 6, 2026
Merged

Split plaintiffs from people directory#48
mikepsinn merged 2 commits into
mainfrom
feature/plaintiffs-people-directory

Conversation

@mikepsinn
Copy link
Copy Markdown
Owner

@mikepsinn mikepsinn commented May 6, 2026

Summary

  • Adds /plaintiffs and /plaintiffs/manage for the Humanity v. Government plaintiff intake, gallery, and management flow.
  • Reframes /people as a searchable coordination directory for humans with assigned public tasks, role filters, profile/task links, and bottom pagination.
  • Redirects legacy /people/manage to /plaintiffs/manage and points memorial/plaintiff CTAs at /plaintiffs.

Validation

  • pnpm --filter @optimitron/web run typecheck:fast
  • pnpm --filter @optimitron/web exec vitest run src/lib/__tests__/url.test.ts src/config/__tests__/site-variant-ui.test.ts
  • Playwright screenshots: packages/web/output/playwright/route-split-audit/review.html

Screenshot Notes

  • Checked desktop/mobile /people, /people?role=legal&q=law, /plaintiffs, /plaintiffs/manage, and /people/[id].
  • /people is intentionally now a coordination directory; /plaintiffs keeps the treaty-for-someone-who-can't conversion flow.

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced a dedicated Plaintiffs section with a public gallery and management interface for registered plaintiffs.
    • Redesigned People directory with an improved card-based layout, enhanced search, and role filtering capabilities.
    • Updated navigation throughout the app to clearly distinguish between the new Plaintiffs and People sections.
    • Streamlined the plaintiffs registration and management workflow.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
optimitron-wishonia Ignored Ignored May 6, 2026 6:27am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Warning

Rate limit exceeded

@mikepsinn has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 43 minutes and 9 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0cd5e168-ea4c-4d66-9bb5-d3ff2c8e219c

📥 Commits

Reviewing files that changed from the base of the PR and between 13648d9 and 83a2c19.

📒 Files selected for processing (6)
  • packages/web/src/app/people/manage/page.tsx
  • packages/web/src/app/people/page.tsx
  • packages/web/src/app/plaintiffs/manage/page.tsx
  • packages/web/src/app/plaintiffs/page.tsx
  • packages/web/src/components/referendum/VoteCounterSplit.tsx
  • packages/web/src/lib/people-directory.server.ts
📝 Walkthrough

Walkthrough

The PR introduces a dedicated "Plaintiffs" feature with new /plaintiffs and /plaintiffs/manage routes. It restructures plaintiff management by moving logic from /people/manage to /plaintiffs/manage, rewrites the /people page as a searchable directory with card-based layout and role filtering, and updates navigation throughout the site to route to plaintiffs instead of people where appropriate.

Changes

Plaintiffs Feature and People Directory Restructuring

Layer / File(s) Summary
Route & Navigation Definitions
packages/web/src/lib/routes.ts, packages/web/src/lib/site-sitemap.ts
New ROUTES entries plaintiffs ("/plaintiffs") and plaintiffsManage ("/plaintiffs/manage") added. New nav items plaintiffsLink, plaintiffsManageLink, and peopleLink introduced. peopleManageLink aliased to plaintiffsManageLink. Primary nav updated to include peopleLink.
Data & Types Layer
packages/web/src/lib/people-directory.server.ts
New server module exports PeopleDirectoryRole, PeopleDirectoryPerson, PeopleDirectoryData types and getPeopleDirectoryData(), parsePeopleDirectoryRole() functions. Implements Prisma queries with role and search filtering, pagination, and task preview mapping.
Plaintiffs Gallery Page
packages/web/src/app/plaintiffs/page.tsx
New page renders public plaintiffs gallery with sorting, filtering, metadata generation, and pagination via PersonFaceTile grid. Includes RepresentedPersonConversionForm and contextual statistics sections.
Plaintiffs Management Page
packages/web/src/app/plaintiffs/manage/page.tsx
New page enforces authentication, fetches user's represented plaintiffs via Prisma with rich nested relations, implements pagination (5 items/page), and renders ManageRepresentedPeopleClient with edit/delete controls and contextual header.
People Directory Page
packages/web/src/app/people/page.tsx
Rewritten to use card-based directory layout with PersonDirectoryCard component. Adds role filters, search form, and pagination controls. Switches data source from gallery to getPeopleDirectoryData() with PeopleDirectoryPerson types. Removes site metadata generation.
People Manage Redirect
packages/web/src/app/people/manage/page.tsx
Converted from full-featured management page to minimal redirect: exports LegacyPeopleManagePage() that redirects to ROUTES.plaintiffsManage.
URL Helpers
packages/web/src/lib/url.ts
Added new buildPlaintiffsUrl() function to generate plaintiffs route URL. Updated buildPeopleUrl() documentation.
Navigation Link Updates
packages/web/src/app/governments/[code]/page.tsx, packages/web/src/app/people/[id]/page.tsx, packages/web/src/components/landing/*, packages/web/src/components/medical/medical-pages.tsx, packages/web/src/components/people/*, packages/web/src/components/referendum/VoteCounterSplit.tsx
Multiple files updated to route to ROUTES.plaintiffs / ROUTES.plaintiffsManage instead of ROUTES.people / ROUTES.peopleManage. Includes metadata titles, header navigation, CTA buttons, and conditional links using plaintiffsLink label.
Site Configuration
packages/web/src/lib/site.ts
Added plaintiffsLink to route-policy prefixes (canonicalPrefixes, publicPrefixes) and campaign/nav block items across multiple site variants (One Percent, War on Disease). Replaced peopleLink usage in several menus.
Tests & Documentation
packages/web/src/lib/__tests__/url.test.ts
Added test for buildPlaintiffsUrl() validating base URL concatenation with ROUTES.plaintiffs.

Sequence Diagram

sequenceDiagram
    participant User
    participant PeoplePage as People<br/>Directory
    participant PlaintiffsPage as Plaintiffs<br/>Gallery
    participant PlaintiffsManage as Plaintiffs<br/>Manage
    participant DataLayer as getPeopleDirectory<br/>Data()
    participant DB as Database

    User->>PeoplePage: Visit /people with filters
    PeoplePage->>DataLayer: getPeopleDirectoryData({role, query, page})
    DataLayer->>DB: Prisma query with role/search filters
    DB-->>DataLayer: Matching people records
    DataLayer-->>PeoplePage: PeopleDirectoryData (paginated)
    PeoplePage-->>User: Render directory cards with filters

    User->>PlaintiffsPage: Visit /plaintiffs
    PlaintiffsPage->>DB: getRepresentedPeopleGalleryData()
    DB-->>PlaintiffsPage: Plaintiffs records
    PlaintiffsPage-->>User: Render plaintiffs gallery + conversion form

    User->>PlaintiffsManage: Visit /plaintiffs/manage
    PlaintiffsManage->>DB: Fetch user's represented plaintiffs (auth required)
    DB-->>PlaintiffsManage: Rich person data with nested relations
    PlaintiffsManage-->>User: Render ManageRepresentedPeopleClient with edit/delete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • mikepsinn/optimitron#47: Modifies the same pages (people/[id]/page.tsx, people/manage/page.tsx, routes, ManageRepresentedPeopleClient) for plaintiffs workflow.
  • mikepsinn/optimitron#43: Updates the same feature surface (routes, people/plaintiffs pages, conversion/manage components) and URL helpers.

Poem

🐰 A carrot for the plaintiffs, hooray!
New routes and pages light the way,
From people to directory we've grown,
With search and filter, fully shown,
The humans shine with cards so bright,
Justice sorted, organized right! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Split plaintiffs from people directory' directly and concisely describes the main change: separating the plaintiffs route and management flow from the people directory, which is the core objective of this changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/plaintiffs-people-directory

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
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 13648d994b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/web/src/app/people/manage/page.tsx Outdated
Copy link
Copy Markdown

@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

🧹 Nitpick comments (12)
packages/web/src/components/referendum/VoteCounterSplit.tsx (1)

15-20: ⚡ Quick win

Stale JSDoc references /people.

The prop comment still describes the link as going to /people and "Invisible Graveyard", but the link now points at ROUTES.plaintiffs. Worth updating so the prop name (linkMemorialToPeople) and the doc don't mislead future readers.

📝 Suggested doc + prop rename
   /**
-   * If true, wrap the memorial line in a Link to /people so people can scroll
-   * through the Invisible Graveyard. Default false to keep the component
-   * passive when memorial linkage isn't desired.
+   * If true, wrap the memorial line in a Link to /plaintiffs so people can
+   * scroll through the plaintiff roster. Default false to keep the component
+   * passive when memorial linkage isn't desired.
    */
-  linkMemorialToPeople?: boolean;
+  linkMemorialToPlaintiffs?: boolean;

Rename callers accordingly (or keep the prop name and just fix the doc if rename churn is undesired).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/components/referendum/VoteCounterSplit.tsx` around lines 15
- 20, The JSDoc for the VoteCounterSplit prop linkMemorialToPeople is stale
(mentions `/people` and "Invisible Graveyard") while the implementation uses
ROUTES.plaintiffs; update the doc to accurately describe that enabling this prop
wraps the memorial line in a Link to ROUTES.plaintiffs and remove references to
`/people`/Invisible Graveyard, and optionally rename the prop to
linkMemorialToPlaintiffs (or keep the prop name but ensure all callers and tests
reflect the new semantics if you choose to rename); if you rename
linkMemorialToPeople to linkMemorialToPlaintiffs, update all usages across the
codebase to the new prop name and adjust any related types/props interfaces
accordingly.
packages/web/src/lib/people-directory.server.ts (1)

137-169: 💤 Low value

Name the page-size constants.

limit = 36, take clamp Math.min(Math.max(limit, 1), 60), and take: 8 for the task preview are magic numbers spread across the function. Pulling them into module-level constants (e.g. DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, TASK_PREVIEW_FETCH_SIZE) makes the bounds reviewable in one place and matches the project's "named constants for magic numbers" rule.

As per coding guidelines: "Use named constants for magic numbers and cite the paper source".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/lib/people-directory.server.ts` around lines 137 - 169,
Introduce module-level named constants for the page-size magic numbers and use
them in getPeopleDirectoryData: add DEFAULT_PAGE_SIZE = 36, MAX_PAGE_SIZE = 60
and TASK_PREVIEW_FETCH_SIZE = 8 at the top of the module, replace the default
param limit = 36 with limit = DEFAULT_PAGE_SIZE, replace the clamp
Math.min(Math.max(limit, 1), 60) assigned to take with Math.min(Math.max(limit,
1), MAX_PAGE_SIZE) (or a helper clamp using DEFAULT_PAGE_SIZE/MAX_PAGE_SIZE),
and replace any hardcoded take: 8 used for task preview fetches with
TASK_PREVIEW_FETCH_SIZE; update references in getPeopleDirectoryData (symbols:
getPeopleDirectoryData, limit, take) and the task-preview fetch site to use
these new constants.
packages/web/src/app/people/page.tsx (5)

17-25: 💤 Low value

ROLE_FILTERS encodes role keys as bare strings.

The labels/values are already typed via PeopleDirectoryRole, so this is mostly cosmetic, but as per coding guidelines: "Use enums instead of magic strings in TypeScript code." Centralizing the role key constants (e.g., as a const object exported from people-directory.server.ts alongside parsePeopleDirectoryRole) makes call-site references symbolic and reduces drift if a role key is renamed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/people/page.tsx` around lines 17 - 25, ROLE_FILTERS uses
literal string role keys; replace those magic strings with centralized
constants: export a const enum-like object or string consts (e.g., ROLE_KEYS)
from people-directory.server.ts alongside parsePeopleDirectoryRole, then update
ROLE_FILTERS to reference those exported symbols instead of raw strings (e.g.,
use ROLE_KEYS.all, ROLE_KEYS.officials, etc.) and keep the label values the same
so types (PeopleDirectoryRole) still align.

142-155: 💤 Low value

Repeated indexed access on verifiedTaskPreview[0].

person.verifiedTaskPreview[0] is read three times in this branch (the conditional, the href, and the title), each of which re-incurs the noUncheckedIndexedAccess undefined check. Destructuring once keeps the JSX tighter and avoids a stale-index footgun if the array shape ever changes:

♻️ Suggested change
-      ) : person.verifiedTaskPreview[0] ? (
-        <section className="mt-4 border-t border-border pt-4">
-          <p className="text-xs font-black uppercase tracking-[0.14em] text-muted-foreground">
-            Verified work
-          </p>
-          <Link
-            className="mt-2 block font-black leading-6 underline-offset-4 hover:underline"
-            href={`${ROUTES.tasks}/${person.verifiedTaskPreview[0].id}`}
-          >
-            {person.verifiedTaskPreview[0].title}
-          </Link>
-        </section>
-      ) : null}
+      ) : null}

…and hoist the verified task above the JSX:

   const topTask = person.openTaskPreview[0] ?? null;
+  const verifiedTask = person.verifiedTaskPreview[0] ?? null;

then render with verifiedTask instead of repeated person.verifiedTaskPreview[0].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/people/page.tsx` around lines 142 - 155, Hoist the first
verified task into a local variable to avoid repeated indexed access: before the
JSX branch reference person.verifiedTaskPreview, create a const like
verifiedTask = person.verifiedTaskPreview[0] and then replace all uses of
person.verifiedTaskPreview[0] in this branch (the conditional, the Link href,
and the Link text) with verifiedTask so you only perform the index access once
and make the JSX clearer.

62-68: ⚡ Quick win

parsePage duplicated across three pages.

The same parsePage helper is reproduced verbatim here, in packages/web/src/app/plaintiffs/page.tsx (lines 99-103, inline), and in packages/web/src/app/plaintiffs/manage/page.tsx (lines 29-35). As per coding guidelines: "Extract copy-paste code to shared functions." Consider extracting to a shared module (e.g., @/lib/url) so the parsing rules stay aligned.

♻️ Suggested extraction
// packages/web/src/lib/search-params.ts
export function parsePageParam(value: string | string[] | undefined): number {
  const raw = Number.parseInt(
    Array.isArray(value) ? (value[0] ?? "1") : (value ?? "1"),
    10,
  );
  return Number.isFinite(raw) && raw > 0 ? raw : 1;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/people/page.tsx` around lines 62 - 68, The parsePage
helper is duplicated across multiple pages (function parsePage in
packages/web/src/app/people/page.tsx and identical copies in plaintiffs pages);
extract it to a single shared utility (e.g., export function
parsePageParam(value: string | string[] | undefined): number in a new module
like packages/web/src/lib/search-params.ts) and replace the inline parsePage
definitions by importing parsePageParam where used (pages: people/page.tsx,
plaintiffs/page.tsx, plaintiffs/manage/page.tsx) so all callers use the same
exported function and signature.

228-249: 💤 Low value

Search form drops page and other filter state on submit.

The <form action={ROUTES.people}> only carries the hidden role input and the q field. When the user submits a search from page 5, the new request goes to page 1 (expected), but if any future filters are added (e.g., country, jurisdiction) they will silently be wiped on every search. Worth a brief comment near this form or a small helper that mirrors the active filter set into hidden inputs the way buildDirectoryHref does — to prevent future regressions when more filters land.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/people/page.tsx` around lines 228 - 249, The form at the
People page (form with action={ROUTES.people}) only submits q and role and
therefore drops other active filters and pagination state; update the form
submission to include hidden inputs for every active filter the page currently
uses (mirror the same logic buildDirectoryHref uses) so future filters like
country, jurisdiction, or page are preserved (except explicitly reset page to 1
on new searches if desired); locate the form in
packages/web/src/app/people/page.tsx and add hidden inputs for each filter
variable (e.g., country, jurisdiction, page, etc.) or create a small helper that
iterates current filter state and renders <input type="hidden" name="..."
value="..."> for each to ensure all active filters are carried on submit.

86-90: 💤 Low value

tags typed as (string | null)[] after .filter(Boolean).

Array.prototype.filter(Boolean) does not narrow the result type in TypeScript — tags will still be (string | null)[], even though at runtime nulls are stripped. With strict mode this happens to work for .join but defeats the purpose of .filter. Either use a typed predicate or build the array conditionally:

♻️ Suggested change
-  const tags = [
-    person.isPublicFigure ? "Public official" : null,
-    person.countryCode,
-    `${person.publicTaskCount} task${person.publicTaskCount === 1 ? "" : "s"}`,
-  ].filter(Boolean);
+  const tags: string[] = [
+    person.isPublicFigure ? "Public official" : null,
+    person.countryCode ?? null,
+    `${person.publicTaskCount} task${person.publicTaskCount === 1 ? "" : "s"}`,
+  ].filter((tag): tag is string => Boolean(tag));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/people/page.tsx` around lines 86 - 90, The tags array
currently uses .filter(Boolean) which doesn't narrow away null in TypeScript,
leaving tags typed as (string | null)[]; change construction so tags is typed as
string[] by either (a) build the array conditionally (only push the "Public
official" string when person.isPublicFigure is true and include
person.countryCode and the task-count string unconditionally) or (b) use a typed
predicate filter (e.g., a function isString(x): x is string) when filtering;
update the variable declaration for tags so its inferred type is string[] and
ensure all references (tags, person.isPublicFigure, person.countryCode,
person.publicTaskCount) still compile.
packages/web/src/app/plaintiffs/page.tsx (4)

113-132: 💤 Low value

Hard-coded page size duplicated across the file.

pageSize: 24 (line 115) and the threshold filteredCount >= 24 (line 132) both encode the same constant. If the page size ever changes, the browse-tools threshold drifts silently.

♻️ Extract a named constant
+const PLAINTIFFS_PAGE_SIZE = 24;
@@
-        pageSize: 24,
+        pageSize: PLAINTIFFS_PAGE_SIZE,
@@
-  const showBrowseTools = hasActiveBrowseState || filteredCount >= 24;
+  const showBrowseTools = hasActiveBrowseState || filteredCount >= PLAINTIFFS_PAGE_SIZE;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/plaintiffs/page.tsx` around lines 113 - 132, Extract the
duplicated numeric literal 24 into a single named constant (e.g., PAGE_SIZE) and
use that constant wherever the page size or threshold is referenced;
specifically replace the inline pageSize: 24 in the data fetch call and the
filteredCount >= 24 check used for showBrowseTools, ensuring
hasActiveBrowseState and currentPage logic remain unchanged and any other
occurrences of the same literal in this file (or nearby components) use
PAGE_SIZE too.

23-49: ⚡ Quick win

Prefer a typed enum/object for sort keys to avoid magic strings.

VALID_SORT_KEYS is a tuple of bare string literals reused across parseSort and the URL surface. As per coding guidelines: "Use enums instead of magic strings in TypeScript code". A const-asserted record or enum lets call sites reference the keys symbolically and avoids drift between this list and RepresentedPeopleSortKey.

♻️ Suggested approach
-const VALID_SORT_KEYS: RepresentedPeopleSortKey[] = [
-  "recent",
-  "oldest",
-  "alphabetical",
-  "died-closest-to-cure",
-];
+const SORT_KEYS = {
+  RECENT: "recent",
+  OLDEST: "oldest",
+  ALPHABETICAL: "alphabetical",
+  DIED_CLOSEST_TO_CURE: "died-closest-to-cure",
+} as const satisfies Record<string, RepresentedPeopleSortKey>;
+
+const VALID_SORT_KEYS = Object.values(SORT_KEYS);

As per coding guidelines: "Use enums instead of magic strings in TypeScript code".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/plaintiffs/page.tsx` around lines 23 - 49, Replace the
magic-string list with a typed symbol collection and use it everywhere: define a
const enum or a const-record (e.g., SortKey and SORT_KEYS) whose keys/values map
to the same values as RepresentedPeopleSortKey, then change VALID_SORT_KEYS to
derive from that symbol collection and update parseSort to validate against
SORT_KEYS (and return a member of SortKey/RepresentedPeopleSortKey), and ensure
parseEnum's allowed parameter is typed from the same symbol collection so
callers use the enum/record symbols instead of raw strings (refer to
VALID_SORT_KEYS, parseSort, parseEnum, and RepresentedPeopleSortKey when making
these replacements).

134-139: 💤 Low value

Filter loop preserves unknown query params.

Object.entries(params) will carry over any non-pagination query key (e.g., edit, tracking params, stale keys) into pagination links. Consider whitelisting the keys that participate in filtering (sort, cause, conditionId, conflictId, country, efficacyLag) to avoid leaking unrelated state into navigation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/plaintiffs/page.tsx` around lines 134 - 139, The loop in
page.tsx that iterates over Object.entries(params) (the for...of block setting
filterParams) currently copies any non-"page" query keys into pagination links;
restrict it to a whitelist of valid filter keys (e.g., "sort", "cause",
"conditionId", "conflictId", "country", "efficacyLag") before calling
filterParams.set, keeping the existing logic for handling array values (use
first element) and excluding empty strings and "page". Update the condition in
that loop to check membership in the allowed-keys set so unrelated query params
(like "edit" or tracking tags) are not propagated.

60-72: 💤 Low value

Consider getRouteMetadata for consistency with sibling pages.

packages/web/src/app/plaintiffs/manage/page.tsx uses getRouteMetadata({ ...plaintiffsManageLink }) while this page reconstructs the title/description manually via getSiteMetadata. As per coding guidelines: "Use getRouteMetadata() for page metadata instead of hardcoding titles in Next.js App Router pages and API routes." If site-aware metadata is required here (multi-tenant sites), a comment explaining the divergence helps; otherwise migrate to getRouteMetadata for the single-source-of-truth on page titles/descriptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/plaintiffs/page.tsx` around lines 60 - 72, The current
generateMetadata function builds title/description via getSiteMetadata using
headers() and getSiteFromHeaders, but sibling page uses getRouteMetadata({
...plaintiffsManageLink }); either replace the manual construction with a
single-source call to getRouteMetadata({ ...plaintiffsLink }) (and use
ROUTES.plaintiffs where appropriate) to match other pages, or if
multi-tenant/site-aware metadata is required keep getSiteMetadata but add a
clear comment above generateMetadata explaining why it diverges from
getRouteMetadata (mentioning headers(), getSiteFromHeaders(), and
plaintiffsLink) so reviewers know the difference.
packages/web/src/app/plaintiffs/manage/page.tsx (1)

103-107: 💤 Low value

Type-narrowing of userId after redirect.

redirect() is typed as never in Next.js, so TypeScript should narrow userId to string below. With strict mode + noUncheckedIndexedAccess this is fine, but if you ever see TS complaining about userId possibly being undefined further down (e.g., inside the where clause at line 122), an explicit assertion (if (!userId) { redirect(...); return; }) makes the narrowing unambiguous.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/web/src/app/plaintiffs/manage/page.tsx` around lines 103 - 107, The
check that calls redirect(getSignInPath(ROUTES.plaintiffsManage)) doesn't make
the TypeScript narrowing of session?.user.id (userId) unambiguous in all
compiler configurations; update the guard in the async page function around
getServerSession(authOptions) so that after calling redirect(...) you also stop
execution (e.g., return) or use an explicit assertion pattern (if (!userId) {
redirect(...); return; }) so that userId is definitely narrowed to string for
subsequent usage (e.g., the where clause that references userId).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/web/src/app/plaintiffs/manage/page.tsx`:
- Around line 197-230: The fallback value for memorial cause category uses the
literal "UNKNOWN" which can drift from the enum; update the mapping in
editablePeople so person.memorial?.causeCategory falls back to the enum constant
(PersonDeathCauseCategory.UNKNOWN) instead of the string literal, ensuring
imports include PersonDeathCauseCategory and updating the expression that
assigns causeCategory to use that enum symbol.

In `@packages/web/src/app/plaintiffs/page.tsx`:
- Around line 140-145: buildPageUrl currently returns a bare "?" when there are
no filterParams and target === 1, which leads to surprising Link navigation;
update buildPageUrl to return the current pathname (no "?" query) when qs is
empty instead of "?", e.g. obtain the pathname via Next.js usePathname() at the
top of the component and have buildPageUrl return that pathname when
next.toString() is empty, keeping the existing behavior of `?${qs}` when qs
exists; this change targets the buildPageUrl function and the filterParams
usage.

In `@packages/web/src/lib/people-directory.server.ts`:
- Around line 183-241: The assignedTasks relation selection currently returns up
to 8 rows ordered by status, so post-fetch filters like the verifiedTaskPreview
(.filter(...).slice(...)) can be empty when the 8 rows are dominated by the
other status; change the query to request two separate, status-scoped selections
instead of one shared assignedTasks window — e.g. add two separate relation
selects (or aliased includes) like assignedTasksActive (where: { status:
TaskStatus.ACTIVE }, take: 3, orderBy: { createdAt: "desc" }) and
assignedTasksVerified (where: { status: TaskStatus.VERIFIED }, take: 2, orderBy:
{ createdAt: "desc" }) and then map openTaskPreview from assignedTasksActive and
verifiedTaskPreview from assignedTasksVerified (removing the post-fetch
.filter/.slice). Ensure you keep publicAssignedTaskWhere/_count usage as needed
and update references to person.assignedTasks to the new aliased fields.

---

Nitpick comments:
In `@packages/web/src/app/people/page.tsx`:
- Around line 17-25: ROLE_FILTERS uses literal string role keys; replace those
magic strings with centralized constants: export a const enum-like object or
string consts (e.g., ROLE_KEYS) from people-directory.server.ts alongside
parsePeopleDirectoryRole, then update ROLE_FILTERS to reference those exported
symbols instead of raw strings (e.g., use ROLE_KEYS.all, ROLE_KEYS.officials,
etc.) and keep the label values the same so types (PeopleDirectoryRole) still
align.
- Around line 142-155: Hoist the first verified task into a local variable to
avoid repeated indexed access: before the JSX branch reference
person.verifiedTaskPreview, create a const like verifiedTask =
person.verifiedTaskPreview[0] and then replace all uses of
person.verifiedTaskPreview[0] in this branch (the conditional, the Link href,
and the Link text) with verifiedTask so you only perform the index access once
and make the JSX clearer.
- Around line 62-68: The parsePage helper is duplicated across multiple pages
(function parsePage in packages/web/src/app/people/page.tsx and identical copies
in plaintiffs pages); extract it to a single shared utility (e.g., export
function parsePageParam(value: string | string[] | undefined): number in a new
module like packages/web/src/lib/search-params.ts) and replace the inline
parsePage definitions by importing parsePageParam where used (pages:
people/page.tsx, plaintiffs/page.tsx, plaintiffs/manage/page.tsx) so all callers
use the same exported function and signature.
- Around line 228-249: The form at the People page (form with
action={ROUTES.people}) only submits q and role and therefore drops other active
filters and pagination state; update the form submission to include hidden
inputs for every active filter the page currently uses (mirror the same logic
buildDirectoryHref uses) so future filters like country, jurisdiction, or page
are preserved (except explicitly reset page to 1 on new searches if desired);
locate the form in packages/web/src/app/people/page.tsx and add hidden inputs
for each filter variable (e.g., country, jurisdiction, page, etc.) or create a
small helper that iterates current filter state and renders <input type="hidden"
name="..." value="..."> for each to ensure all active filters are carried on
submit.
- Around line 86-90: The tags array currently uses .filter(Boolean) which
doesn't narrow away null in TypeScript, leaving tags typed as (string | null)[];
change construction so tags is typed as string[] by either (a) build the array
conditionally (only push the "Public official" string when person.isPublicFigure
is true and include person.countryCode and the task-count string
unconditionally) or (b) use a typed predicate filter (e.g., a function
isString(x): x is string) when filtering; update the variable declaration for
tags so its inferred type is string[] and ensure all references (tags,
person.isPublicFigure, person.countryCode, person.publicTaskCount) still
compile.

In `@packages/web/src/app/plaintiffs/manage/page.tsx`:
- Around line 103-107: The check that calls
redirect(getSignInPath(ROUTES.plaintiffsManage)) doesn't make the TypeScript
narrowing of session?.user.id (userId) unambiguous in all compiler
configurations; update the guard in the async page function around
getServerSession(authOptions) so that after calling redirect(...) you also stop
execution (e.g., return) or use an explicit assertion pattern (if (!userId) {
redirect(...); return; }) so that userId is definitely narrowed to string for
subsequent usage (e.g., the where clause that references userId).

In `@packages/web/src/app/plaintiffs/page.tsx`:
- Around line 113-132: Extract the duplicated numeric literal 24 into a single
named constant (e.g., PAGE_SIZE) and use that constant wherever the page size or
threshold is referenced; specifically replace the inline pageSize: 24 in the
data fetch call and the filteredCount >= 24 check used for showBrowseTools,
ensuring hasActiveBrowseState and currentPage logic remain unchanged and any
other occurrences of the same literal in this file (or nearby components) use
PAGE_SIZE too.
- Around line 23-49: Replace the magic-string list with a typed symbol
collection and use it everywhere: define a const enum or a const-record (e.g.,
SortKey and SORT_KEYS) whose keys/values map to the same values as
RepresentedPeopleSortKey, then change VALID_SORT_KEYS to derive from that symbol
collection and update parseSort to validate against SORT_KEYS (and return a
member of SortKey/RepresentedPeopleSortKey), and ensure parseEnum's allowed
parameter is typed from the same symbol collection so callers use the
enum/record symbols instead of raw strings (refer to VALID_SORT_KEYS, parseSort,
parseEnum, and RepresentedPeopleSortKey when making these replacements).
- Around line 134-139: The loop in page.tsx that iterates over
Object.entries(params) (the for...of block setting filterParams) currently
copies any non-"page" query keys into pagination links; restrict it to a
whitelist of valid filter keys (e.g., "sort", "cause", "conditionId",
"conflictId", "country", "efficacyLag") before calling filterParams.set, keeping
the existing logic for handling array values (use first element) and excluding
empty strings and "page". Update the condition in that loop to check membership
in the allowed-keys set so unrelated query params (like "edit" or tracking tags)
are not propagated.
- Around line 60-72: The current generateMetadata function builds
title/description via getSiteMetadata using headers() and getSiteFromHeaders,
but sibling page uses getRouteMetadata({ ...plaintiffsManageLink }); either
replace the manual construction with a single-source call to getRouteMetadata({
...plaintiffsLink }) (and use ROUTES.plaintiffs where appropriate) to match
other pages, or if multi-tenant/site-aware metadata is required keep
getSiteMetadata but add a clear comment above generateMetadata explaining why it
diverges from getRouteMetadata (mentioning headers(), getSiteFromHeaders(), and
plaintiffsLink) so reviewers know the difference.

In `@packages/web/src/components/referendum/VoteCounterSplit.tsx`:
- Around line 15-20: The JSDoc for the VoteCounterSplit prop
linkMemorialToPeople is stale (mentions `/people` and "Invisible Graveyard")
while the implementation uses ROUTES.plaintiffs; update the doc to accurately
describe that enabling this prop wraps the memorial line in a Link to
ROUTES.plaintiffs and remove references to `/people`/Invisible Graveyard, and
optionally rename the prop to linkMemorialToPlaintiffs (or keep the prop name
but ensure all callers and tests reflect the new semantics if you choose to
rename); if you rename linkMemorialToPeople to linkMemorialToPlaintiffs, update
all usages across the codebase to the new prop name and adjust any related
types/props interfaces accordingly.

In `@packages/web/src/lib/people-directory.server.ts`:
- Around line 137-169: Introduce module-level named constants for the page-size
magic numbers and use them in getPeopleDirectoryData: add DEFAULT_PAGE_SIZE =
36, MAX_PAGE_SIZE = 60 and TASK_PREVIEW_FETCH_SIZE = 8 at the top of the module,
replace the default param limit = 36 with limit = DEFAULT_PAGE_SIZE, replace the
clamp Math.min(Math.max(limit, 1), 60) assigned to take with
Math.min(Math.max(limit, 1), MAX_PAGE_SIZE) (or a helper clamp using
DEFAULT_PAGE_SIZE/MAX_PAGE_SIZE), and replace any hardcoded take: 8 used for
task preview fetches with TASK_PREVIEW_FETCH_SIZE; update references in
getPeopleDirectoryData (symbols: getPeopleDirectoryData, limit, take) and the
task-preview fetch site to use these new constants.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b48c46ba-da5b-4629-a149-ebf0b00a8865

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4e185 and 13648d9.

📒 Files selected for processing (19)
  • packages/web/src/app/governments/[code]/page.tsx
  • packages/web/src/app/people/[id]/page.tsx
  • packages/web/src/app/people/manage/page.tsx
  • packages/web/src/app/people/page.tsx
  • packages/web/src/app/plaintiffs/manage/page.tsx
  • packages/web/src/app/plaintiffs/page.tsx
  • packages/web/src/components/landing/FinalCTASection.tsx
  • packages/web/src/components/landing/WhyPlaySection.tsx
  • packages/web/src/components/medical/medical-pages.tsx
  • packages/web/src/components/people/ManageRepresentedPeopleClient.tsx
  • packages/web/src/components/people/RepresentedPersonConversionForm.tsx
  • packages/web/src/components/people/RepresentedPersonForm.tsx
  • packages/web/src/components/referendum/VoteCounterSplit.tsx
  • packages/web/src/lib/__tests__/url.test.ts
  • packages/web/src/lib/people-directory.server.ts
  • packages/web/src/lib/routes.ts
  • packages/web/src/lib/site-sitemap.ts
  • packages/web/src/lib/site.ts
  • packages/web/src/lib/url.ts

Comment thread packages/web/src/app/plaintiffs/manage/page.tsx
Comment thread packages/web/src/app/plaintiffs/page.tsx
Comment thread packages/web/src/lib/people-directory.server.ts Outdated
@mikepsinn mikepsinn merged commit 57420b3 into main May 6, 2026
7 of 8 checks passed
@mikepsinn mikepsinn deleted the feature/plaintiffs-people-directory branch May 6, 2026 06:38
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.

1 participant