Skip to content

feat: add "pending only" filter and count badge to member list (#148)#165

Merged
Sendi0011 merged 5 commits into
JointSave-org:mainfrom
cLamberti:feat/pending-deposits-filter
Jun 29, 2026
Merged

feat: add "pending only" filter and count badge to member list (#148)#165
Sendi0011 merged 5 commits into
JointSave-org:mainfrom
cLamberti:feat/pending-deposits-filter

Conversation

@cLamberti

Copy link
Copy Markdown
Contributor

Description

Adds a client-side "Pending only" quick filter and an always-visible pending-count badge to the pool member list (group-members.tsx).

Closes #148

Changes

  • frontend/lib/member-filters.ts (new) — pure helpers isPendingMember / countPendingMembers / filterPendingMembers, built on a single source-of-truth predicate (status === "pending").
  • frontend/lib/member-filters.test.ts (new) — 6 unit tests (tsx --test).
  • frontend/package.json — registers the new test file in the test:unit script.
  • frontend/components/group/group-members.tsxShow all / Pending only ToggleGroup, {N} pending badge, filtered render, and an "Everyone has deposited" empty state.

Implementation notes

  • Filtering and counting are derived inline each render (never stored in state), so the badge and list always reflect the current members array.
  • The "pending" predicate is centralized in member-filters.ts. It mirrors the status the list already displays (the Clock icon); late is intentionally excluded to match the AC wording. Including late later is a one-line change.
  • The filter's empty state ("Everyone has deposited") is separate from the existing members.length === 0 ("No members yet") branch.

Accessibility & UX

  • ToggleGroup and its items have aria-labels; the active option is visually distinct.
  • Per-member status icons now expose role="img" + aria-label (Paid/Pending/Late) so screen readers announce deposit status (previously icon-only).
  • Member avatars are tinted by status (green = paid, yellow = pending, red = late), matching the row's status icon, so pending/late members are scannable at a glance. The pending clock icon was changed from muted gray to the existing yellow accent.
  • Count badge uses tabular-nums + whitespace-nowrap. Yellow tones chosen to meet WCAG 4.5:1 (avatar text yellow-800 / dark:yellow-300).

Known limitation / follow-up

No code path was found that transitions pool_members.status from pending to paid/late (members are inserted as pending; the real deposit signal lives in pool_activity / on-chain state). This filter faithfully mirrors the displayed status, so with current data members remain pending. A follow-up issue is recommended to transition member status on deposit so the filter/badge become useful against live data.

Note: pre-existing type errors (not introduced by this PR)

tsc --noEmit reports 2 errors, both in app/dashboard/create/[type]/page.tsx — a file this PR does not touch:

app/dashboard/create/[type]/page.tsx:81  TS2322  Property 'prefill' does not exist ... <TargetForm prefill={prefill} />
app/dashboard/create/[type]/page.tsx:82  TS2322  Property 'prefill' does not exist ... <FlexibleForm prefill={prefill} />

Root cause: the duplicate-pool / "new cycle" prefill prop is implemented only on RotationalForm (rotational-form.tsx accepts prefill?: DuplicatePrefill); TargetForm and FlexibleForm declare no props, yet the page passes prefill to all three. So duplicating a Target/Flexible pool silently doesn't pre-fill, and tsc / next build fail on these lines. Pre-existing on main, unrelated to #148; a separate issue is recommended.

Type of Change

  • feat: new feature
  • fix: bug fix
  • chore: maintenance, tooling, dependencies
  • docs: documentation only
  • refactor: code restructuring (no functional changes)
  • test: adding or updating tests

How Has This Been Tested?

  • cargo test — N/A (no smart-contract changes)
  • pnpm build — not green: blocked by the 2 pre-existing type errors in an untouched file (see note above), not by this PR
  • pnpm lint — unavailable in this toolchain (Next 16 removed next lint; ESLint not configured)
  • Manual testing (below)

Automated: pnpm test:unit57/57 passing (6 new). tsc --noEmitzero errors in the changed files (lib/member-filters.ts, components/group/group-members.tsx).
Manual: rendered GroupMembers locally and verified Show all / Pending only / empty state (see Screenshots).

Checklist

  • My code follows the coding conventions of this project
  • I have added/updated tests if needed
  • I have updated documentation if needed (N/A — no docs affected)
  • My changes generate no new warnings or errors (the 2 reported type errors are pre-existing and in a file this PR does not touch)

Screenshots

Show all — status-tinted avatars + always-on count badge:
showAll

Pending only — list narrowed to pending members:
pending

Empty state — all deposited ("Everyone has deposited"):
paid

Unit testspnpm test:unit (57/57):
tests

image
How the screenshots were produced locally (Windows — all scaffolding reverted)

The pool detail page needs env config, a connected wallet, and real pool data — and real pools start with every member pending, so the filter can't be shown "narrowing" against live data. The screenshots were produced with temporary, fully-reverted local scaffolding:

  1. Install (repo uses pnpm 10.33.0). On Windows, npm install aborts because a transitive dependency (@stellar/stellar-sdk) runs a yarn setup postinstall that fails under cmd. Worked around with:

    npx pnpm@10.33.0 install --ignore-scripts

    (--ignore-scripts only skips that broken setup; tsx / esbuild / next get their binaries via platform packages.)

  2. Env: copied .env.example.env.local. The app has a hard env guard (lib/env.ts) that throws if any of 9 NEXT_PUBLIC_* vars are missing; the example values are enough to boot (Supabase values are placeholders).

  3. Isolated demo: added a throwaway client page rendering <GroupMembers groupId="demo" />, and temporarily replaced the members source with a module-level mock array (mixed paid / pending / late) so the filter states are demonstrable without a live pool.

  4. Captured the states, then reverted everything — demo page, .env.local, and the mock were removed. The final diff contains only the 4 files listed above (verified via git status).

## Summary
Adds a client-side **"Pending only"** quick filter and an always-visible **pending-count badge** to the pool member list (`group-members.tsx`). Implements JointSave-org#148.

Closes JointSave-org#148

## Changes
- **`frontend/lib/member-filters.ts`** _(new)_ — pure helpers `isPendingMember` / `countPendingMembers` / `filterPendingMembers`, built on a single source-of-truth predicate (`status === "pending"`).
- **`frontend/lib/member-filters.test.ts`** _(new)_ — 6 unit tests (`tsx --test`).
- **`frontend/package.json`** — registers the new test file in the `test:unit` script.
- **`frontend/components/group/group-members.tsx`** — `Show all` / `Pending only` ToggleGroup, `{N} pending` badge, filtered render, and an "Everyone has deposited" empty state.

## Acceptance criteria
- [x] Toggling narrows the visible list to `status === "pending"` members.
- [x] Count badge is accurate and derived from `members` on every render (no duplicated state, so it tracks the data).
- [x] No new data fetching — purely a client-side view of data already on the page.

## Implementation notes
- Filtering and counting are derived inline each render (never stored in state), so the badge and list always reflect the current `members` array.
- The "pending" predicate is centralized in `member-filters.ts`. It mirrors the status the list already displays (the `Clock` icon); `late` is intentionally excluded to match the AC wording. If product later wants `late` included, that's a one-line change in the predicate.
- The filter's empty state ("Everyone has deposited") is separate from the existing `members.length === 0` ("No members yet") branch.

## Accessibility & UX
> Note: the avatar/color polish below was requested by the maintainer during review and goes slightly beyond the strict AC.

- ToggleGroup and its items have `aria-label`s; the active option is visually distinct.
- Per-member status icons now expose `role="img"` + `aria-label` (Paid/Pending/Late) so screen readers announce deposit status (previously icon-only).
- Member avatars are tinted by status (green = paid, yellow = pending, red = late), matching the row's status icon, so pending/late members are scannable at a glance. The pending clock icon was changed from muted gray to the existing yellow accent for prominence.
- Count badge uses `tabular-nums` + `whitespace-nowrap`. Yellow tones were chosen to meet WCAG 4.5:1 (avatar text `yellow-800` / `dark:yellow-300`).

## Known limitation / follow-up
No code path was found that transitions `pool_members.status` from `pending` to `paid`/`late` (members are inserted as `pending`; the real deposit signal lives in `pool_activity` / on-chain state). This filter faithfully mirrors the **displayed** status, so with current data members remain `pending` — making that status reflect real deposits is a separate, pre-existing concern. **A follow-up issue is recommended** to transition member status on deposit so the filter/badge become useful against live data.

## Note: pre-existing type errors (not introduced by this PR)
`tsc --noEmit` reports 2 errors, both in `app/dashboard/create/[type]/page.tsx` — a file this PR does **not** touch:

```
app/dashboard/create/[type]/page.tsx:81  TS2322  Property 'prefill' does not exist ... <TargetForm prefill={prefill} />
app/dashboard/create/[type]/page.tsx:82  TS2322  Property 'prefill' does not exist ... <FlexibleForm prefill={prefill} />
```

Root cause: the duplicate-pool / "new cycle" `prefill` prop is implemented only on `RotationalForm` (`rotational-form.tsx` accepts `prefill?: DuplicatePrefill`); `TargetForm` and `FlexibleForm` declare no props, yet the page passes `prefill` to all three. So duplicating a Target/Flexible pool silently doesn't pre-fill, and `tsc` / `next build` fail on these lines. Pre-existing on `main`, unrelated to JointSave-org#148; a separate issue is recommended.

## Testing
- `pnpm test:unit` → **57/57 passing** (includes the 6 new tests).
- `tsc --noEmit` → **zero errors in changed files** (`lib/member-filters.ts`, `components/group/group-members.tsx`). The only errors reported are the 2 pre-existing ones noted above.
- `next lint` is unavailable in this toolchain (Next 16 removed `next lint`; ESLint is not configured in the repo).

## Screenshots
| Show all | Pending only | Empty (all deposited) |
|---|---|---|
| _(attach)_ | _(attach)_ | _(attach)_ |

<details>
<summary>How the screenshots were produced locally (Windows — all scaffolding reverted)</summary>

The pool detail page needs env config, a connected wallet, and real pool data — and real pools start with every member `pending`, so the filter can't be shown "narrowing" against live data. The screenshots were produced with temporary, fully-reverted local scaffolding:

1. **Install** (repo uses pnpm `10.33.0`). On Windows, `npm install` aborts because a transitive dependency (`@stellar/stellar-sdk`) runs a `yarn setup` postinstall that fails under cmd. Worked around with:
   ```bash
   npx pnpm@10.33.0 install --ignore-scripts
   ```
   (`--ignore-scripts` only skips that broken setup; `tsx` / `esbuild` / `next` get their binaries via platform packages.)
2. **Env**: copied `.env.example` → `.env.local`. The app has a hard env guard (`lib/env.ts`) that throws if any of 9 `NEXT_PUBLIC_*` vars are missing; the example values are enough to boot (Supabase values are placeholders).
3. **Isolated demo**: added a throwaway client page rendering `<GroupMembers groupId="demo" />`, and temporarily replaced the `members` source with a module-level mock array (mixed `paid` / `pending` / `late`) so the filter states are demonstrable without a live pool.
4. Captured the three states, then **reverted everything** — demo page, `.env.local`, and the mock were removed. The final diff contains only the 4 files listed above (verified via `git status`).

</details>
@Sendi0011 Sendi0011 self-requested a review June 29, 2026 07:37
@cLamberti

Copy link
Copy Markdown
Contributor Author

Hello, maintainer,
There are conflicts with package.json; I'll resolve them too.

@Sendi0011 Sendi0011 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@cLamberti I really like the filter logic here, the test coverage explicitly checking that late members are excluded shows you read the issue's criteria closely, and the generic filterPendingMembers is a nice touch.
One real thing to fix before merging though: members is currently set to a hardcoded DEMO_MEMBERS array, with your own comment noting "TEMP DEMO — screenshots only; remove before committing" — looks like this got left in by accident. Right now this would replace every pool's real member list with the same four fake members. Could you swap that back to data?.db?.pool_members ?? []? Once that's done, this is in great shape — the rest of the logic and the toggle UI look solid.

@Sendi0011 Sendi0011 self-requested a review June 29, 2026 07:49
@cLamberti

Copy link
Copy Markdown
Contributor Author

@Sendi0011 So sorry about that — pushed a fix, the scaffolding should be fully reverted now.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread frontend/app/members-demo/page.tsx Outdated
@Sendi0011

Copy link
Copy Markdown
Contributor

Great job @cLamberti thanks for contributing

@Sendi0011 Sendi0011 merged commit bf8977e into JointSave-org:main Jun 29, 2026
3 checks passed
@cLamberti

Copy link
Copy Markdown
Contributor Author

Thank you @Sendi0011, and I apologize again for the issues with TEMP DEMO.

Thank you for the feedback; this was a very good experience. Thank you.

Best regards,
Christopher Lamberti

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.

[Feature] Add a "members who haven't deposited yet" quick filter on the member list

2 participants