feat(explore): surface policy bills as top-level data source tab#184
Conversation
The policy bills feature was previously buried as a small badge next to a selected state on other data source tabs, below five other components. The D4BL founder didn't know the feature existed when shown the tool. Promote Policy Bills to a top-level entry in DataSourceTabs, with a bills-first layout that doesn't require a state click to discover: - Recency-colored state map (green hot = recent action, dark = dormant) - Filter rail for state / status / topic (narrowed via BillStatus / PolicyTopic types so typos become compile errors) - Activity feed sorted by last_action_date DESC with stagger-fade entrance on the first 5 rows - 4-segment PhaseGlyph per bill showing legislative lifecycle (introduced / passed / signed / failed), mapped to the 5 statuses the ingestion pipeline actually emits - Empty-state placeholder in terminal-style monospace The existing per-state PolicyBadge on other tabs is preserved unchanged. PolicyExploreView owns its own fetch lifecycle; page.tsx short-circuits the metric-oriented fetchData when activeSource.key === 'policy'. POLICY_TOPICS is hoisted into explore-config.ts as the single source of truth for both PolicyTable and PolicyFilterPanel. Closes #183
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a top-level "policy" data source and a bills-first view. When the policy tab is active ExplorePage renders PolicyExploreView (map colored by bill recency, PolicyFilterPanel, and an activity feed with phase glyphs); metric fetch/render is short-circuited for the policy tab. Changes
Sequence DiagramsequenceDiagram
participant User
participant ExplorePage
participant PolicyExploreView
participant API_Server as API_Server
participant StateMap
participant PolicyFilterPanel
participant BillFeed as BillFeedRow
User->>ExplorePage: select "Policy" tab
ExplorePage->>PolicyExploreView: render (activeSource.key === 'policy')
PolicyExploreView->>API_Server: GET /api/explore/policies?limit=...
API_Server-->>PolicyExploreView: bills[]
PolicyExploreView->>PolicyExploreView: aggregateBillsByState(), compute indicators
PolicyExploreView->>StateMap: render with recency coloring
PolicyExploreView->>PolicyFilterPanel: render filters (state/status/topic)
User->>PolicyFilterPanel: update filters
PolicyFilterPanel->>PolicyExploreView: onChange(filters)
PolicyExploreView->>PolicyExploreView: filter & sort bills (last_action_date DESC)
PolicyExploreView->>BillFeed: render feed (staggered animation, pulse newest)
BillFeed->>User: display bill rows with PhaseGlyph and links
User->>StateMap: click state
StateMap->>PolicyExploreView: toggle stateFips
PolicyExploreView->>BillFeed: re-render filtered feed
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ade1ada02f
ℹ️ 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".
Greptile SummaryThis PR promotes Policy Bills from a buried per-state drawer to a top-level explore tab, wiring up a new Key changes:
Three minor P2 items worth addressing:
Confidence Score: 4/5PR is on the happy path to merge; all three remaining items are P2 polish with no production-reliability risk. The core implementation is solid — correct data flow, clean component boundaries, proper AbortController usage, accessibility attributes, and no regression to existing tabs. The three P2 issues (misleading loading empty-state, missing useMemo on stateNameByFips, potential key collision) are low-severity and don't affect correctness in the current data volume. None block merge. ui-nextjs/components/explore/PolicyExploreView.tsx — the three P2 items all live here. Important Files Changed
Sequence DiagramsequenceDiagram
participant U as User
participant EP as ExplorePage
participant PEV as PolicyExploreView
participant API as /api/explore/policies
U->>EP: clicks "Policy Bills" tab
EP->>EP: handleSourceChange('policy')
EP->>EP: fetchData() → early return (setLoading false)
EP->>PEV: render PolicyExploreView
PEV->>API: GET /api/explore/policies?limit=5000
API-->>PEV: PolicyBill[]
PEV->>PEV: aggregateBillsByState → mapIndicators (useMemo)
PEV->>PEV: filteredBills (useMemo, sorted by last_action_date DESC, sliced to 50)
PEV-->>U: StateMap (recency heat) + PolicyFilterPanel + BillFeed
U->>PEV: clicks state on map / selects state dropdown
PEV->>PEV: setFilters({ stateFips })
PEV->>PEV: filteredBills re-derived (client-side, no re-fetch)
PEV-->>U: feed narrowed to selected state
U->>PEV: toggles status / topic chip
PEV->>PEV: setFilters({ statuses / topics })
PEV->>PEV: filteredBills re-derived (client-side)
PEV-->>U: feed filtered, no re-fetch
Reviews (1): Last reviewed commit: "feat(explore): surface policy bills as t..." | Re-trigger Greptile |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui-nextjs/app/globals.css`:
- Around line 109-123: The keyframes name billFadeIn violates
keyframes-name-pattern; rename the `@keyframes` identifier to kebab-case (e.g.,
bill-fade-in) and update the corresponding .bill-fade-in rule's animation
property to reference the new kebab-case name so the `@keyframes` declaration and
the animation usage match (update the symbol `@keyframes` billFadeIn and the
.bill-fade-in animation value).
In `@ui-nextjs/components/explore/BillFeedRow.tsx`:
- Around line 66-74: The "view" link currently uses generic link text and must
include an accessible name; update the anchor rendered in BillFeedRow (the <a>
that checks bill.url) to include an aria-label that embeds identifying bill
context (for example using bill.bill_number or bill.title) such as
`aria-label={`View details for ${bill.bill_number}`}` so screen readers hear a
unique, descriptive label for each bill row; ensure the field used exists on the
passed bill object (bill.bill_number or bill.title) and falls back to a safe
value if missing.
In `@ui-nextjs/components/explore/PhaseGlyph.tsx`:
- Around line 45-47: In PhaseGlyph, the pulsing segment class uses
'animate-pulse' without respecting reduced-motion preferences; update the
conditional that builds the class for pulse && isLastFilled (referenced
variables pulse and isLastFilled in the PhaseGlyph component) to append
'motion-reduce:animate-none' alongside 'animate-pulse' so the string becomes
'inline-block w-2 h-2.5 rounded-[1px] animate-pulse motion-reduce:animate-none'
when pulsing, leaving the non-pulse branch unchanged.
In `@ui-nextjs/components/explore/PolicyExploreView.tsx`:
- Around line 27-31: The feed defaults to national because filters.stateFips is
initialized as null; update PolicyExploreView to auto-select a state after
initial data load (or make the feed conditional on filters.stateFips) by: when
bills/state list is available, setFilters to a non-null stateFips (e.g., the
user's selectedState or first state in the available states) via a useEffect
that watches the loaded state list, or alter the filteredBills computation to
return an empty/state-scoped list when filters.stateFips is null; specifically
modify the useState<PolicyFilters> initialization and add a useEffect that calls
setFilters({ ...filters, stateFips: chosenFips }) or adjust the filteredBills
logic to guard on filters.stateFips (references: PolicyFilters, filters,
setFilters, filteredBills, PolicyExploreView).
🪄 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: ASSERTIVE
Plan: Pro
Run ID: ead69e5d-4a0e-4bf3-b507-e45c228705eb
📒 Files selected for processing (8)
ui-nextjs/app/explore/page.tsxui-nextjs/app/globals.cssui-nextjs/components/explore/BillFeedRow.tsxui-nextjs/components/explore/PhaseGlyph.tsxui-nextjs/components/explore/PolicyExploreView.tsxui-nextjs/components/explore/PolicyFilterPanel.tsxui-nextjs/components/explore/PolicyTable.tsxui-nextjs/lib/explore-config.ts
Backend: - Add ORDER BY last_action_date DESC NULLS LAST to /api/explore/policies so the 5000-row limit returns the actual newest bills, not an arbitrary subset. Without this, the client-side sort in PolicyExploreView was slicing an unordered sample. Frontend accessibility: - Add aria-label on BillFeedRow "view" links with bill number + title, so screen readers no longer hear a stream of identical "view" links. - Add motion-reduce:animate-none to the PhaseGlyph pulse segment; the global prefers-reduced-motion override only covered .bill-fade-in. Frontend correctness: - Rename @Keyframes billFadeIn → bill-fade-in (stylelint keyframes-name- pattern violation). - Use bill.url as the primary React key in the activity feed, with a composite fallback including introduced_date. OpenStates reuses bill_numbers across legislative sessions, so state+bill_number is not a unique key. - Memoize stateNameByFips so PolicyFilterPanel gets a stable prop reference across re-renders. - Gate empty-state message on allBills !== null; the previous code showed "select a state to begin monitoring" during the initial load before bills fetched. - Remove the dead "select a state" empty-state branch; the landing view intentionally shows the 50 newest bills nationally (discoverability goal from #183), so that branch was unreachable.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui-nextjs/components/explore/PolicyExploreView.tsx`:
- Around line 72-96: Compute the total number of matching bills before applying
FEED_LIMIT so the header shows the real filtered count: inside the useMemo that
defines filteredBills, derive a variable (e.g., totalFiltered = result.length)
immediately after filtering and before result.slice(...) and expose that value
to the UI (for example return an object {items: result.slice(0, FEED_LIMIT),
totalFiltered} or set a separate state/selector) then update the header to read
totalFiltered instead of allBills.length; touch the same logic referenced by
filteredBills, allBills, FEED_LIMIT, and filters so both the list and the
“showing X of Y” use the pre-slice total.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: ea1d14a3-da58-42b5-a966-c770ff95286d
📒 Files selected for processing (5)
src/d4bl/app/api.pyui-nextjs/app/globals.cssui-nextjs/components/explore/BillFeedRow.tsxui-nextjs/components/explore/PhaseGlyph.tsxui-nextjs/components/explore/PolicyExploreView.tsx
The feed header was showing 'showing {filteredBills.length} of {allBills.length}',
which was misleading once filters were active — it could display 'showing 12 of
5000' when only 12 bills actually matched the filter. Split the useMemo into
matchingBills (full filtered result, pre-slice) and filteredBills (sliced to
FEED_LIMIT), and use matchingBills.length as the denominator so the header now
honestly reports the filtered count.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui-nextjs/components/explore/PolicyExploreView.tsx`:
- Around line 35-36: In PolicyExploreView, ensure loading is cleared whenever
you bail out due to missing auth: before any early return that checks
session?.access_token (and the other similar early return around lines 50-51),
call setLoading(false) so loading cannot remain true; in practice add
setLoading(false) immediately before each `if (!session?.access_token) return;`
(and the other auth-return) or move the auth check earlier so you never
setLoading(true) before confirming session, ensuring setLoading(false) is always
invoked on all exit paths.
- Around line 129-131: The error message div in PolicyExploreView should be made
screen-reader announceable: update the JSX for the error block in the
PolicyExploreView component to include semantic alert attributes (e.g.,
role="alert", aria-live="assertive", aria-atomic="true") so assistive tech
announces fetch failures immediately, and ensure the error value is safely
stringified (e.g., use error?.message ?? String(error)) to satisfy TypeScript
strictness before rendering.
- Line 39: The frontend currently calls
fetch(`${API_BASE}/api/explore/policies?limit=5000`) in PolicyExploreView.tsx
which silently truncates results and feeds incomplete data into
aggregateBillsByState(); either implement proper pagination or surface the limit
to users: update the fetch flow in PolicyExploreView (and any caller of
aggregateBillsByState()) to support cursor/offset pagination by requesting
batches (e.g., while the API returns a nextCursor/hasMore) and merge results
before calling aggregateBillsByState(), or if the backend cannot be changed now,
add a clear UI badge/text near the map/feed stating “Showing first 5000 bills
(results limited)” and pass that metadata into aggregateBillsByState()/map
rendering so counts are not presented as complete. Ensure you reference the API
response fields (nextCursor/hasMore or offset+total) when implementing the loop
and update calls to aggregateBillsByState() to use the concatenated result set.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: 97f5e99f-6974-4f5e-8ad4-65421669f9c6
📒 Files selected for processing (1)
ui-nextjs/components/explore/PolicyExploreView.tsx
- Prevent loading from getting stuck at true when auth disappears mid-fetch. If the request was aborted during an auth change, the finally block skipped setLoading(false) (signal.aborted), and the next call returned early at the !session?.access_token check before setLoading(false) could run — leaving the UI spinning forever. Now the early-return path explicitly clears loading and allBills. - Surface the 5000-bill API cap in the feed header when the response hits the limit. Shows 'showing newest 5000' in amber with a tooltip explaining that per-state counts may be incomplete for dormant bills. Full cursor-based pagination is out of scope for this PR; this is the honest-label interim fix CodeRabbit suggested as an alternative. - Add role='alert' + aria-live='polite' to the error container so fetch failures are announced to screen readers.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui-nextjs/components/explore/PolicyExploreView.tsx`:
- Around line 177-183: The truncation warning that currently only uses a `title`
attribute is inaccessible on touch and for many screen readers; update the UI in
PolicyExploreView around the conditional that checks `allBills.length >=
API_BILL_LIMIT` to expose the cap explanation to assistive tech by adding an
accessible label (e.g., an `aria-label` on the outer span) and/or a
visually-hidden text node (using your app's `sr-only` utility class) with the
same message ("Response hit the {API_BILL_LIMIT}-bill cap; per-state counts may
be incomplete for dormant bills.") so the information is available to screen
readers and touch users while retaining the visible "showing newest
{API_BILL_LIMIT}" text; ensure any added attributes comply with TypeScript
strict typing.
🪄 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: ASSERTIVE
Plan: Pro
Run ID: 3a11404f-3fcf-4c75-b5a7-c3ae414a5972
📒 Files selected for processing (1)
ui-nextjs/components/explore/PolicyExploreView.tsx
The round-3 truncation warning used a `title` attribute alone for the cap explanation, but tooltips are unreliable for screen readers and absent on touch devices. Add aria-label with the full cap message so the context is announced by assistive tech while keeping the title for hover on desktop pointer devices.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui-nextjs/components/explore/PolicyExploreView.tsx`:
- Around line 39-42: In PolicyExploreView, the early-return branch that handles
missing session access_token calls setLoading(false) and setAllBills(null) but
doesn't clear the previous error, leaving the error banner visible; update that
branch to also call the error state setter (e.g., setError(null) or
setError(undefined) depending on your state shape) before returning so any prior
fetch error is cleared when auth is gone.
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b59d71b2-b514-4e7c-a850-40f5318acf08
📒 Files selected for processing (1)
ui-nextjs/components/explore/PolicyExploreView.tsx
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 2 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 2 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Summary
DataSourceTabs, matching the design direction in refactor(explore): surface policy bills as a top-level data source tab #183. The D4BL founder didn't know the feature existed when shown the tool; this PR makes it discoverable in zero clicks.last_action_dateDESC with stagger-fade on the first 5 rows.PhaseGlyphshowing where it sits in the legislative lifecycle (introduced/passed/signed/failed), mapped to the exact 5 status values the ingestion pipeline emits.What changed
New components (
ui-nextjs/components/explore/)PhaseGlyph.tsx— 4-segment monospace lifecycle indicator with tone-mapped colors (active green / signed bright / failed red / dormant gray), optional pulse on the last filled segmentBillFeedRow.tsx— one feed row with phase glyph, bill number, topic chips, title, relative last-action timestamp, view linkPolicyFilterPanel.tsx— state selector + status chips (with inline phase glyphs) + topic chips, narrowed types (Set<BillStatus>,Set<PolicyTopic>)PolicyExploreView.tsx— owns its own fetch lifecycle, composesStateMap+PolicyFilterPanel+ activity feedModified
ui-nextjs/lib/explore-config.ts— addedABBREV_TO_FIPS,aggregateBillsByState,daysSinceLastAction,billAggregateToIndicatorRow,formatRelativeDate,BILL_STATUSES/BillStatus,POLICY_TOPICS/PolicyTopic,statusToPhase+PhaseGlyphtypes, plus apolicyentry inDATA_SOURCESui-nextjs/app/explore/page.tsx— early-return infetchDatawhenactiveSource.key === 'policy'; branch render to<PolicyExploreView />in place of the metric-map layoutui-nextjs/app/globals.css—@keyframes billFadeIn+.bill-fade-inutility class +prefers-reduced-motionoverrideui-nextjs/components/explore/PolicyTable.tsx— importPOLICY_TOPICSfromexplore-config.ts(dedup with new filter panel)Design direction
The tab intentionally defaults to a state view, not a national aggregate — bills are event-shaped data (status + timestamps), not a continuous metric, so a choropleth of raw counts would just reward populous states. The map is recolored by recency of last bill action instead. Full design rationale is in #183's body.
What's explicitly out of scope
PolicyTable.STATUS_COLORSwithPhaseGlyph.TONE_COLORS(would change the visual of the existing per-state drawer on other tabs — valid follow-up, not this PR)<FilterChip>/<BillRow>primitive acrossPolicyTable/PolicyFilterPanel/MetricFilterPanel(same reasoning)Test plan
npm run lintinui-nextjs/— clean (0 errors, 1 pre-existing warning unrelated to this PR)npm run buildinui-nextjs/—/exploreroute compiles/explore→ click "Policy Bills" tab → bills load, feed shows most recent 50${State} — ${N} bills tracked · last action Xd agoPolicyBadgestill appears and opens the slide-out drawer as before (no regression)prefers-reduced-motionactive → stagger-fade animation disabledCloses #183
Summary by CodeRabbit
New Features
UI
Bug Fixes
Tweaks