Skip to content

Add Company and Job Title filters to Members page - #37

Closed
Bjjj834 wants to merge 2 commits into
mainfrom
feature/member-company-title-filters
Closed

Add Company and Job Title filters to Members page#37
Bjjj834 wants to merge 2 commits into
mainfrom
feature/member-company-title-filters

Conversation

@Bjjj834

@Bjjj834 Bjjj834 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Problem

The Members page (/community/members, ~993 members / ~100 pages) only had a single free-text search box. Users had no way to narrow the list by Company or Job Title specifically.

Current Members page behavior (investigated before making changes)

  • Rendered by ArmorMembers.js, routed at /members under Main.js.
  • Data source: the browser fetches the entire Google Sheet (SHEET_ID=1IREd2vCxo7rDGOoDUYtzoywVGLqqUFCsu3qGsA0HsMc, tab Sheet-1) directly from the Google Sheets API in one request — no backend, no database, no paginated API.
  • Pagination is client-side only (filtered.slice((page-1)*pageSize, page*pageSize), pageSize=10) over the full in-memory array.
  • The existing search already filtered the full loaded dataset, not just the current page — this was correct and preserved.
  • Field names: Company Name column → member.company, Job Title column → member.title.
  • Conclusion: since the full dataset is already safely loaded client-side, the new filters could be implemented entirely in this repo with no backend dependency and no risk of only filtering the visible page. There was no external-backend blocker.

Proposed solution

  • memberFilters.js — pure, unit-tested helpers: value normalization, deduped/sorted option lists (blank-free), and the three match functions (general search, Company = exact case/whitespace-insensitive match, Job Title = case-insensitive keyword/contains match).
  • FilterCombobox.js — a shared searchable dropdown/autocomplete used for both filters, with keyboard navigation, an individual clear button, and accessible labeling.
  • ArmorMembers.js — wires both filters in next to the existing search box, combines all three conditions with AND semantics, adds removable filter chips, a "Clear filters" action, and an empty state ("No members match the selected filters.") with its own clear action.
  • useDebouncedValue.js — 250ms debounce for the free-text search box.
  • setupTests.js@testing-library/jest-dom was already a devDependency but never wired up (no setupTests.js existed), so matchers like toBeInTheDocument/toHaveValue weren't available to any test in the suite. Added the standard CRA setup file.

Data and API changes

None. No backend/API changes — everything operates on the dataset already loaded into the browser, per the investigation above.

Filter and pagination behavior

  • Filters combine with the general search using AND semantics.
  • Changing any filter resets pagination to page 1 (skipped on initial mount so a shared/reloaded URL keeps its page).
  • Page is clamped into range once the filtered dataset is known (handles a shared URL like ?page=999 against a narrower filter).
  • Filters persist across Next/Previous page navigation.
  • Search box is debounced (250ms); Company/Job Title commit on selection, Enter, or blur (not per keystroke), so there's no per-keystroke re-filtering to debounce there.
  • Search/company/jobTitle/page are synced to the URL (e.g. ?company=Anote&jobTitle=CEO&page=2), empty params omitted, so the page is refresh-safe and shareable.

Manual verification (real data, not mocks)

Ran the app locally against the live Google Sheet (993 real members, 100 pages) and verified:

  • Company "Anote" → 132 real matches across 14 pages (was previously unreachable via the old 10-per-page-only view).
  • Job Title "CEO" → 71 real matches across 8 pages, correctly including variants like "Founder / CEO", "CEO&Founder", "Co-Founder and CEO", "Ceo" (lowercase), etc.
  • Company "Anote" + Job Title "CEO" combined → 6 matches, correctly excluding Anote members with other titles.
  • Company matching is case-insensitive against real data (an "ANOTE" row matched the "Anote" filter).
  • Clearing an individual chip, "Clear filters", empty state, and pagination-preserves-filters all behaved as expected against the live dataset.

Tests completed

  • 32 tests passing across 3 suites (npm test), including 2 new suites: memberFilters.test.js (pure-logic unit tests) and ArmorMembers.test.js (integration tests covering Company/Job Title filtering, AND-combination with search, case-insensitivity, keyword matching, page-reset-on-filter-change, filter persistence across pagination, clear-all and individual-chip clearing, empty state, deduped/blank-free dropdown options, URL hydration/sync, and loading/error states) — using a 25-row fixture specifically constructed so matches span pages 1, 2, and 3, to catch any "only filters the visible page" regression.
  • npm run lint: 0 errors (160 pre-existing warnings elsewhere in the repo, none introduced by this change).
  • npm run build (CI=false, matching this repo's own deploy.yml/ci.yml convention): succeeds.
  • Manually verified in a live browser against the real dataset (see above).

Known limitations

  • ArmorMembers.js still calls the Google Sheets API with a hardcoded API key committed in source (pre-existing, unrelated to this PR — same pattern that was previously fixed in ArmorReferrals.js). Not addressed here to keep this PR focused on the filtering feature.
  • Because filtering happens synchronously in memory (the whole dataset is already loaded), there's no separate "filtering" loading spinner — only the initial full-dataset fetch shows a loading state. This is a deliberate scope choice, not an oversight.

Bjjj834 added 2 commits July 29, 2026 17:29
jest-dom was already a devDependency but no setupTests.js imported it,
so matchers like toBeInTheDocument/toHaveValue were unavailable to any
test in the suite.
The Members page already loads the full ~993-row Google Sheet into the
browser in one request and paginates/searches it client-side, so the
new filters apply to the complete dataset rather than just the current
page.

- memberFilters.js: pure normalize/dedupe/match helpers (unit tested)
- FilterCombobox.js: searchable dropdown shared by both filters —
  Company requires an exact (case/whitespace-insensitive) match against
  known values; Job Title uses case-insensitive keyword/contains
  matching so "CEO" matches "Founder / CEO", "Founder & CEO", etc.
- ArmorMembers.js: wires up both filters alongside the existing search,
  combines all three with AND semantics, resets to page 1 on filter
  change (but not on initial URL-hydrated load), clamps page into range,
  syncs search/company/jobTitle/page to URL query params (omitting
  empty ones), adds removable filter chips, a Clear filters action, and
  an empty state for zero matches
- Debounces the free-text search input (250ms) to avoid re-filtering on
  every keystroke

Fixed a related race in FilterCombobox: its value-sync effect ran once
on mount as all effects do, which could revert a user's first keystroke
if the mount-time effect flush was delayed past that keystroke. Skipped
that effect's first run since initial state already derives from props.
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