feat(supporter): render tier-styled chip on self profile view#143
Merged
Conversation
added 2 commits
May 31, 2026 18:55
Adds the supporter recognition pill promised on /support ("A
supporter pill on your public profile") to the owner-viewing-self
path of /u/[handle]. Tier-specific styling distinguishes the three
TIERS: coffee (warm brown), standard (accent), generous (gold).
Lapsed states render with the same chip shape but muted colours per
the design promise — "the pill stays — recognition is permanent —
but accent perks revert to free-tier until the next payment lands".
Server changes:
- `SupporterStatus` gains `current_tier_key: Option<String>`,
derived at read time by a LATERAL join to the most-recent
completed `revolut_orders` row. Schema header for
supporter_status deliberately keeps the tier off the table
(rename-safety + denorm-drift avoidance) so the read-side join
is the canonical path.
- `SupporterStatusDto` (returned by `GET /v1/me/supporter`) gains
the same field. Backward-compatible — existing callers that
ignore the new optional field still work.
Web changes:
- New `SupporterChip` component with three tier palettes built off
CSS custom properties (theme-respecting), an `active`/`lapsed`
variant for each, and a `size` prop (`sm` for compact surfaces
like the future TopBar wire). Returns `null` for `state === 'none'`
so callers don't need a conditional render.
- Wired into `/u/[handle]/page.tsx` self path only. The chip
appears alongside the existing "You" badge in the header. Public/
friend paths don't get the chip in this PR — extending
`PublicSummaryResponse` with supporter info needs threading the
supporter store through `render_summary` + a handle->user_id
lookup, deferred to a follow-up PR.
Test coverage:
- 10 new SupporterChip tests: null status, none state, three tier
labels, name plate appendage, lapsed text marker, unknown-tier
fallback, accessible label composition, size variants.
- Existing 4 supporter store tests updated for the new field +
asserting tier_key round-trips through the seed path.
Follow-ups:
- PR for topbar chip (trivial — layout.tsx already has the
Promise.allSettled scaffold; add getSupporterStatus + thread
through TopBar prop).
- PR for public/friend profile chip (needs PublicSupporterInfo +
PublicSummaryResponse extension + render_summary supporter
threading + UserStore handle->id lookup).
- PR for /discover/profiles chip (needs bulk-fetch SupporterStore
method to avoid N+1 queries).
This was referenced Jun 1, 2026
ntatschner
pushed a commit
that referenced
this pull request
Jun 1, 2026
Surfaces the supporter pill on every signed-in page (not just
/u/<handle> self view) by piggybacking on the existing layout-level
Promise.allSettled scaffold. `getSupporterStatus` joins the existing
location / shared / catalog fan-out so the chip costs one extra
fetch per shell render — request-level cached by React anyway.
Web changes:
- layout.tsx adds getSupporterStatus to the Promise.allSettled
block, fail-soft to null on error so a /v1/me/supporter hiccup
doesn't blank the chrome (same posture as the other shell
fetches).
- TopBar.tsx accepts an optional `supporter` prop, renders
<SupporterChip size="sm" /> next to the @handle pill. Compact
palette size mirrors the existing topbar density.
E2E fixture (required per `Playwright fixture default rule`):
- api-mock.ts gets a default `/v1/me/supporter` fixture pointing
at state=none so existing scenarios that don't care about
supporter logic don't 599 on the new layout fetch.
- New `supporterStatus(tier, plate)` helper for tests that DO
want to exercise the chip.
Self-profile chip from PR #143 continues to work; the topbar chip
just makes the recognition visible on /dashboard, /journey, /sharing
etc. without forcing the user to navigate to their profile page.
Follow-up: public/friend profile chip (#145), discover/profiles
chip (#146).
ntatschner
pushed a commit
that referenced
this pull request
Jun 1, 2026
Extends the supporter recognition pill (PR #143 self-only, PR #144 topbar) to anyone visiting a public profile or accepted-share view. Continuation of the chip rollout: #143 → #144 → **#145 (this PR)** → #146 (discover bulk). Server changes: - `SupporterStore` gains `get_by_handle_public(handle)` — single-query JOIN of `users` + `supporter_status` + LATERAL most- recent completed `revolut_orders`. Returns `Ok(None)` for unknown handles, missing rows, or `state=none`; returns `Ok(Some(_))` only for `active`/`lapsed`. Case-insensitive handle match mirrors the rest of the project. - `PublicSummaryResponse` gains `supporter: Option<PublicSupporterInfo>`. `PublicSupporterInfo` is a deliberate projection of the store shape — only `state` + `current_tier_key` + `name_plate` are surfaced. `grace_until`, payment timestamps, and `cancelled_at` are explicitly NOT exposed to strangers (fingerprinting + churn leak avoidance). - `render_summary` + `render_summary_scoped` now take a `&dyn SupporterStore` and populate `supporter` on every public summary response. Fail-soft on lookup error (warn log; chip just doesn't render). Three handler call sites updated: `public_summary`, `friend_summary`, `preview_summary`. Each takes `Extension(Arc<dyn SupporterStore>)`; same dyn-cast pattern as `share_metadata_dyn`. - `main.rs` wires `supporter_store_dyn` + adds the Extension layer alongside `share_metadata_dyn`. - `openapi.rs` registers `PublicSupporterInfo` so the TS client regen picks it up. Web changes: - `/u/[handle]/page.tsx` extends the supporter wiring from PR #143. Self path keeps fetching `/v1/me/supporter` for full DTO. Public + shared paths now read `data.supporter` from the extended PublicSummaryResponse. Both feed the same `<SupporterChip>` via a `ChipStatus` type alias (the three-field overlap between `SupporterStatusDto` and `PublicSupporterInfo`). Tests: - 4 new SupporterStore tests on the memstore: unknown handle, unbound handle (row exists but no handle binding — simulates Postgres JOIN miss), bound handle returns active row with case-insensitive lookup, state=none filtered out. - `MemorySupporterStore` gains `bind_handle(handle, user_id)` so tests can simulate the users-table side of the JOIN. - All existing supporter + sharing tests still pass (35 sharing, 8 supporter, 666 filtered). Follow-up: PR #146 — discover/profiles chip (needs bulk `get_many_public(user_ids)` to avoid N+1).
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lights up the supporter recognition pill promised on
/support("A supporter pill on your public profile") for the owner-viewing-self path of/u/[handle]. Tier-specific styling distinguishes the three TIERS: coffee (warm brown), standard (accent), generous (gold). Lapsed states render with the same chip shape but muted colours per the design promise — "the pill stays — recognition is permanent — but accent perks revert to free-tier until the next payment lands".Scope choice — self-path only
The original ask was a three-surface chip (profile + topbar + discover) but each surface has different invasiveness:
/v1/me/supporteralready returns the data; just addscurrent_tier_key.PublicSupporterInfo+ extendingPublicSummaryResponse+ threading the supporter store throughrender_summary+ a handle→user_id lookup. Real plumbing.layout.tsxalready doesPromise.allSettled, addgetSupporterStatus+ thread to aTopBarprop.SupporterStoremethod to avoid N+1 queries across the paginated profile list.Splitting now means each follow-up can ship + promote independently with focused review.
Server changes
SupporterStatusgainscurrent_tier_key: Option<String>, derived at read time via aLEFT JOIN LATERALto the most-recent completedrevolut_ordersrow. Schema header forsupporter_statusdeliberately keeps the tier off the table (tier-rename safety + denorm-drift avoidance), so the read-side join is the canonical path.SupporterStatusDto(returned byGET /v1/me/supporter) gains the same field. Backward-compatible — existing callers that ignore the new optional field still work.Web changes
SupporterChipcomponent with three tier palettes built off CSS custom properties (theme-respecting), anactive/lapsedvariant for each, and asizeprop (smfor the future TopBar wire). Returnsnullforstate === 'none'so callers don't need a conditional render./u/[handle]/page.tsxself path only. The chip appears alongside the existing "You" badge in the header. Public/friend paths don't get the chip in this PR.Test plan
cargo test -p starstats-server config::tests supporters::— all green (4 existing supporter tests passed against the new field shape after seed-row update)cargo fmt -p starstats-servercleancargo clippy -p starstats-server --bin starstats-server -- -D warningscleanpnpm --filter web run test:run— 40 tests pass (10 new in SupporterChip.test.tsx covering null/none gating, three tier labels, name plate, lapsed marker, unknown-tier fallback, accessible label, size variants)pnpm --filter web run typecheckcleanpnpm --filter web run lint— no new warnings (3 pre-existing remain in unrelated files)/u/<your-handle>while signed in as yourself — chip renders with your current tier and name plate. Sign out, visit same URL — no chip (public path doesn't fetch supporter yet, expected).Follow-ups
PublicSummaryResponseextension/discover/profileschip + bulk-fetch