Skip to content

Conversation

@jesusthecreator017
Copy link
Contributor

@jesusthecreator017 jesusthecreator017 commented Feb 12, 2026

Why

The site had NO pagination at all, which made it difficult to navigate all 70 something members listed. Additionally, the search bar functionality was limited. This PR aimed at fixing these issues.

What

Issue(s): #332

  • Created CustomPagination component so that other files could implement pagination as well
  • Created CustomPaginationSelect component so that other files could implement the select feature as well
  • Added Pagination, Sorting, and Filtering logic to getMembers endpoint
  • Added Filtering Logic to get getMemberCount endpoint
  • Removed Sorting and Filtering logic from the client side (should allow for an increase in performance)
  • Added _hooks folder with a useDebounce function to allow the getMembers endpoint to make use of a debounce
  • Added Pagination UI from shadcn and fixed any issues it had on import

Test Plan

  • Visually make sure pagination works :p
  • Run some quick queries through the search bar to check filtering functionality
  • Click on each sorting method
  • Click on the Time sorting button to test its functionality
  • Ran format, lint, and typecheck

Images

image

Checklist

  • [ X] Database: No schema changes, OR I have contacted the Development Lead to run db:push before merging
  • [X ] Environment Variables: No environment variables changed, OR I have contacted the Development Lead to modify them on Coolify BEFORE merging.

Summary by CodeRabbit

  • New Features

    • Improved pagination UI: first/last, ellipses, Previous/Next, page links, highlighted current page, URL-driven navigation.
    • Page-size selector (10, 25, 50, 100) with integrated bottom pagination.
    • Debounced member search plus server-driven sorting, pagination, and filters; school/major dropdowns populated dynamically.
  • Chores

    • UI dependency updated and a small public export adjustment in UI types.

Ran ui-add for shadcn pagination component
Fixed any issues from the generated pagination.tsx file
Exported ButtonProps from button.tsx
Updated members-table to include the optional inputs
Implemented Generic Pagination to members-table component
Removed logic rendering total amount of member
Updated getMembers and getMemberCount endpoints
Moved filtering logic from frontend to backend
Commented out a lot of logic for the moment
Added compatability to search via full name
(This endpoint is getting large :p)
Ran format, lint and typecheck
@jesusthecreator017 jesusthecreator017 self-assigned this Feb 12, 2026
@jesusthecreator017 jesusthecreator017 requested a review from a team as a code owner February 12, 2026 01:23
@jesusthecreator017 jesusthecreator017 added Onboarding Good first issue for onboarding Developers Minor Small change - 1 reviewer required Blade Change modifies code in Blade app API Change modifies code in the global API/tRPC package labels Feb 12, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds server-driven member listing with pagination, sorting, filtering, and debounced search; new client pagination and page-size selector components; a UI pagination primitives module; a generic useDebounce hook; and enhanced API procedures for paginated/filterable member queries and counts.

Changes

Cohort / File(s) Summary
App pagination components
apps/blade/src/app/admin/_components/CustomPagination.tsx, apps/blade/src/app/admin/_components/CustomPaginationSelect.tsx
New client components: CustomPagination updates URL page query and renders prev/next, first/last, and ellipses; CustomPaginationSelect provides a page-size selector and emits numeric changes via callback.
Members table (client integration)
apps/blade/src/app/admin/club/members/_components/members-table.tsx
Replaced client-side sorting/filtering with server-driven queries; wired currentPage, pageSize, debounced search, time/field sorting, and school/major filters to new API endpoints; integrated pagination/select components and reset page on size/filter changes.
API: member router
packages/api/src/routers/member.ts
Added parameterized getMembers, getMemberCount, getDistinctSchools, getDistinctMajors; introduced input validation, search (ILIKE across fields), field/time sorting, pagination (limit/offset), and filters; switched to permProcedure and added DB helpers (asc, ilike, or, isNotNull).
UI primitives & exports
packages/ui/src/pagination.tsx, packages/ui/src/button.tsx, packages/ui/package.json
New pagination primitives (Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis); exported ButtonProps; bumped @radix-ui/react-slot version.
Client hook
apps/blade/src/app/admin/_hooks/debounce.ts
Added useDebounce<T>(value, delay = 500) hook returning a debounced value.
Minor types
apps/blade/src/app/forms/[formName]/_components/utils.ts
Converted a runtime import to a type-only import for form-related types (no runtime change).
App manifest
apps/gemiknights/package.json
Bumped @radix-ui/react-slot version to match workspace change.

Sequence Diagram

sequenceDiagram
    participant User as User
    participant UI as "Members UI (Client)"
    participant Router as "Next Router"
    participant API as "Member API (Server)"
    participant DB as "Database"

    User->>UI: change page / pageSize / search / sort / filters
    UI->>Router: update query params (page,pageSize,searchTerm,sort...,school,major)
    Router->>API: query getMembers(currentPage,pageSize,searchTerm,sortField,sortOrder,sortByTime,schoolFilter,majorFilter)
    API->>DB: SELECT ... WHERE (ILIKE / filters) ORDER BY ... LIMIT ... OFFSET ...
    DB-->>API: rows
    API->>DB: COUNT ... WHERE (filters)
    DB-->>API: totalCount
    API-->>UI: return members + totalCount
    UI->>User: render rows, pagination, page-size selector
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

Major, UI, Feature

Suggested reviewers

  • DVidal1205
🚥 Pre-merge checks | ✅ 5 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (14 files):

⚔️ apps/blade/src/app/_components/navigation/reusable-user-dropdown.tsx (content)
⚔️ apps/blade/src/app/_components/navigation/user-dropdown.tsx (content)
⚔️ apps/blade/src/app/admin/club/data/_components/MemberDemographics.tsx (content)
⚔️ apps/blade/src/app/admin/club/members/_components/members-table.tsx (content)
⚔️ apps/blade/src/app/admin/forms/[slug]/responses/_components/PerUserResponsesView.tsx (content)
⚔️ apps/blade/src/app/admin/page.tsx (content)
⚔️ apps/blade/src/app/forms/[formName]/_components/utils.ts (content)
⚔️ apps/cron/src/crons/reminder.ts (content)
⚔️ apps/gemiknights/package.json (content)
⚔️ packages/api/src/routers/dues-payment.ts (content)
⚔️ packages/api/src/routers/member.ts (content)
⚔️ packages/ui/package.json (content)
⚔️ packages/ui/src/button.tsx (content)
⚔️ pnpm-lock.yaml (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title follows the required format with issue number [#332], is concise (39 chars), and clearly describes the main changes: member pagination and search implementation.
No Hardcoded Secrets ✅ Passed No hardcoded API keys, passwords, tokens, or secrets found in modified files. Sensitive configuration properly externalized through imports.
Validated Env Access ✅ Passed All eight modified files properly use validated env imports instead of raw process.env access, following codebase conventions.
No Typescript Escape Hatches ✅ Passed Pull request avoids TypeScript escape hatches entirely with proper type definitions through interfaces and generics throughout all modified files.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch blade/member-pagination-search

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: 5

🤖 Fix all issues with AI agents
In `@apps/blade/src/app/admin/_components/CustomPagination.tsx`:
- Around line 81-86: The PaginationPrevious/PaginationNext controls still fire
changePage when at bounds because aria-disabled only informs AT; update the
CustomPagination component to also prevent activation: set the control's
disabled prop (or remove/replace the onClick) when currentPage <= 1 for
PaginationPrevious and when currentPage >= totalPages for PaginationNext, and
keep aria-disabled in sync; additionally guard the changePage invocation inside
the onClick handlers (no-op if out of range) so clicking or programmatic
activation cannot change pages when disabled.

In `@apps/blade/src/app/admin/_components/CustomPaginationSelect.tsx`:
- Around line 1-8: Add the Next.js client directive to this component so event
handlers like onValueChange work on the client: insert the string "use client"
as the very first line of the CustomPaginationSelect component file (before any
imports) so the React component and handlers (e.g., Select, SelectTrigger,
onValueChange usage) run on the client rather than the server.

In `@apps/blade/src/app/admin/club/members/_components/members-table.tsx`:
- Line 49: The currentPage computed in members-table.tsx (const currentPage =
Number(searchParams.get("page") || 1)) isn't validated and can become NaN or
negative; update the logic that derives currentPage (used by the MembersTable
pagination) to parse the value (e.g., parseInt), treat non-numeric results as 1,
clamp the result to a minimum of 1, and if you have a known totalPages or
pageCount available in this component further clamp to that upper bound before
using it for rendering or navigation.

In `@packages/api/src/routers/member.ts`:
- Around line 374-393: The time-based sorting branch currently calls
query.orderBy(...) without reassigning the result and mixes
Member.dateCreated/timeCreated inconsistently; change the sort-by-time branch to
assign the result back (query = query.orderBy(...)) and use the correct columns
consistently (both clauses should reference Member.timeCreated, using asc when
input.sortOrder === "asc" and desc when "desc") so that sorting by time is
actually applied; keep the other branches (sortField and default) unchanged.
- Around line 400-407: The getMemberCount endpoint is currently exposed as
publicProcedure which allows unauthenticated enumeration via the searchTerm
filter; change its declaration from publicProcedure to permProcedure and apply
the same permission check used by getMembers (controlPerms.or(["READ_MEMBERS",
"READ_CLUB_DATA"], ctx)) so only authorized users can call getMemberCount, and
keep the existing searchTerm input handling; alternatively, if public access is
required, remove the searchTerm filtering entirely from getMemberCount so it
only returns an unfiltered total count.
🧹 Nitpick comments (6)
apps/blade/src/app/admin/_hooks/debounce.ts (1)

3-19: Clean debounce implementation!

The hook correctly handles cleanup to prevent stale updates. One tiny typo on line 7: "timout" → "timeout".

-    // Set a timout to update the debounced value after the short delay
+    // Set a timeout to update the debounced value after the short delay
packages/ui/src/pagination.tsx (1)

44-62: Consider keyboard accessibility for PaginationLink.

PaginationLink renders an <a> tag, but it's used with onClick handlers (without href) in CustomPagination. Anchor tags without href aren't keyboard-focusable by default.

Two options:

  1. Add role="button" and tabIndex={0} when no href is provided
  2. Use a <button> element when onClick is the primary interaction
♻️ Suggested enhancement
 const PaginationLink = ({
   className,
   isActive,
   size = "icon",
+  href,
   ...props
 }: PaginationLinkProps) => (
   <a
     aria-current={isActive ? "page" : undefined}
+    role={href ? undefined : "button"}
+    tabIndex={href ? undefined : 0}
     className={cn(
       buttonVariants({
         variant: isActive ? "outline" : "ghost",
         size,
       }),
       className,
     )}
+    href={href}
     {...props}
   />
 );
packages/api/src/routers/member.ts (1)

359-372: Consider extracting the shared filter logic.

The ILIKE filter conditions are duplicated between getMembers (lines 359-372) and getMemberCount (lines 414-424). Extracting this to a helper would improve maintainability.

♻️ Example refactor
// At the top of the file or in a shared utils
const buildMemberSearchFilter = (searchPattern: string) =>
  or(
    ilike(Member.firstName, searchPattern),
    ilike(Member.lastName, searchPattern),
    ilike(Member.email, searchPattern),
    ilike(Member.discordUser, searchPattern),
    ilike(Member.company, searchPattern),
    sql`CONCAT(${Member.firstName}, ' ', ${Member.lastName}) ILIKE ${searchPattern}`,
  );

Also applies to: 409-424

apps/blade/src/app/admin/_components/CustomPagination.tsx (1)

16-21: Minor: Interface name doesn't match component purpose.

MemberPaginationProps suggests member-specific usage, but CustomPagination is generic. Consider renaming to CustomPaginationProps for consistency.

-interface MemberPaginationProps {
+interface CustomPaginationProps {
   itemCount: number;
   pageSize: number;
   currentPage: number;
   className?: string;
 }
apps/blade/src/app/admin/club/members/_components/members-table.tsx (1)

52-63: Consider adding loading and error states.

The component renders immediately with empty data (members ?? [], totalCount ?? 0) without indicating loading or errors. Per repo patterns, gating rendering until all required data succeeds provides a more coherent UX.

♻️ Suggested pattern
const { data: members, isLoading: membersLoading, error: membersError } = api.member.getMembers.useQuery({...});
const { data: totalCount, isLoading: countLoading } = api.member.getMemberCount.useQuery({...});
const { data: duesPayingStatus, isLoading: duesLoading } = api.member.getDuesPayingMembers.useQuery();

const isLoading = membersLoading || countLoading || duesLoading;

if (isLoading) return <LoadingSpinner />;
if (membersError) return <ErrorMessage error={membersError} />;

Based on learnings: "gating rendering should occur only when all required data fetches succeed...implement a unified loading/state or error handling."

apps/blade/src/app/admin/_components/CustomPaginationSelect.tsx (1)

28-30: "Members" label is hardcoded — limits reusability.

If you plan to reuse this component elsewhere (e.g., for events or hackers), consider adding a label prop.

 interface CustomPaginationSelectProps {
   pageSize: number;
   onPageSizeChange: (pageSize: number) => void;
   options?: number[];
   className?: string;
+  label?: string;
 }

 // In the component:
-        <SelectValue>{pageSize} Members</SelectValue>
+        <SelectValue>{pageSize} {label ?? "Members"}</SelectValue>

@jesusthecreator017 jesusthecreator017 force-pushed the blade/member-pagination-search branch from b20359a to 9beb119 Compare February 13, 2026 03:10
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: 2

🤖 Fix all issues with AI agents
In `@apps/blade/src/app/admin/club/members/_components/members-table.tsx`:
- Around line 126-155: The major filter needs an explicit "All Majors" option
and the school button label has a typo; update the ResponsiveComboBox for majors
to include "All Majors" as the first item (similar to schools), ensure
getItemValue/getItemLabel handle that option and onItemSelect maps "All Majors"
to clearing the filter (call setMajorFilter("") and reset page param), and fix
the school buttonPlaceholder from "All School's" to "All Schools" so both combo
boxes behave consistently and are clearable; reference ResponsiveComboBox,
setMajorFilter, setSchoolFilter, buttonPlaceholder, and onItemSelect.
- Around line 55-73: The component currently renders when members are available
but other dependent queries (duesPayingStatus, schools, majors, totalCount) may
still be loading or errored; update the render gating to wait for a unified
ready state by combining the queries' loading/error flags from
api.member.getMembers.useQuery, getMemberCount.useQuery,
getDuesPayingMembers.useQuery, getDistinctSchools.useQuery, and
getDistinctMajors.useQuery (e.g., aggregate isLoading/isError or status fields)
and show a single loading spinner or an error UI if any query is
loading/errored, only rendering the table and using
members/duesPayingStatus/schools/majors when all queries have succeeded.

Comment on lines 55 to 73
const { data: members } = api.member.getMembers.useQuery({
currentPage,
pageSize,
searchTerm: debounceSearchTerm,
sortField: sortByTime ? undefined : (sortField ?? undefined),
sortOrder: sortByTime ? timeSortOrder : (sortOrder ?? undefined),
sortByTime,
schoolFilter,
majorFilter,
});
const { data: totalCount } = api.member.getMemberCount.useQuery({
searchTerm: debounceSearchTerm,
schoolFilter,
majorFilter,
});
const { data: duesPayingStatus } = api.member.getDuesPayingMembers.useQuery();
const { data: schools } = api.member.getDistinctSchools.useQuery();
const { data: majors } = api.member.getDistinctMajors.useQuery();

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

Avoid partial rendering while dependent queries load or fail.

Right now, the table can render with members loaded but duesPayingStatus/filters still pending or failed, which can mislabel dues status and show incomplete filter options. Gate rendering on a unified loading/error state so the UI appears only when all required data is ready.

✅ Suggested unified gating
-  const { data: members } = api.member.getMembers.useQuery({
+  const membersQuery = api.member.getMembers.useQuery({
     currentPage,
     pageSize,
     searchTerm: debounceSearchTerm,
     sortField: sortByTime ? undefined : (sortField ?? undefined),
     sortOrder: sortByTime ? timeSortOrder : (sortOrder ?? undefined),
     sortByTime,
     schoolFilter,
     majorFilter,
   });
-  const { data: totalCount } = api.member.getMemberCount.useQuery({
+  const totalCountQuery = api.member.getMemberCount.useQuery({
     searchTerm: debounceSearchTerm,
     schoolFilter,
     majorFilter,
   });
-  const { data: duesPayingStatus } = api.member.getDuesPayingMembers.useQuery();
-  const { data: schools } = api.member.getDistinctSchools.useQuery();
-  const { data: majors } = api.member.getDistinctMajors.useQuery();
+  const duesQuery = api.member.getDuesPayingMembers.useQuery();
+  const schoolsQuery = api.member.getDistinctSchools.useQuery();
+  const majorsQuery = api.member.getDistinctMajors.useQuery();
+
+  const isLoading =
+    membersQuery.isLoading ||
+    totalCountQuery.isLoading ||
+    duesQuery.isLoading ||
+    schoolsQuery.isLoading ||
+    majorsQuery.isLoading;
+
+  const hasError =
+    membersQuery.error ||
+    totalCountQuery.error ||
+    duesQuery.error ||
+    schoolsQuery.error ||
+    majorsQuery.error;
+
+  if (isLoading) return <div>Loading members...</div>;
+  if (hasError) return <div>Failed to load members.</div>;
+
+  const members = membersQuery.data ?? [];
+  const totalCount = totalCountQuery.data ?? 0;
+  const duesPayingStatus = duesQuery.data ?? [];
+  const schools = schoolsQuery.data ?? [];
+  const majors = majorsQuery.data ?? [];

Based on learnings: In the KnightHacks/forge repository, apply the pattern in TSX React components: gating rendering should occur only when all required data fetches succeed. Do not render partial success when some queries fail; instead, implement a unified loading/state or error handling that surfaces a single, coherent state once all data is ready or failed.

Also applies to: 232-274

🤖 Prompt for AI Agents
In `@apps/blade/src/app/admin/club/members/_components/members-table.tsx` around
lines 55 - 73, The component currently renders when members are available but
other dependent queries (duesPayingStatus, schools, majors, totalCount) may
still be loading or errored; update the render gating to wait for a unified
ready state by combining the queries' loading/error flags from
api.member.getMembers.useQuery, getMemberCount.useQuery,
getDuesPayingMembers.useQuery, getDistinctSchools.useQuery, and
getDistinctMajors.useQuery (e.g., aggregate isLoading/isError or status fields)
and show a single loading spinner or an error UI if any query is
loading/errored, only rendering the table and using
members/duesPayingStatus/schools/majors when all queries have succeeded.

Comment on lines 126 to 155
<div>
<ResponsiveComboBox
items={["All Schools", ...(schools ?? [])]}
renderItem={(school) => <span>{school}</span>}
getItemValue={(school) => school}
getItemLabel={(school) => school}
onItemSelect={(school) => {
setSchoolFilter(school === "All Schools" ? "" : school);
const params = new URLSearchParams(searchParams);
params.set("page", "1");
router.push("?" + params.toString());
}}
buttonPlaceholder="All School's"
inputPlaceholder="Search schools..."
/>
</div>
<div>
<ResponsiveComboBox
items={majors ?? []}
renderItem={(major) => <span>{major}</span>}
getItemValue={(major) => major}
getItemLabel={(major) => major}
onItemSelect={(major) => {
setMajorFilter(major);
const params = new URLSearchParams(searchParams);
params.set("page", "1");
router.push("?" + params.toString());
}}
buttonPlaceholder="All Majors"
inputPlaceholder="Search majors..."
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

Add a clearable “All Majors” option (and fix the school label).

Once a major is selected there’s no way to clear it back to “All.” Also, the school placeholder reads “All School’s.” Consider adding an explicit “All Majors” option and fixing the label.

✨ Suggested fix
-            <ResponsiveComboBox
-              items={["All Schools", ...(schools ?? [])]}
+            <ResponsiveComboBox
+              items={["All Schools", ...(schools ?? [])]}
               renderItem={(school) => <span>{school}</span>}
               getItemValue={(school) => school}
               getItemLabel={(school) => school}
               onItemSelect={(school) => {
                 setSchoolFilter(school === "All Schools" ? "" : school);
                 const params = new URLSearchParams(searchParams);
                 params.set("page", "1");
                 router.push("?" + params.toString());
               }}
-              buttonPlaceholder="All School's"
+              buttonPlaceholder="All Schools"
               inputPlaceholder="Search schools..."
             />
-            <ResponsiveComboBox
-              items={majors ?? []}
+            <ResponsiveComboBox
+              items={["All Majors", ...(majors ?? [])]}
               renderItem={(major) => <span>{major}</span>}
               getItemValue={(major) => major}
               getItemLabel={(major) => major}
               onItemSelect={(major) => {
-                setMajorFilter(major);
+                setMajorFilter(major === "All Majors" ? "" : major);
                 const params = new URLSearchParams(searchParams);
                 params.set("page", "1");
                 router.push("?" + params.toString());
               }}
               buttonPlaceholder="All Majors"
               inputPlaceholder="Search majors..."
             />
📝 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
<div>
<ResponsiveComboBox
items={["All Schools", ...(schools ?? [])]}
renderItem={(school) => <span>{school}</span>}
getItemValue={(school) => school}
getItemLabel={(school) => school}
onItemSelect={(school) => {
setSchoolFilter(school === "All Schools" ? "" : school);
const params = new URLSearchParams(searchParams);
params.set("page", "1");
router.push("?" + params.toString());
}}
buttonPlaceholder="All School's"
inputPlaceholder="Search schools..."
/>
</div>
<div>
<ResponsiveComboBox
items={majors ?? []}
renderItem={(major) => <span>{major}</span>}
getItemValue={(major) => major}
getItemLabel={(major) => major}
onItemSelect={(major) => {
setMajorFilter(major);
const params = new URLSearchParams(searchParams);
params.set("page", "1");
router.push("?" + params.toString());
}}
buttonPlaceholder="All Majors"
inputPlaceholder="Search majors..."
<div>
<ResponsiveComboBox
items={["All Schools", ...(schools ?? [])]}
renderItem={(school) => <span>{school}</span>}
getItemValue={(school) => school}
getItemLabel={(school) => school}
onItemSelect={(school) => {
setSchoolFilter(school === "All Schools" ? "" : school);
const params = new URLSearchParams(searchParams);
params.set("page", "1");
router.push("?" + params.toString());
}}
buttonPlaceholder="All Schools"
inputPlaceholder="Search schools..."
/>
</div>
<div>
<ResponsiveComboBox
items={["All Majors", ...(majors ?? [])]}
renderItem={(major) => <span>{major}</span>}
getItemValue={(major) => major}
getItemLabel={(major) => major}
onItemSelect={(major) => {
setMajorFilter(major === "All Majors" ? "" : major);
const params = new URLSearchParams(searchParams);
params.set("page", "1");
router.push("?" + params.toString());
}}
buttonPlaceholder="All Majors"
inputPlaceholder="Search majors..."
/>
</div>
🤖 Prompt for AI Agents
In `@apps/blade/src/app/admin/club/members/_components/members-table.tsx` around
lines 126 - 155, The major filter needs an explicit "All Majors" option and the
school button label has a typo; update the ResponsiveComboBox for majors to
include "All Majors" as the first item (similar to schools), ensure
getItemValue/getItemLabel handle that option and onItemSelect maps "All Majors"
to clearing the filter (call setMajorFilter("") and reset page param), and fix
the school buttonPlaceholder from "All School's" to "All Schools" so both combo
boxes behave consistently and are clearable; reference ResponsiveComboBox,
setMajorFilter, setSchoolFilter, buttonPlaceholder, and onItemSelect.

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

🤖 Fix all issues with AI agents
In `@packages/api/src/routers/member.ts`:
- Around line 327-438: getMembers permits CHECKIN_CLUB_EVENT via
controlPerms.or([...], ctx) while getMemberCount and related endpoints require
READ_MEMBERS or READ_CLUB_DATA, causing inconsistent access; make the permission
sets consistent by changing the controlPerms.or call in getMembers to require
the same permissions as getMemberCount (READ_MEMBERS or READ_CLUB_DATA) or
alternatively update getMemberCount to include CHECKIN_CLUB_EVENT—locate the
controlPerms.or invocation inside the getMembers handler and adjust the array of
permission strings (READ_MEMBERS, READ_CLUB_DATA, etc.) to match the other
endpoints (or vice versa) so all member list/count/filter endpoints enforce the
same permission set.

In `@packages/ui/src/pagination.tsx`:
- Around line 102-113: The wrapper in PaginationEllipsis currently has
aria-hidden which hides the entire subtree (including the sr-only text); remove
aria-hidden from the <span> wrapper in PaginationEllipsis and instead set
aria-hidden="true" on the decorative MoreHorizontal icon component so the icon
is ignored by AT while the "More pages" sr-only text remains accessible to
screen readers.
- Around line 44-64: PaginationLink lacks Space key activation when rendered as
a button (href undefined). Add an onKeyDown handler on the <a> in the
PaginationLink component that, when href is falsy, detects the Space key (e.key
=== " " || e.key === "Spacebar" || e.keyCode === 32), calls e.preventDefault(),
and then invokes the existing click behavior by calling props.onClick?.(e) (or
falling back to e.currentTarget.click() if no onClick provided); keep the
handler attached only when href is undefined and ensure other props are still
spread into the element so existing handlers are preserved.
🧹 Nitpick comments (2)
packages/api/src/routers/member.ts (1)

353-423: Avoid unbounded member fetch when pagination params are missing

If currentPage or pageSize is omitted, the handler returns all members, undermining server-side pagination and risking large responses. Prefer safe defaults and always apply offset/limit.

🔧 Suggested fix (default pagination)
-      if (input?.currentPage && input.pageSize) {
-        // Calculate the offset and set the search term to be sql friendly
-        const offset = (input.currentPage - 1) * input.pageSize;
-        const searchPattern = `%${input.searchTerm ?? ""}%`;
+      const currentPage = input?.currentPage ?? 1;
+      const pageSize = input?.pageSize ?? 10;
+      const offset = (currentPage - 1) * pageSize;
+      const searchPattern = `%${input?.searchTerm ?? ""}%`;
 
-        return await query.offset(offset).limit(input.pageSize);
-      }
-
-      return await db.query.Member.findMany();
+      return await query.offset(offset).limit(pageSize);
apps/blade/src/app/admin/club/members/_components/members-table.tsx (1)

119-123: Reset pagination when the search term changes

If a user is on page N and searches, the result set may shrink and show an empty page. Reset to page 1 on search input changes to avoid that UX trap.

🔧 Suggested fix (reset page on search)
-            <Input
+            <Input
               placeholder="Search members..."
               value={searchTerm}
-              onChange={(e) => setSearchTerm(e.target.value)}
+              onChange={(e) => {
+                setSearchTerm(e.target.value);
+                if (currentPage !== 1) {
+                  const params = new URLSearchParams(searchParams);
+                  params.set("page", "1");
+                  router.push("?" + params.toString());
+                }
+              }}
               className="pl-8"
             />

@jesusthecreator017 jesusthecreator017 force-pushed the blade/member-pagination-search branch from 9beb119 to 1f2c6c7 Compare February 13, 2026 03:23
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: 1

Caution

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

⚠️ Outside diff range comments (1)
apps/blade/src/app/admin/club/members/_components/members-table.tsx (1)

117-124: ⚠️ Potential issue | 🟡 Minor

Reset pagination when search changes.
If a user is on a later page, tightening the search can yield empty results. Resetting to page 1 avoids that UX.

🔧 Suggested change
-            <Input
+            <Input
               placeholder="Search members..."
               value={searchTerm}
-              onChange={(e) => setSearchTerm(e.target.value)}
+              onChange={(e) => {
+                setSearchTerm(e.target.value);
+                if (currentPage !== 1) {
+                  const params = new URLSearchParams(searchParams);
+                  params.set("page", "1");
+                  router.push("?" + params.toString());
+                }
+              }}
               className="pl-8"
             />
🤖 Fix all issues with AI agents
In `@packages/api/src/routers/member.ts`:
- Around line 353-424: The current logic only builds filters/sorting when both
input.currentPage and input.pageSize are present, causing no filters applied
otherwise; change the flow in the handler so you always construct the base query
(let query = db.select().from(Member)), build the conditions array and apply
query = query.where(and(...conditions)) when conditions.length>0, and always
apply the sorting block (the input.sortByTime / input.sortField branches) to
that query; only apply offset and limit when both input.currentPage and
input.pageSize exist (compute offset = (currentPage-1)*pageSize and call
query.offset(...).limit(...)); return the final query (with or without
pagination) instead of short-circuiting to db.query.Member.findMany(),
referencing the existing symbols Member, query, conditions, input.currentPage,
input.pageSize, sortByTime, sortField, and offset/limit logic.

@cataladev
Copy link
Contributor

out of scope

@jesusthecreator017
Copy link
Contributor Author

Note to self: Skeleton page to handle loading state, because searching up a something looks ugly :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API Change modifies code in the global API/tRPC package Blade Change modifies code in Blade app Minor Small change - 1 reviewer required Onboarding Good first issue for onboarding Developers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants