Skip to content

2026 US Midterms Dashboard#4693

Merged
aseckin merged 63 commits into
mainfrom
2026-midterms
Jun 4, 2026
Merged

2026 US Midterms Dashboard#4693
aseckin merged 63 commits into
mainfrom
2026-midterms

Conversation

@aseckin
Copy link
Copy Markdown
Contributor

@aseckin aseckin commented May 5, 2026

Summary by CodeRabbit

  • New Features
    • 2026 US Midterms Hub: hero/OG image, interactive maps (SVG & tile), chamber control & congress outcome cards, seat‑distribution charts, consequence grid, watch cards, live badge, community insights carousel, insight tiles, new charts/gauges and map legend/components.
  • Localization
    • Full Midterms Hub copy added for English, Spanish, Portuguese, Czech, Traditional Chinese, and Simplified Chinese.
  • Chores
    • Added geodata/visualization libraries.
  • Bug Fixes
    • Minor whitespace/type-declaration formatting fix.
  • Documentation
    • Small README note about translation generation.

aseckin and others added 2 commits May 5, 2026 16:35
Adds /midterms-2026 hub modeled on /labor-hub: server-rendered page
with senate map (geographic on md+, tile grid on smaller screens),
chamber control sidebar, things-to-watch cards, conditional
consequences (mock data), and a data-driven community insights
carousel pulling key_factors and top comments from project 32840.

Post IDs in data.ts are placeholders (0); real question IDs will be
wired in once curated. OG image route mirrors /og/labor-hub. Adds 58
midtermsHub* i18n keys seeded with English copy across all six locale
files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves the Midterms 2026 hub from placeholder data to live API:
- Senate races resolved as subquestions of group post 40598
- Senate/House plurality and Congress outcome read from multiple-choice
  posts; Voter Turnout and Election Integrity wired through

Visual + UX overhaul:
- Adopted Labor Hub primitives (SectionCard / SectionHeader /
  ContentParagraph) and typography tokens across every section
- Map redrawn with react-simple-maps + d3-geo (geoAlbersUsa, hand-tuned
  scale + translate) cropped to the contested east; uncontested states
  recede at 50% opacity, contested states are clickable
- Things to Watch uses the consumer view tile (bell curve / radial
  gauge) flipping to a forecast timeline
- Community Insights renders only top comments via Labor Hub's
  ActivityCard purple variant in a gradient-faded carousel
- Electoral Consequences keeps mock rows with new mobile labels
- Hero, badges, chamber + congress cards retuned for sizing, padding
  and number alignment; tabular-nums on every percentage we render

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c959ad18-f046-45f4-b9a9-79b8532bccd5

📥 Commits

Reviewing files that changed from the base of the PR and between 76294ce and 1230c06.

📒 Files selected for processing (1)
  • front_end/src/utils/core/colors.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • front_end/src/utils/core/colors.ts

📝 Walkthrough

Walkthrough

Adds a Midterms 2026 hub: static race and tile data, server fetchers and post helpers, state-color and theme utilities, interactive geographic/tile maps with portalled tooltips, visual primitives and charts, cards/carousels/sections, OG preview route, i18n payloads across multiple locales, and dependency/docs updates.

Changes

Midterms 2026 feature

Layer / File(s) Summary
All Midterms hub files (single checkpoint)
front_end/src/app/(main)/midterms-2026/**/*, front_end/src/utils/*, front_end/messages/*, front_end/package.json, front_end/README.md, front_end/global.d.ts, front_end/src/components/*, front_end/src/app/og/midterms-2026/*, front_end/src/app/(storefront)/page.tsx, front_end/src/app/(main)/labor-hub/components/activity_card.tsx
Adds hub constants/data, cached server fetchers, post helpers, state color and theme hooks, geographic and tile maps with tooltip portal, CvBar/consequence gauge/seat-distribution chart components, carousel/cards/sections (Hero, Map, ThingsToWatch, Consequences, SeatDistributions, CommunityInsights, Footer), OG preview page and screenshot proxy route, locale strings for multiple languages, stripMarkdown, new chart/arc utilities, tile grid, and dependency + docs edits.

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs:

Suggested reviewers:

  • elisescu
  • hlbmtc
  • cemreinanc

"I nibble code and plant a rune,
Maps and charts beneath the moon.
Strings in tongues and tooltips bright,
A hub that hums through day and night.
🐇✨"

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-midterms

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Cleanup: Preview Environment Removed

The preview environment for this PR has been destroyed.

Resource Status
🌐 Preview App Deleted
🗄️ PostgreSQL Branch Deleted
⚡ Redis Database Deleted
🔧 GitHub Deployments Removed
📦 Docker Image Retained (auto-cleanup via GHCR policies)

Cleanup triggered by PR close at 2026-06-04T18:13:26Z

Copy link
Copy Markdown
Contributor

@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: 18

🧹 Nitpick comments (5)
front_end/src/app/og/midterms-2026/page.tsx (1)

27-41: ⚡ Quick win

Move OG copy to i18n keys instead of hardcoded English literals.

The heading and description are hardcoded in TSX. Please wire these through translations so this page stays consistent with the rest of the localized Midterms hub content.

Based on learnings: Do not hardcode English strings in TSX components; prefer i18n strings via the app’s translation setup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/og/midterms-2026/page.tsx` around lines 27 - 41, Replace
the hardcoded heading and paragraph in the Midterms 2026 page with i18n keys:
import the app's translation hook (e.g., useTranslations or useT) at the top of
the component, call it (e.g., const t = useTranslations('Midterms2026')), and
replace the two <span> parts and the <p> copy with t('title.part1'),
t('title.part2'), and t('description') respectively so the JSX becomes styled
spans using those translation values; then add matching keys (title.part1,
title.part2, description) to the locale JSONs for all supported languages.
Ensure existing className and inline style attributes remain unchanged and that
you pass any needed HTML/markup-safe variants if your i18n library requires it.
front_end/src/app/(main)/midterms-2026/components/tile_map.tsx (1)

31-41: ⚡ Quick win

Use a container ref instead of closest(".tile-map-container").

handleEnter resolves the positioning origin via e.currentTarget.closest(".tile-map-container"), which silently couples this component to a magic class name on its own root div (line 52). If anyone renames or removes the class — including a stylesheet refactor — the tooltip stops appearing without any TypeScript or runtime error. A useRef on the wrapper <div> would make the dependency type-checked and refactor-proof.

♻️ Sketch
-import { FC, MouseEvent, useState } from "react";
+import { FC, MouseEvent, useRef, useState } from "react";
@@
 const TileMap: FC<Props> = ({ races }) => {
   const [hovered, setHovered] = useState<HoverState>(null);
+  const containerRef = useRef<HTMLDivElement>(null);
   const racesByState = new Map(races.map((r) => [r.state, r]));
@@
-    const parent = e.currentTarget.closest(".tile-map-container");
-    if (!parent) return;
+    const parent = containerRef.current;
+    if (!parent) return;
     const parentRect = parent.getBoundingClientRect();
@@
-    <div className="tile-map-container relative">
+    <div ref={containerRef} className="relative">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/components/tile_map.tsx around lines
31 - 41, The handler handleEnter currently uses
e.currentTarget.closest(".tile-map-container") which couples the logic to a
magic class; change the wrapper div to use a React ref (e.g., const containerRef
= useRef<HTMLDivElement | null>(null)) and replace the closest lookup with
containerRef.current (use containerRef.current.getBoundingClientRect()) when
computing parentRect; update the wrapper <div> to ref={containerRef} and ensure
handleEnter still calls setHovered with the computed x/y using that ref, and
guard for null ref before computing coordinates.
front_end/src/app/(main)/midterms-2026/components/insight_card.tsx (1)

33-46: ⚡ Quick win

Replace custom regex markdown stripper with existing strip-markdown utility.

The current stripMarkdown() function uses regex replacements that handle bold, italic, links, inline-code, blockquotes, and newlines, but misses headers (#), lists (-, *), fenced code blocks (```), images (![alt](url)), HTML, tables, and escaped characters — all commonly used in Metaculus comments. This causes leaked markup in carousel previews.

The codebase already imports and uses strip-markdown in front_end/src/utils/markdown.ts via the remark parser. Consider either:

  • Importing strip-markdown directly for simple 320-character truncation
  • Creating a simple wrapper function in utils/markdown.ts to standardize markdown stripping across the app

This avoids maintaining a parallel regex stripper and handles the full spectrum of markdown syntax.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/components/insight_card.tsx around
lines 33 - 46, Replace the custom regex stripper in insight_card.tsx by using
the project-wide markdown utility: remove the local stripMarkdown function and
import the standardized strip-markdown wrapper from
front_end/src/utils/markdown.ts (or import strip-markdown directly) and call it
inside extractCommentText(comment: CommentType) before slicing to 320 chars;
ensure you use the synchronous wrapper/signature provided by the utils module
(or create one there that returns a plain string) so extractCommentText keeps
returning stripResult.slice(0, 320) and no async changes are needed.
front_end/src/app/(main)/midterms-2026/page.tsx (1)

37-52: No Suspense boundaries — all five async sections must resolve before any HTML is streamed.

Each section (ElectionsMapSection, ThingsToWatchSection, etc.) performs independent data fetches. Without wrapping them in <Suspense>, Next.js 15 holds the response until every async server component finishes, making page TTFB as slow as the combined critical path of the slowest fetch.

Wrapping each section individually in <Suspense fallback={<SectionSkeleton />}> lets the page shell arrive immediately and each section stream in as its data resolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/page.tsx around lines 37 - 52, The
page renders five async sections without Suspense boundaries, causing Next.js to
wait for all fetches before streaming; wrap each async component
(ElectionsMapSection, ThingsToWatchSection, ElectoralConsequencesSection,
CommunityInsightsSection, FooterSection) in a React <Suspense
fallback={<SectionSkeleton/>}> boundary so the shell streams immediately and
each section streams in as its data resolves, and add the necessary import for
Suspense from 'react' and a lightweight SectionSkeleton (or per-section
skeletons) to use as the fallback.
front_end/declarations/react-simple-maps.d.ts (1)

1-12: ⚡ Quick win

projection type is incomplete — forces an unsafe double-cast in geographic_map.tsx.

The current union string | ((opts: { width: number; height: number }) => unknown) omits the D3 GeoProjection object variant that react-simple-maps v3 accepts directly. This is why geographic_map.tsx (line 139) needs projection as unknown as string — a cast that fully disables type-checking for that prop, even though projection is created from geoAlbersUsa().scale(...).translate(...).

Since d3 is already a project dependency, GeoProjection is available:

♻️ Proposed fix
 declare module "react-simple-maps" {
   import * as React from "react";
+  import type { GeoProjection } from "d3-geo";

   export interface ComposableMapProps extends React.SVGProps<SVGSVGElement> {
     projection?:
       | string
+      | GeoProjection
       | ((opts: { width: number; height: number }) => unknown);

With this change, geographic_map.tsx can drop the double-cast:

-  projection={projection as unknown as string}
+  projection={projection}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/declarations/react-simple-maps.d.ts` around lines 1 - 12, The
projection prop in ComposableMapProps is missing the D3 GeoProjection type which
forces unsafe casts; update the declare module "react-simple-maps" by importing
GeoProjection from "d3-geo" and include GeoProjection in the union for
projection (alongside string and the function type) so code like
geographic_map.tsx can pass a geoAlbersUsa() result without double-casting;
modify the ComposableMapProps interface (projection?) accordingly to accept
string | GeoProjection | ((opts: { width: number; height: number }) => unknown).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front_end/messages/cs.json`:
- Around line 2189-2234: The Czech locale file contains many midtermsHub* keys
with English text (e.g., midtermsHubMetaTitle, midtermsHubMetaDescription,
midtermsHubHeroTitleLine1, midtermsHubHeroSubtitle,
midtermsHubThingsToWatchSubtitle, midtermsHubDemPct, midtermsHubRepPct,
midtermsHubFooterDisclaimer, midtermsHubClickToView, etc.); replace each English
string in cs.json with the proper Czech translation, or if translations are not
ready, route these specific keys to the English locale fallback (so Czech users
don’t see untranslated UI) by updating the cs locale entries to reference the en
values or configuring the i18n fallback for these keys until localized. Ensure
you update every midtermsHub* key present in the diff (not just a subset) so
there are no remaining English strings.

In `@front_end/messages/en.json`:
- Line 2191: The translation key "midtermsHubCongressSummary" currently contains
a hardcoded forecast sentence; change it to a neutral templated string (e.g. a
placeholder like "{forecastSummary}" or "{summary}") in en.json and update the
consumer that renders this key to interpolate and supply current forecast text
from the live forecast data (or remove the key and render the forecast text
directly from the forecast component); ensure the identifier
"midtermsHubCongressSummary" remains so callers can switch to the interpolated
value without breaking references.

In `@front_end/messages/es.json`:
- Around line 2189-2234: The new Midterms Hub localization keys (e.g.,
midtermsHubMetaTitle, midtermsHubMetaDescription, midtermsHubHeroTitleLine1,
midtermsHubHeroTitleLine2, midtermsHubHeroSubtitle,
midtermsHubThingsToWatchSubtitle, midtermsHubConsequencesSubtitle,
midtermsHubCongressSummary, midtermsHubThingsToWatch, midtermsHubVoterTurnout,
midtermsHubTurnoutContext, midtermsHubElectionIntegrity,
midtermsHubIntegrityContext, midtermsHubElectoralConsequences,
midtermsHubConsequenceQuestion, midtermsHubConsequenceClimate,
midtermsHubConsequenceMinWage, midtermsHubConsequenceImmigration,
midtermsHubConsequenceShutdown, midtermsHubCommunityInsights,
midtermsHubScrollLeft, midtermsHubScrollRight, midtermsHubDemocrat,
midtermsHubRepublican, midtermsHubNotContested, midtermsHubDemPct,
midtermsHubRepPct, midtermsHubNoForecast, midtermsHubFooterDisclaimer,
midtermsHubUpdatedRealtime, midtermsHubMetaculusUser, midtermsHubComingSoon,
midtermsHubClickToView) are still in English in front_end/messages/es.json;
translate each value into Spanish (or replace with a clear Spanish fallback
strategy) so Spanish users see localized UI/metadata, keeping placeholders like
{date}, {count}, and {pct} intact and preserving punctuation and capitalization
conventions appropriate for Spanish.

In `@front_end/messages/pt.json`:
- Around line 2187-2232: The pt.json file contains new midtermsHub* keys with
English text (e.g., midtermsHubMetaTitle, midtermsHubHeroSubtitle,
midtermsHubChamberSenate, midtermsHubThingsToWatch, midtermsHubComingSoon,
midtermsHubFooterDisclaimer, etc.); replace each English value with proper
Portuguese translations for those keys or remove the keys so the app falls back
to the default locale; ensure you update every midtermsHub* entry introduced in
the diff (midtermsHubMetaTitle through midtermsHubClickToView) so no English
placeholders remain in the Portuguese locale.

In `@front_end/messages/zh.json`:
- Around line 2191-2236: The zh.json entries for the new midterms hub are still
in English (keys like "midtermsHubMetaTitle", "midtermsHubMetaDescription",
"midtermsHubLastUpdatedFull", "midtermsHubChamberSenate",
"midtermsHubChamberHouse", "midtermsHubChamberGovernor",
"midtermsHubChamberControl", "midtermsHubCongressForecast",
"midtermsHubDemsNeed", "midtermsHubOutcomeRepRep", "midtermsHubOutcomeRepDem",
"midtermsHubOutcomeDemRep", "midtermsHubOutcomeDemDem",
"midtermsHubCongressSummary", "midtermsHubThingsToWatch",
"midtermsHubVoterTurnout", "midtermsHubTurnoutContext",
"midtermsHubElectionIntegrity", "midtermsHubIntegrityContext",
"midtermsHubElectoralConsequences", "midtermsHubConsequenceQuestion",
"midtermsHubConsequenceIfRep", "midtermsHubConsequenceIfDem",
"midtermsHubConsequenceClimate", "midtermsHubConsequenceMinWage",
"midtermsHubConsequenceImmigration", "midtermsHubConsequenceShutdown",
"midtermsHubCommunityInsights", "midtermsHubScrollLeft",
"midtermsHubScrollRight", "midtermsHubDemocrat", "midtermsHubRepublican",
"midtermsHubNotContested", "midtermsHubDemPct", "midtermsHubRepPct",
"midtermsHubNoForecast", "midtermsHubFooterDisclaimer",
"midtermsHubHeroTitleLine1", "midtermsHubHeroTitleLine2",
"midtermsHubHeroSubtitle", "midtermsHubThingsToWatchSubtitle",
"midtermsHubConsequencesSubtitle", "midtermsHubUpdatedRealtime",
"midtermsHubMetaculusUser", "midtermsHubComingSoon", "midtermsHubClickToView");
translate each English value into appropriate Simplified Chinese copy and
replace the English strings in zh.json, preserving placeholders like {date},
{count}, and {pct} exactly and keeping key names unchanged.

In `@front_end/src/app/`(main)/midterms-2026/components/chamber_control_card.tsx:
- Around line 7-10: CURRENT_SENATE and CURRENT_HOUSE are hardcoded and the
option-label literals "Democrats"/"Republicans" are inlined causing silent
breakage; update the component to import baseline seat counts from a shared data
source (e.g., data.ts) or fetch the current counts at runtime so CURRENT_SENATE
and CURRENT_HOUSE are not hardcoded in chamber_control_card.tsx, and replace the
literal option strings used with named constants defined alongside
CHAMBER_QUESTIONS (and used by getMultipleChoiceOptionProbability) so label
changes are greppable and centralized; ensure the component falls back
gracefully if getMultipleChoiceOptionProbability returns null and logs or
displays a clear placeholder.
- Around line 83-89: The text percentages and bar widths disagree because
demShare/repShare are normalized (demProb/(demProb+repProb)) while demPct/repPct
use raw probabilities; update the displayed percentages to use the same
normalized shares as the bars: compute total as now, derive demShare and
repShare, then set demPct = demShare != null ? Math.round(demShare * 10) / 10 :
null and repPct = repShare != null ? Math.round(repShare * 10) / 10 : null (or
similar rounding) so the text percentages and the bar visualization (using
demShare/repShare) match; keep demProb/repProb available if you later decide to
add a third "other" segment.

In `@front_end/src/app/`(main)/midterms-2026/components/chamber_tabs.tsx:
- Around line 35-48: The tooltip for inactive tabs in chamber_tabs.tsx is
inaccessible because the button is disabled (unfocusable) and the tooltip only
appears on hover; change the <button> for inactive tabs to remain focusable by
replacing disabled with aria-disabled="true" and ensure it has tabIndex={0}, add
a unique id on the tooltip <span> and set aria-describedby pointing from the
button to that id, and make the tooltip visible on keyboard focus as well as
hover (e.g., toggle aria-hidden and the visible class on focus/blur or use
group-focus utility) so screen readers and keyboard users can discover the
"Coming soon" message while preserving the non-interactive semantics.

In `@front_end/src/app/`(main)/midterms-2026/components/congress_outcome_card.tsx:
- Around line 80-87: In congress_outcome_card.tsx the inline style forces at
least a 1% bar by using Math.max(o.pct ?? 0, 1) which displays a bar for null or
0 values; change the logic so the bar width is set to `${o.pct > 0 ? o.pct :
0}%` (or conditionally render the bar element only when o.pct > 0) and leave the
displayed percentage span as `{o.pct != null ? `${o.pct.toFixed(1)}%` : "—"}` so
zeros and nulls are not visually exaggerated.

In `@front_end/src/app/`(main)/midterms-2026/components/consequence_row.tsx:
- Around line 66-73: Clamp the percent value before rendering: inside the
ConsequenceRow component (where pct is used), compute a clampedPct =
Math.min(100, Math.max(0, pct)) and use clampedPct for the inline style width
and for the displayed text instead of the original pct so the bar cannot
overflow or render oddly when pct is outside 0..100.

In `@front_end/src/app/`(main)/midterms-2026/components/state_tooltip.tsx:
- Around line 18-38: The tooltip currently uses isDem (false when demWinPct is
null) to pick MIDTERMS_COLORS.repPrimary, making "no forecast" appear red;
change the color logic in state_tooltip.tsx (look for demWinPct, isDem,
probLabel, MIDTERMS_COLORS) so that when demWinPct == null you use a neutral
color (e.g. MIDTERMS_COLORS.neutral or a provided fallback) instead of the
repPrimary/demPrimary branch; update the style expression for the span's color
to: demWinPct == null ? MIDTERMS_COLORS.neutral : isDem ?
MIDTERMS_COLORS.demPrimary : MIDTERMS_COLORS.repPrimary.

In `@front_end/src/app/`(main)/midterms-2026/data.ts:
- Around line 62-68: MOCK_CONSEQUENCES is hardcoded placeholder data being shown
as real percentages; replace its direct use in the UI by gating the display
behind a feature flag or "Coming soon" state and wire the consumer to real
conditional-question data when available. Specifically, stop exporting/consuming
MOCK_CONSEQUENCES as live output in components that render ConsequenceRow data
(look for usages of MOCK_CONSEQUENCES and the ConsequenceRow type), add a
boolean flag (e.g., showConsequences or feature flag) that toggles between
rendering a non-forecast placeholder message and the real data source, and
update the data-loading flow to pull the actual conditional questions/results
into the same consumer once ready. Ensure the UI shows the placeholder text
instead of these hardcoded percentages until the real feed is connected.

In `@front_end/src/app/`(main)/midterms-2026/helpers/fetch_dashboard_data.ts:
- Around line 58-83: fetchChamberData currently lets
ServerPostsApi.getPostsWithCP errors bubble up (causing Promise.all consumers to
fail); wrap the API call in a try/catch around ServerPostsApi.getPostsWithCP
inside fetchChamberData and on any error return the same default ChamberData
shape used when ids is empty (all fields null) so the UI degrades gracefully
like fetchSenateRaces; keep the existing mapping logic when the call succeeds
and only use the default null-filled object in the catch path.

In `@front_end/src/app/`(main)/midterms-2026/helpers/post_utils.ts:
- Around line 56-66: getNumericForecast currently casts post.question to
QuestionWithNumericForecasts and scales centers[0] without verifying the
question type; add the same question-type guard used in
getQuestionBinaryProbability/getMultipleChoiceOptionProbability (check
question.type/isNumeric/isDiscrete/isDate or a shared type-guard) at the top of
getNumericForecast (or return null if the question is not a numeric-like
question), then proceed to read question.aggregations/...centers[0] and call
scaleInternalLocation only for numeric-like questions to avoid scaling
probability values from binary/multiple-choice posts.

In `@front_end/src/app/`(main)/midterms-2026/sections/community_insights.tsx:
- Around line 12-15: fetchCommunityInsights() can throw and crash the page; wrap
the call in a try/catch (while still calling getTranslations()) and treat any
failure as a benign absence of data: e.g. call fetchCommunityInsights() inside
try, assign to insights, and on catch set insights = [] or return null so the
component returns null instead of throwing; also guard for non-array results
before checking insights.length (use Array.isArray(insights)). Target the
existing getTranslations(), fetchCommunityInsights(), and the insights variable
in this file.

In `@front_end/src/app/`(main)/midterms-2026/sections/footer.tsx:
- Line 1: The footer currently imports and uses format from date-fns which
formats in the runtime/local timezone; replace that with formatInTimeZone from
date-fns-tz and render the timestamp in true UTC. Concretely: change the import
to import { formatInTimeZone } from "date-fns-tz" (removing format), then update
any call site(s) such as where you compute the "Last updated" string (e.g., uses
of format(updatedAt, "HH:mm 'UTC'") or similar) to use
formatInTimeZone(updatedAt, "UTC", "HH:mm 'UTC'") so the displayed HH:mm matches
actual UTC time. Ensure you update all occurrences in footer.tsx that format the
update timestamp.

In `@front_end/src/app/og/midterms-2026/route/route.ts`:
- Around line 10-13: Guard against missing screenshot service env vars before
building the URL and making requests: check
process.env.SCREENSHOT_SERVICE_API_URL (and
process.env.SCREENSHOT_SERVICE_API_KEY) and return/throw early or skip calling
new URL if they are falsy, move the new URL(...) creation for screenshotEndpoint
into the try block (or after the guard) to avoid throwing outside error
handling, and when setting headers in the request (the code around the lines
handling the API key) do not add an empty API key header—only include the
Authorization/API key header if SCREENSHOT_SERVICE_API_KEY is present so
misconfiguration surfaces immediately instead of producing noisy downstream
failures.
- Around line 24-31: The POST to screenshotEndpoint uses fetch without a timeout
causing potential hangs; wrap the call in an AbortController (create a
controller, pass controller.signal into the fetch call where r is assigned),
start a setTimeout to call controller.abort() after a chosen timeout (e.g.
3–10s), and clear the timeout after fetch finishes (in finally). Update the
fetch invocation that sends payload to include the signal and handle the
abort/timeout case (detect AbortError or check r.ok) so the route returns a
proper error response instead of hanging.

---

Nitpick comments:
In `@front_end/declarations/react-simple-maps.d.ts`:
- Around line 1-12: The projection prop in ComposableMapProps is missing the D3
GeoProjection type which forces unsafe casts; update the declare module
"react-simple-maps" by importing GeoProjection from "d3-geo" and include
GeoProjection in the union for projection (alongside string and the function
type) so code like geographic_map.tsx can pass a geoAlbersUsa() result without
double-casting; modify the ComposableMapProps interface (projection?)
accordingly to accept string | GeoProjection | ((opts: { width: number; height:
number }) => unknown).

In `@front_end/src/app/`(main)/midterms-2026/components/insight_card.tsx:
- Around line 33-46: Replace the custom regex stripper in insight_card.tsx by
using the project-wide markdown utility: remove the local stripMarkdown function
and import the standardized strip-markdown wrapper from
front_end/src/utils/markdown.ts (or import strip-markdown directly) and call it
inside extractCommentText(comment: CommentType) before slicing to 320 chars;
ensure you use the synchronous wrapper/signature provided by the utils module
(or create one there that returns a plain string) so extractCommentText keeps
returning stripResult.slice(0, 320) and no async changes are needed.

In `@front_end/src/app/`(main)/midterms-2026/components/tile_map.tsx:
- Around line 31-41: The handler handleEnter currently uses
e.currentTarget.closest(".tile-map-container") which couples the logic to a
magic class; change the wrapper div to use a React ref (e.g., const containerRef
= useRef<HTMLDivElement | null>(null)) and replace the closest lookup with
containerRef.current (use containerRef.current.getBoundingClientRect()) when
computing parentRect; update the wrapper <div> to ref={containerRef} and ensure
handleEnter still calls setHovered with the computed x/y using that ref, and
guard for null ref before computing coordinates.

In `@front_end/src/app/`(main)/midterms-2026/page.tsx:
- Around line 37-52: The page renders five async sections without Suspense
boundaries, causing Next.js to wait for all fetches before streaming; wrap each
async component (ElectionsMapSection, ThingsToWatchSection,
ElectoralConsequencesSection, CommunityInsightsSection, FooterSection) in a
React <Suspense fallback={<SectionSkeleton/>}> boundary so the shell streams
immediately and each section streams in as its data resolves, and add the
necessary import for Suspense from 'react' and a lightweight SectionSkeleton (or
per-section skeletons) to use as the fallback.

In `@front_end/src/app/og/midterms-2026/page.tsx`:
- Around line 27-41: Replace the hardcoded heading and paragraph in the Midterms
2026 page with i18n keys: import the app's translation hook (e.g.,
useTranslations or useT) at the top of the component, call it (e.g., const t =
useTranslations('Midterms2026')), and replace the two <span> parts and the <p>
copy with t('title.part1'), t('title.part2'), and t('description') respectively
so the JSX becomes styled spans using those translation values; then add
matching keys (title.part1, title.part2, description) to the locale JSONs for
all supported languages. Ensure existing className and inline style attributes
remain unchanged and that you pass any needed HTML/markup-safe variants if your
i18n library requires it.
🪄 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: CHILL

Plan: Pro

Run ID: 7f9b684e-1062-44a5-9327-4d9c934bea6f

📥 Commits

Reviewing files that changed from the base of the PR and between 4ac15b6 and e4644f8.

⛔ Files ignored due to path filters (1)
  • front_end/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • front_end/declarations/react-simple-maps.d.ts
  • front_end/global.d.ts
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/package.json
  • front_end/public/us-states-10m.json
  • front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx
  • front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx
  • front_end/src/app/(main)/midterms-2026/components/consumer_tile_client.tsx
  • front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx
  • front_end/src/app/(main)/midterms-2026/components/insight_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/insights_carousel.tsx
  • front_end/src/app/(main)/midterms-2026/components/live_badge.tsx
  • front_end/src/app/(main)/midterms-2026/components/map_legend.tsx
  • front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx
  • front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx
  • front_end/src/app/(main)/midterms-2026/components/tile_map.tsx
  • front_end/src/app/(main)/midterms-2026/components/watch_card.tsx
  • front_end/src/app/(main)/midterms-2026/constants.ts
  • front_end/src/app/(main)/midterms-2026/data.ts
  • front_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.ts
  • front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts
  • front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts
  • front_end/src/app/(main)/midterms-2026/helpers/state_color.ts
  • front_end/src/app/(main)/midterms-2026/page.tsx
  • front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx
  • front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx
  • front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx
  • front_end/src/app/(main)/midterms-2026/sections/footer.tsx
  • front_end/src/app/(main)/midterms-2026/sections/hero.tsx
  • front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx
  • front_end/src/app/og/midterms-2026/page.tsx
  • front_end/src/app/og/midterms-2026/route/route.ts

Comment thread front_end/messages/cs.json Outdated
Comment thread front_end/messages/en.json Outdated
Comment thread front_end/messages/es.json Outdated
Comment thread front_end/messages/pt.json Outdated
Comment thread front_end/messages/zh.json Outdated
Comment thread front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts
Comment thread front_end/src/app/(main)/midterms-2026/sections/footer.tsx Outdated
Comment thread front_end/src/app/og/midterms-2026/route/route.ts Outdated
Comment thread front_end/src/app/og/midterms-2026/route/route.ts
UI/UX
- Continuous color gradient for senate states (getColorInSpectrum) so
  adjacent forecasts (e.g. 37% vs 45% Dem) read as visibly different
- Theme-aware map stroke, uncontested fill, chamber bar divider so the
  map blends with the SectionCard in both light and dark modes
- Higher light-mode opacity for uncontested states (0.75 / 1.0 hover)
- Tile + geographic map tooltips now render via a portal anchored in
  document.body, with viewport clamping so tile taps near a screen edge
  no longer get clipped
- Two-stage tap on tile map: first tap shows tooltip, tapping the
  tooltip navigates (mouse devices keep one-click navigation)
- Map tabs / legend offset matches sidebar card padding; legend stacks
  vertically below xl so it doesn't collide with tabs
- Mobile: LiveBadge hidden, hero subtitle uses the same typography as
  Things-to-Watch
- Bars (Chamber, Congress, Consequences) reskinned to consumer-view
  style — softer fill, sharper border, theme-aware border color, hover
  state driven by the parent row (group/cv) so the whole row reacts
- Click-to-open on Chamber and Congress rows (opens question new tab)
- Congress Outcome rows now stack bar under label so labels can use
  full width
- Tile map vertically centered in the side-by-side layout (md to <lg)

Layout
- Tile map renders below lg (was below md); geographic map at lg+
- Section grid is 2-col at md+ (tile + sidebar) with the chamber sidebar
  capped at 35%
- Hero title + description live on the page bg, outside the white card

Insights
- Community Insights uses a blue ActivityCard variant (added to
  Labor Hub's activity_card)
- Carousel has gradient fade-out cutoffs and arrow controls inline with
  the section header

Code review fixes
- community_insights wraps fetchCommunityInsights in try/catch with
  Array.isArray guard so a comments-API hiccup can't crash the page
- footer uses formatInTimeZone(latest, "UTC", ...) so the displayed
  HH:mm matches actual UTC regardless of server locale
- og/route guards SCREENSHOT_SERVICE_API_URL/_KEY env vars and skips
  the api_key header when unset

i18n
- Translated all midtermsHub* keys to es / cs / pt / zh / zh-TW
- "Congress Control Forecast" → "Congress Control"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@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: 8

♻️ Duplicate comments (1)
front_end/src/app/og/midterms-2026/route/route.ts (1)

41-45: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing request timeout — screenshot call can hang indefinitely.

This fetch still has no AbortController/signal, so a stalled screenshot backend will hold the server-side route open until the Node.js process-level timeout (or forever), tying up a server thread.

⏱️ Proposed fix — add an AbortController timeout
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10_000);
     const r = await fetch(screenshotEndpoint, {
       method: "POST",
       headers,
       body: JSON.stringify(payload),
+      signal: controller.signal,
     });
+    clearTimeout(timeoutId);

And update the catch block to surface timeout errors distinctly:

-  } catch {
-    return NextResponse.json({ error: "screenshot failed" }, { status: 500 });
+  } catch (err) {
+    const isTimeout = err instanceof Error && err.name === "AbortError";
+    return NextResponse.json(
+      { error: isTimeout ? "screenshot timed out" : "screenshot failed" },
+      { status: 504 }
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/og/midterms-2026/route/route.ts` around lines 41 - 45, Add
an AbortController-based timeout around the POST to screenshotEndpoint: create
an AbortController, set a timer (e.g. setTimeout) that calls controller.abort()
after a chosen ms, pass controller.signal into the fetch options alongside
method/headers/body, and clear the timer once the response arrives; update the
existing catch block that awaits the fetch (the try/catch surrounding the fetch
to screenshotEndpoint that assigns to r) to detect an abort/timeout (check for
error.name === "AbortError" or error.type === "aborted") and surface/log a
distinct timeout error vs other fetch errors. Ensure you reference the existing
local variables screenshotEndpoint, headers, payload and the response variable r
when implementing the change.
🧹 Nitpick comments (1)
front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx (1)

12-17: ⚡ Quick win

Hardcoded MC option labels couple this component to upstream question wording.

"Rep Senate / Rep House" etc. must match the post's MC option strings exactly. If a question author ever edits the option text (typo fix, "Republican Senate / Republican House", etc.), getMultipleChoiceOptionProbability returns null for all four outcomes and the card silently degrades to four em-dashes with no signal.

Define these labels alongside the question constant in a shared module so the coupling is greppable, and consider asserting (or warning) when none of the four lookups match.

Also applies to: 38-41

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/components/congress_outcome_card.tsx
around lines 12 - 17, The OUTCOME_OPTION_LABEL mapping in
congress_outcome_card.tsx is hardcoded and must instead be defined alongside the
question's MC option constants in a shared module so the label keys stay in sync
with the authored question text; move the four option strings into the shared
question constant (exported), import and use that shared mapping in
OUTCOME_OPTION_LABEL or eliminate OUTCOME_OPTION_LABEL and reference the shared
strings directly when calling getMultipleChoiceOptionProbability, and add a
runtime check in the component (using getMultipleChoiceOptionProbability) to log
or assert (e.g., processLogger.warn or console.warn) if all four lookups return
null so authors are alerted when labels no longer match the question options.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front_end/messages/pt.json`:
- Line 2200: The string for key "midtermsHubCongressSummary" contains hardcoded
forecast outcome text that can become stale; replace it with a neutral,
non-committal template or interpolation token (e.g., a generic summary like "As
previsões atuais para o Congresso mostram resultados variados; veja os detalhes
nos gráficos." or a template expecting runtime variables) and update the UI to
render dynamic forecast values instead of this fixed sentence so the message
uses live data from the forecast model rather than static text.

In `@front_end/src/app/`(main)/midterms-2026/components/cv_bar.tsx:
- Around line 47-52: The width floor in cv_bar.tsx is forcing a visible 1% for
zero/unknown probabilities; change the width assignment in the style object to
use the raw pct (coerced to 0 for null/undefined) instead of Math.max — e.g. set
width to `${pct ?? 0}%` so pct === 0 renders a 0-width bar and callers who want
a minimum-visible sliver can apply their own Math.max(pct, MIN_VISIBLE) before
passing pct into this component; update the style definition (the const style
object) accordingly and remove Math.max usage.

In `@front_end/src/app/`(main)/midterms-2026/components/geographic_map.tsx:
- Around line 184-188: The contested state regions are only mouse-interactive;
make them keyboard-accessible by adding tabindex={0} and role="button" to the
interactive element(s) and by wiring keyboard handlers: implement onKeyDown that
triggers the same actions as onClick when Enter or Space is pressed (call
handleClick(race) when isContested), and call handleEnter(abbr, e) on focus (or
onFocus) and setHovered(null) on blur to mirror the current
onMouseEnter/onMouseLeave behavior; update the elements using the existing
symbols isContested, abbr, handleEnter, handleClick, setHovered so keyboard
users can focus and activate the contested states.
- Line 187: The current onMouseLeave on the SVG path clears hovered immediately,
unmounting the tooltip before its handlers run; change the logic so hovered
remains set while the pointer is over the tooltip portal. Concretely, modify the
SVG path onMouseLeave handler (where setHovered(null) is called) to detect
pointer transition into the tooltip (use event.relatedTarget or PointerEvent and
contains checks) and only clear hovered if the pointer left both the path and
the tooltip; also add onMouseEnter/onMouseLeave (or
onPointerEnter/onPointerLeave) handlers to the tooltip portal element that call
setHovered(id) on enter and setHovered(null) on leave so the tooltip can receive
clicks and onDismiss. Ensure you reference the existing setHovered and hovered
state and the tooltip portal render to implement these handlers.

In `@front_end/src/app/`(main)/midterms-2026/components/map_tooltip_portal.tsx:
- Around line 55-67: The viewport-top check in useLayoutEffect incorrectly mixes
viewport coordinates with document scroll (rect.top vs window.scrollY); update
the logic that computes placeBelow inside useLayoutEffect (near tooltipRef, rect
and setAdjustment) to use a pure viewport-local comparison such as const
placeBelow = rect.top < VIEWPORT_PADDING (or equivalently rect.top -
VIEWPORT_PADDING < 0) instead of comparing rect.top to window.scrollY so the
tooltip placement is based only on viewport clipping.
- Around line 69-80: The click-outside listener in the useEffect (handler
function "handle" which uses onDismiss, insideRef, tooltipRef) should listen for
"mousedown" instead of "click" to avoid the race with the opener's onClick;
update document.addEventListener("click", handle) to
document.addEventListener("mousedown", handle) and the corresponding removal
document.removeEventListener("click", handle) to
document.removeEventListener("mousedown", handle) while keeping the same cleanup
and dependency array.

In `@front_end/src/app/`(main)/midterms-2026/components/responsive_map.tsx:
- Around line 18-24: The mobile branch currently hides ChamberTabs so small
screens lose the chamber selector; update the lg:hidden block that renders
TileMap to also render the same ChamberTabs above TileMap so the tabs are
visible on mobile. Specifically, in the JSX branch that contains <TileMap
races={races} /> (the div with className "flex h-full items-center p-5
lg:hidden"), add the <ChamberTabs /> component (matching the one used with
<GeographicMap />) above the TileMap container so both TileMap and ChamberTabs
render on small screens.

In `@front_end/src/app/og/midterms-2026/route/route.ts`:
- Around line 47-50: The code currently forwards the screenshot service HTTP
status (variable r) directly to the client via NextResponse.json; change this so
any non-ok upstream response (r.ok === false) returns a normalized generic error
to callers—e.g., return NextResponse.json({ error: "Upstream service error" }, {
status: 502 })—so that 4xx/5xx from the screenshot backend are not leaked;
update the if (!r.ok) branch in route.ts (the block using r and
NextResponse.json) to always respond with a generic message and status 502
instead of r.status.

---

Duplicate comments:
In `@front_end/src/app/og/midterms-2026/route/route.ts`:
- Around line 41-45: Add an AbortController-based timeout around the POST to
screenshotEndpoint: create an AbortController, set a timer (e.g. setTimeout)
that calls controller.abort() after a chosen ms, pass controller.signal into the
fetch options alongside method/headers/body, and clear the timer once the
response arrives; update the existing catch block that awaits the fetch (the
try/catch surrounding the fetch to screenshotEndpoint that assigns to r) to
detect an abort/timeout (check for error.name === "AbortError" or error.type ===
"aborted") and surface/log a distinct timeout error vs other fetch errors.
Ensure you reference the existing local variables screenshotEndpoint, headers,
payload and the response variable r when implementing the change.

---

Nitpick comments:
In `@front_end/src/app/`(main)/midterms-2026/components/congress_outcome_card.tsx:
- Around line 12-17: The OUTCOME_OPTION_LABEL mapping in
congress_outcome_card.tsx is hardcoded and must instead be defined alongside the
question's MC option constants in a shared module so the label keys stay in sync
with the authored question text; move the four option strings into the shared
question constant (exported), import and use that shared mapping in
OUTCOME_OPTION_LABEL or eliminate OUTCOME_OPTION_LABEL and reference the shared
strings directly when calling getMultipleChoiceOptionProbability, and add a
runtime check in the component (using getMultipleChoiceOptionProbability) to log
or assert (e.g., processLogger.warn or console.warn) if all four lookups return
null so authors are alerted when labels no longer match the question options.
🪄 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: CHILL

Plan: Pro

Run ID: 28248d20-ebb2-4a1e-ba86-6737319e6d36

📥 Commits

Reviewing files that changed from the base of the PR and between e4644f8 and d1f100b.

📒 Files selected for processing (24)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/src/app/(main)/labor-hub/components/activity_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx
  • front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx
  • front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx
  • front_end/src/app/(main)/midterms-2026/components/insight_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/map_legend.tsx
  • front_end/src/app/(main)/midterms-2026/components/map_tooltip_portal.tsx
  • front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx
  • front_end/src/app/(main)/midterms-2026/components/tile_map.tsx
  • front_end/src/app/(main)/midterms-2026/constants.ts
  • front_end/src/app/(main)/midterms-2026/helpers/state_color.ts
  • front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx
  • front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx
  • front_end/src/app/(main)/midterms-2026/sections/footer.tsx
  • front_end/src/app/(main)/midterms-2026/sections/hero.tsx
  • front_end/src/app/og/midterms-2026/route/route.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • front_end/src/app/(main)/midterms-2026/sections/hero.tsx
  • front_end/messages/en.json

Comment thread front_end/messages/pt.json Outdated
Comment thread front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx
Comment thread front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx Outdated
Comment thread front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx Outdated
Comment thread front_end/src/app/og/midterms-2026/route/route.ts
aseckin and others added 2 commits May 14, 2026 09:35
- og route: add 15s AbortController timeout; return generic 502 on
  upstream failure to avoid leaking screenshot service status codes;
  surface AbortError as 504.
- map tooltip portal: viewport-local placeBelow check (was mixing
  rect.top with window.scrollY); switch outside-click listener from
  click to mousedown to avoid racing the opener; expose onHoverChange
  so parents can keep the tooltip alive while it is hovered.
- geographic map: add keyboard accessibility to contested states
  (tabIndex/role/onKeyDown/onFocus/onBlur) and opt uncontested states
  out of the tab order; defer the path's onMouseLeave via
  requestAnimationFrame so a pointer transition into the tooltip portal
  no longer unmounts it before the tooltip's onClick can fire.
- locale files: replace the hardcoded forecast-outcome summary in
  midtermsHubCongressSummary with neutral copy across en/es/cs/pt/zh/
  zh-TW so the sentence does not go stale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@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

🧹 Nitpick comments (3)
front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx (2)

268-274: 💤 Low value

pressed style for uncontested states is unreachable.

Uncontested geographies receive { tabIndex: -1 } only (no onMouseDown/onClick/role), so react-simple-maps will never apply the pressed variant to them. The isContested ? … : UNCONTESTED_OPACITY_HOVER and strokeWidth: 1.5 branches here are effectively dead. Minor nit — feel free to simplify to the contested-only values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/components/geographic_map.tsx around
lines 268 - 274, The pressed style contains unreachable branches for uncontested
geographies (ternary for opacity and strokeWidth) because uncontested items are
not interactive; simplify the pressed variant to only the contested values by
removing the ternary expressions and UNCONTESTED_OPACITY_HOVER reference and
hardcoding the contested values (use isContested's contested styles: strokeWidth
2 and opacity 1) in the pressed style definition referenced by the pressed key
in the style object for the geography component (look for the pressed style,
isContested, and UNCONTESTED_OPACITY_HOVER in geographic_map.tsx).

142-181: 💤 Low value

Consider memoizing the per-geometry event handlers.

handleEnter, handleClick, and handleKeyDown are recreated on every render and are spread into ~50 <Geography> children via inline arrow wrappers (Lines 227-235). handleLeave and handleTooltipHoverChange are already useCallback-wrapped for the same reason — wrapping these three would keep the pattern consistent and avoid invalidating each path's listener identities on every render. Low priority since the list is small, but it's a cheap consistency win.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/components/geographic_map.tsx around
lines 142 - 181, The handlers handleEnter, handleClick, and handleKeyDown are
recreated each render and should be memoized with useCallback to avoid
invalidating event listeners on each <Geography> child; wrap handleEnter in
useCallback with setHovered (and any refs like tooltipHoveredRef if used) in its
dependency array, wrap handleClick in useCallback (no changing external state
aside from window navigation) and include any dependencies used, and wrap
handleKeyDown in useCallback and depend on the memoized handleClick; update any
places that reference these functions so they use the memoized versions.
front_end/src/app/og/midterms-2026/route/route.ts (1)

59-63: ⚡ Quick win

Consider validating the upstream response Content-Type.

The code assumes the screenshot service returns a PNG but doesn't verify the Content-Type header from the upstream response. If the service returns an error page or JSON, it will be served to clients as image/png, resulting in broken images.

♻️ Proposed validation
     const buf = await r.arrayBuffer();
+    const contentType = r.headers.get("content-type");
+    if (contentType && !contentType.startsWith("image/")) {
+      return NextResponse.json(
+        { error: "Upstream service error" },
+        { status: 502 }
+      );
+    }
     return new NextResponse(buf, {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/og/midterms-2026/route/route.ts` around lines 59 - 63, The
handler currently trusts the upstream response `r` and always returns `buf` as
`image/png`; validate `r.status` and `r.headers.get("content-type")` (e.g.,
ensure it startsWith("image/png") or matches an allowed image MIME) before
constructing the `NextResponse`; if the upstream status is non-200 or the
Content-Type is not an expected image, return an appropriate error
`NextResponse` (propagate the upstream status and Content-Type or use a safe
JSON/text error) instead of serving non-image payloads as `image/png`. Ensure
the checks are done where `buf` and `r` are used (the code around the `const buf
= await r.arrayBuffer();` and the `new NextResponse(...)` call).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front_end/src/app/`(main)/midterms-2026/components/geographic_map.tsx:
- Around line 220-237: Interactive geography paths expose role="button" and
tabIndex=0 but lack an accessible name and hint; update the interactiveProps
object to include an appropriate aria-label (e.g., derived from race.state or
race.question.title, falling back to abbr) and add aria-haspopup="true" (or
aria-haspopup="dialog" if applicable) so screen readers announce the state and
that activation opens a forecast, keeping existing handlers like handleEnter,
handleKeyDown, handleClick, and setHovered unchanged.

In `@front_end/src/app/og/midterms-2026/route/route.ts`:
- Line 8: The pageUrl construction uses the user-controlled variable theme
directly, allowing special characters to break or inject query params; update
the code that builds pageUrl (the const pageUrl declaration) to URL-encode theme
using encodeURIComponent before interpolation so the theme query value is safely
escaped (preserve PUBLIC_APP_URL and the non-interactive flag while replacing
theme with the encoded value).

---

Nitpick comments:
In `@front_end/src/app/`(main)/midterms-2026/components/geographic_map.tsx:
- Around line 268-274: The pressed style contains unreachable branches for
uncontested geographies (ternary for opacity and strokeWidth) because
uncontested items are not interactive; simplify the pressed variant to only the
contested values by removing the ternary expressions and
UNCONTESTED_OPACITY_HOVER reference and hardcoding the contested values (use
isContested's contested styles: strokeWidth 2 and opacity 1) in the pressed
style definition referenced by the pressed key in the style object for the
geography component (look for the pressed style, isContested, and
UNCONTESTED_OPACITY_HOVER in geographic_map.tsx).
- Around line 142-181: The handlers handleEnter, handleClick, and handleKeyDown
are recreated each render and should be memoized with useCallback to avoid
invalidating event listeners on each <Geography> child; wrap handleEnter in
useCallback with setHovered (and any refs like tooltipHoveredRef if used) in its
dependency array, wrap handleClick in useCallback (no changing external state
aside from window navigation) and include any dependencies used, and wrap
handleKeyDown in useCallback and depend on the memoized handleClick; update any
places that reference these functions so they use the memoized versions.

In `@front_end/src/app/og/midterms-2026/route/route.ts`:
- Around line 59-63: The handler currently trusts the upstream response `r` and
always returns `buf` as `image/png`; validate `r.status` and
`r.headers.get("content-type")` (e.g., ensure it startsWith("image/png") or
matches an allowed image MIME) before constructing the `NextResponse`; if the
upstream status is non-200 or the Content-Type is not an expected image, return
an appropriate error `NextResponse` (propagate the upstream status and
Content-Type or use a safe JSON/text error) instead of serving non-image
payloads as `image/png`. Ensure the checks are done where `buf` and `r` are used
(the code around the `const buf = await r.arrayBuffer();` and the `new
NextResponse(...)` call).
🪄 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: CHILL

Plan: Pro

Run ID: 8000d226-cfe6-4542-a611-684fd5c82b55

📥 Commits

Reviewing files that changed from the base of the PR and between d1f100b and 4cd9e95.

⛔ Files ignored due to path filters (1)
  • front_end/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/package.json
  • front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx
  • front_end/src/app/(main)/midterms-2026/components/map_tooltip_portal.tsx
  • front_end/src/app/og/midterms-2026/route/route.ts
✅ Files skipped from review due to trivial changes (4)
  • front_end/messages/zh.json
  • front_end/package.json
  • front_end/messages/cs.json
  • front_end/messages/zh-TW.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • front_end/src/app/(main)/midterms-2026/components/map_tooltip_portal.tsx
  • front_end/messages/en.json

Comment thread front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx Outdated
Comment thread front_end/src/app/og/midterms-2026/route/route.ts Outdated
aseckin and others added 9 commits May 14, 2026 11:00
- map: re-center projection and drop the right-side crop so the full
  US is visible now that more states will be contested.
- chamber tabs: remove the House tab (kept Senate active, Governor
  disabled).
- chamber control card: bump bars to h-5 (match Congress Outcome);
  reorder seat totals to Dems first ("Current: D 47 — R 53"); prefix
  the percentages with "Forecast:"; drop the inline "Dems need +N"
  text. The seats-needed line now lives in a hover tooltip below the
  row with a "Click to view forecasting question" disclaimer; the
  tooltip subject auto-picks the trailing party.
- congress outcome card: render the split-control rows (Rep Senate /
  Dem House and Dem Senate / Rep House) in neutral purple so they no
  longer read as either party.
- electoral consequences: add inline donkey + elephant SVG icons in
  the "IF DEM / IF REP CONGRESS" header cells so the fills tint to the
  party color via currentColor.
- cv bar: theme-aware opacity (bumped in dark mode for contrast);
  remove the 1px→2px border thickening on hover, keep the color shift.
- footer: replace the mock "Last updated <date> UTC" line with a
  static "Forecasts are updated real-time." copy; drop the now-unused
  getLatestUpdateTime helper and the LastUpdatedFull / DemsNeed i18n
  keys.
- i18n: add ChamberCurrent / ChamberForecast / ChamberTooltipBody /
  ChamberTooltipDisclaimer / PartyDemocrats / PartyRepublicans /
  ForecastsRealtime keys across en/es/cs/pt/zh/zh-TW.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- chamber control card: right-align the Current row so the seat totals
  line up with the Forecast: percentages above; unify the D/R divider
  to "/" in both rows at 50% opacity; fix the Dem+Rep bar overflow by
  switching from flex+gap (which exceeded the container by the gap
  width) to a 2-column grid with `${dem}fr ${rep}fr` template columns;
  bold the seats count and probability in the tooltip body via
  `t.rich` + `<b>` markup in each locale.
- chamber row tooltip: stateful client component now. On touch devices
  (matchMedia '(hover: none)'), the first tap on a row opens the
  tooltip and swallows the click so the wrapped link doesn't navigate;
  a small mobile-only close button (×) dismisses, and clicking the
  tooltip body opens the question. Tapping outside the row dismisses
  via a document mousedown listener. Desktop still uses pure CSS
  hover. The wrapper exposes `data-open` so cv_bar can react to the
  tap-open state in addition to hover.
- cv_bar: add `fill` prop so adjacent bars driven by a grid template
  can render at width:100% (used by Chamber Control). Bump hover
  opacities (0.7→0.85 light, 0.85→0.95 dark) so the active state
  reads more clearly. React to both `group/cr` hover and
  `group-data-[open]/cr` so the bars highlight when the tooltip is
  shown via touch tap as well as hover.
- geographic map: shift projection translate from [380, 270] to
  [400, 270] so the country sits slightly past dead-center, giving
  the chamber tabs overlay breathing room on the left.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- chamber control: center the "Current: D 47 / R 53" row (was
  right-aligned).
- cv_bar: make the active state unmistakable. Hover / tooltip-shown
  now drives the bar to full color (no opacity), darkens the border
  to the deep `borderColor` variant, and wraps it in a colored glow
  ring via box-shadow. Three triggers fire the same look:
  `group-hover/cv`, `group-hover/cr`, and `group-data-[open]/cr` so
  the highlight reads identically whether the row is hovered on
  desktop or tapped open on touch.
- electoral consequences: redesign the party header row to match the
  reference layout — two saturated colored cards (red for Republican
  Congress, navy blue-900 for Democratic Congress), centered party
  logo on top in white, bold title + small uppercase subtitle below.
  The "Question" column header above the rows is now empty, letting
  the row content speak for itself.
- i18n: add HeaderRepTitle / HeaderRepSubtitle / HeaderDemTitle /
  HeaderDemSubtitle keys across all six locales for the new card
  copy.
- chamber row tooltip: replace the SSR-unfriendly setState-in-effect
  touch detection with useSyncExternalStore around a (hover: none)
  matchMedia query (lint compliant; same behavior).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- electoral consequences: rewrite as a client component
  (ConsequenceGrid) that tracks a per-column hover state. Hovering
  the colored party header card or any bar cell in that column lights
  the entire column: the header card switches to a darker /
  more-saturated background, and the bars in that column receive the
  full active treatment (solid color fill, darkened border, glow
  ring) regardless of pointer position.
- party headers reformatted to a horizontal layout — logo on the
  left, two stacked lines on the right (bold title, small uppercase
  subtitle). Cards are noticeably shorter as a result.
- cv_bar: add an `active` prop that maps to `data-active` and a
  matching `data-[active]:` Tailwind variant, so external state (like
  the column-hover above) can force the active styling. Existing
  group-hover/cv, group-hover/cr, and group-data-[open]/cr triggers
  remain.
- constants: swap the split-control color from purple (#9B7AD6 /
  #6F4DB8) to pink (#E879A6 / #BE3F7E). Affects the RD and DR rows in
  the Congress Outcome card.
- consequence_row.tsx removed (its layout is now inlined inside the
  grid; the section file resolves the question copy and passes it
  through as a prop).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the pink fill on the RD and DR (Rep Senate / Dem House and
Dem Senate / Rep House) rows in the Congress Outcome card with a
transparent-fill bar wrapped in a multi-colored dashed border whose
dashes alternate between the dem and rep colors.

- cv_bar: add an `alternatingColors: [string, string]` prop. When
  set, the bar renders as inline SVG with two stacked rect outlines
  that share the same dash array but offset by one dash, producing a
  continuous alternating-color dashed border. Glow ring still fires
  on the same hover / data-active triggers, sourced from the first
  color in the pair.
- congress outcome: model outcomes as a discriminated union (`solid`
  vs `alternating`), so the bar variant is type-driven. RD/DR now
  flow through the alternating path.
- constants: drop the now-orphaned splitPrimary / splitBorder hex
  tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In dark mode the uncontested state fill was #2d3845 — visibly
lighter than the page body (bg-blue-50-dark = #22262b), which made
uncontested states stand out instead of receding. Drop the
uncontested fill to the page bg color so the tiles read as
"cutouts" of the SectionCard down to the page below. Hover variants
shift to the previous resting shades so hover feedback stays
visible. Light mode was already at #eff4f4 (= bg-blue-200); only
the dark tokens move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Revert the SVG dashed-border approach and use a CSS linear gradient
instead. Split-control rows (Rep Senate / Dem House and Dem Senate /
Rep House) now render as a single bar whose fill transitions from
the rep color on the left to the dem color on the right, with a
matching darker gradient border.

- cv_bar: replace `alternatingColors` with `gradientColors:
  [GradientColorStop, GradientColorStop]` where each stop carries
  its own fill + border hex. Implemented via the padding-box /
  border-box dual-gradient trick so a rounded border can hold its
  own color gradient. Two gradient values (rest + active) live on
  CSS variables; the active triggers (group-hover/cv,
  group-hover/cr, group-data-[open]/cr, data-[active]) swap which
  one the `background` shorthand reads, alongside the existing glow
  ring.
- congress outcome: rename the discriminated union variant from
  `alternating` to `gradient`; RD and DR rows pass the rep + dem
  color stops (rep first → left edge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The padding-box/border-box dual-gradient trick painted the full-
opacity border gradient behind the semi-transparent fill, so the
fill composited over the border gradient and read as a much darker
bar than its solid neighbors.

Switch to a layered approach: the bar element itself paints the
semi-transparent gradient fill directly (no underlying full-opacity
layer), and a child overlay paints the gradient border into a 1px
ring via a mask-composite trick — so the border lives only in the
border region and never bleeds into the fill area.

The Rep Senate / Dem House and Dem Senate / Rep House bars now read
at the same lightness as the solid red and blue bars on either side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@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)
front_end/src/app/(main)/midterms-2026/data.ts (1)

114-124: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

mailInBallots is configured but never surfaced downstream.

mailInBallots: 43527 is added to CHAMBER_QUESTIONS, so fetchChamberData (which iterates Object.values(CHAMBER_QUESTIONS)) now requests this post in the bulk API call, but the ChamberData type and the returned mapping in fetch_dashboard_data.ts (Lines 205-238) only expose the other five keys. As a result this id is fetched and then discarded, and no consumer can access it. Either wire it into ChamberData/fetchChamberData, or drop it from CHAMBER_QUESTIONS until a consumer exists.

🛠️ Wire it through (in fetch_dashboard_data.ts)
 export type ChamberData = {
   senateControl: PostWithForecasts | null;
   houseControl: PostWithForecasts | null;
   congressOutcome: PostWithForecasts | null;
   voterTurnout: PostWithForecasts | null;
   electionIntegrity: PostWithForecasts | null;
+  mailInBallots: PostWithForecasts | null;
 };

and add to both the empty-default object and the mapped return:

+    mailInBallots: byId.get(CHAMBER_QUESTIONS.mailInBallots) ?? null,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/data.ts around lines 114 - 124,
CHAMBER_QUESTIONS now includes mailInBallots (ID 43527) but fetchChamberData and
the ChamberData type in fetch_dashboard_data.ts currently omit that key so the
fetched post is discarded; update the ChamberData type to include mailInBallots,
add mailInBallots to the empty-default object used before mapping, and include
mailInBallots in the mapped return where other keys (senateControl,
houseControl, congressOutcome, voterTurnout, electionIntegrity) are assigned so
consumers can access the fetched value; reference symbols: CHAMBER_QUESTIONS,
mailInBallots, ChamberData, fetchChamberData in fetch_dashboard_data.ts.
♻️ Duplicate comments (1)
front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts (1)

225-229: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

fetchChamberData still throws on API failure while sibling fetchers degrade gracefully.

getPostsWithCP here is not wrapped in try/catch, unlike fetchSenateRaces, fetchGovernorRaces, and fetchSeatDistributions. Since consumers call these via Promise.all, a single chamber API hiccup fails the entire hub render instead of showing the map with empty chamber tiles. Wrap the call and fall back to the all-null ChamberData shape on error.

🛡️ Suggested guard
-  const { results } = await ServerPostsApi.getPostsWithCP({
-    ids,
-    limit: ids.length,
-  });
-  const byId = new Map(results.map((p) => [p.id, p]));
+  let results: PostWithForecasts[] = [];
+  try {
+    ({ results } = await ServerPostsApi.getPostsWithCP({
+      ids,
+      limit: ids.length,
+    }));
+  } catch {
+    results = [];
+  }
+  const byId = new Map(results.map((p) => [p.id, p]));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/helpers/fetch_dashboard_data.ts
around lines 225 - 229, fetchChamberData currently calls
ServerPostsApi.getPostsWithCP without error handling so a single API failure
throws and breaks the Promise.all; wrap the getPostsWithCP call in a try/catch
inside fetchChamberData, and on any error return the same fallback shape the
other fetchers use (an all-null ChamberData object) instead of throwing;
preserve the logic that builds byId (new Map(results.map((p) => [p.id, p])))
when the call succeeds, but return the fallback ChamberData when the call throws
or returns invalid results.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front_end/src/app/`(main)/midterms-2026/components/consequence_gauge.tsx:
- Around line 25-28: The large-arc-flag is computed from pct instead of the
actual sweep angle causing inverted arcs for pct between ~90 and ~90.9; modify
the logic in the progress calculation to compute the swept angle = (pct/100) *
ARC_ANGLE and set the large-arc-flag based on whether that angle > Math.PI
(180°) before calling describeArc (i.e., pass a flag derived from angle >
Math.PI into describeArc or have describeArc compute it from the angle),
referencing the existing pct, ARC_ANGLE, and describeArc symbols so the flag is
correct for all pct values.

---

Outside diff comments:
In `@front_end/src/app/`(main)/midterms-2026/data.ts:
- Around line 114-124: CHAMBER_QUESTIONS now includes mailInBallots (ID 43527)
but fetchChamberData and the ChamberData type in fetch_dashboard_data.ts
currently omit that key so the fetched post is discarded; update the ChamberData
type to include mailInBallots, add mailInBallots to the empty-default object
used before mapping, and include mailInBallots in the mapped return where other
keys (senateControl, houseControl, congressOutcome, voterTurnout,
electionIntegrity) are assigned so consumers can access the fetched value;
reference symbols: CHAMBER_QUESTIONS, mailInBallots, ChamberData,
fetchChamberData in fetch_dashboard_data.ts.

---

Duplicate comments:
In `@front_end/src/app/`(main)/midterms-2026/helpers/fetch_dashboard_data.ts:
- Around line 225-229: fetchChamberData currently calls
ServerPostsApi.getPostsWithCP without error handling so a single API failure
throws and breaks the Promise.all; wrap the getPostsWithCP call in a try/catch
inside fetchChamberData, and on any error return the same fallback shape the
other fetchers use (an all-null ChamberData object) instead of throwing;
preserve the logic that builds byId (new Map(results.map((p) => [p.id, p])))
when the call succeeds, but return the fallback ChamberData when the call throws
or returns invalid results.
🪄 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: CHILL

Plan: Pro

Run ID: 245c70af-7c85-4929-b1a2-3dbe1f924515

📥 Commits

Reviewing files that changed from the base of the PR and between 1e32e22 and f59a46d.

📒 Files selected for processing (23)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx
  • front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/consequence_gauge.tsx
  • front_end/src/app/(main)/midterms-2026/components/consequence_grid.tsx
  • front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx
  • front_end/src/app/(main)/midterms-2026/components/map_legend.tsx
  • front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx
  • front_end/src/app/(main)/midterms-2026/components/tile_map.tsx
  • front_end/src/app/(main)/midterms-2026/components/watch_card.tsx
  • front_end/src/app/(main)/midterms-2026/data.ts
  • front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts
  • front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts
  • front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx
  • front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx
  • front_end/src/app/(main)/midterms-2026/sections/seat_distributions.tsx
  • front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx
💤 Files with no reviewable changes (2)
  • front_end/src/app/(main)/midterms-2026/components/map_legend.tsx
  • front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
  • front_end/src/app/(main)/midterms-2026/sections/seat_distributions.tsx
  • front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx
  • front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx
  • front_end/src/app/(main)/midterms-2026/components/tile_map.tsx
  • front_end/messages/pt.json
  • front_end/src/app/(main)/midterms-2026/components/watch_card.tsx
  • front_end/messages/cs.json
  • front_end/messages/zh.json
  • front_end/messages/en.json

Copy link
Copy Markdown
Contributor

@ncarazon ncarazon left a comment

Choose a reason for hiding this comment

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

A few minor comments, otherwise LGTM

Comment thread front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx Outdated
Comment thread front_end/src/app/(main)/midterms-2026/components/consequence_gauge.tsx Outdated
- Chamber Control: derive both Senate & House marginals from the Congress
  Control question (#34484) by summing the two-chamber outcomes; drop the
  row links and the "Click to view" tooltip disclaimer.
- Electoral Consequences: rows are now clickable links to their question;
  add back the three numeric conditionals (NSF budget, Democracy Threat
  Index, Article III judges) rendering median + (25th-75th) per column.
- congress_outcome_card: use next/link <Link> instead of <a> (PR feedback).
- Key Drivers: swipeable stepped carousel (MobileCarousel) on mobile,
  3-column grid on desktop.
- Seat Distribution: center the "… Seat Advantage" labels under each half
  and add responsive bottom room so they clear the axis ticks on mobile.
- Extract describeArc to shared src/utils/charts/arc.ts with the large-arc
  flag derived from the swept angle (fixes inverted-arc edge case); use it
  in both consequence_gauge and binary_cp_bar (PR feedback / dedup).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@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

♻️ Duplicate comments (3)
front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts (1)

258-283: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

fetchChamberData throws on API failure while other fetchers degrade gracefully.

fetchSenateRaces, fetchGovernorRaces, and fetchSeatDistributions wrap their API calls in try/catch and return nulls on failure. fetchChamberData does not, so any ServerPostsApi.getPostsWithCP failure at line 270 propagates to consumers (e.g., via Promise.all), potentially failing the entire hub render instead of displaying partial data.

🛡️ Suggested guard
   }

+  let results: PostWithForecasts[] = [];
+  try {
-  const { results } = await ServerPostsApi.getPostsWithCP({
-    ids,
-    limit: ids.length,
-  });
+    ({ results } = await ServerPostsApi.getPostsWithCP({
+      ids,
+      limit: ids.length,
+    }));
+  } catch {
+    results = [];
+  }
   const byId = new Map(results.map((p) => [p.id, p]));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/helpers/fetch_dashboard_data.ts
around lines 258 - 283, fetchChamberData currently lets
ServerPostsApi.getPostsWithCP errors bubble up; wrap the API call (the await
ServerPostsApi.getPostsWithCP({...}) inside fetchChamberData) in a try/catch,
log or report the caught error, and on failure return the same graceful shape as
when ids is empty (i.e., an object with senateControl, houseControl,
congressOutcome, voterTurnout, electionIntegrity all set to null) so consumers
receive nulls like fetchSenateRaces/fetchGovernorRaces/fetchSeatDistributions
do; keep using CHAMBER_QUESTIONS and byId mapping logic in the success path.
front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx (2)

173-237: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Bar widths and displayed percentages disagree when "Other" options are non-trivial.

Lines 184–187 normalize demProb + repProb to 100% for bar geometry (demShare, repShare), but lines 189–190 compute demPct and repPct from the raw probabilities. When the underlying MC question has a non-zero "Other" slice (e.g., independent outcomes), the text might read "45% / 45%" while the bars render as 50% / 50%, which looks inconsistent.

Use the same normalized shares for both the bar widths and the displayed percentages so the text and visualization match.

🔧 Suggested fix
   const demShare =
     demProb != null && total && total > 0 ? (demProb / total) * 100 : null;
   const repShare = demShare != null ? 100 - demShare : null;

-  const demPct = demProb != null ? Math.round(demProb * 1000) / 10 : null;
-  const repPct = repProb != null ? Math.round(repProb * 1000) / 10 : null;
+  const demPct = demShare != null ? Math.round(demShare * 10) / 10 : null;
+  const repPct = repShare != null ? Math.round(repShare * 10) / 10 : null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/components/chamber_control_card.tsx
around lines 173 - 237, ChamberRow currently normalizes demShare/repShare for
the bar widths but computes demPct/repPct from raw probabilities, causing text
to disagree with the bars; fix by deriving the displayed percentages from the
normalized shares (demShare and repShare) instead of demProb/repProb, keeping
the same rounding/formatting and null checks (i.e., replace the demPct and
repPct calculations with computations based on demShare/repShare * 100 so the
text and CvBar geometry match).

30-31: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Hardcoded seat baselines will silently drift before Election Day 2026.

CURRENT_SENATE and CURRENT_HOUSE are baked into the component. These counts will change due to vacancies, party switches, and special elections between now and November 2026. When they drift, the "Current D N — R M" row and any derived labels (e.g., "Dems need +X") will be quietly incorrect with no alert to engineering.

Move these to a shared constant in data.ts (so updating one place fixes the entire hub) or, better, source them from a dedicated API/question so the dashboard stays current automatically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@front_end/src/app/`(main)/midterms-2026/components/chamber_control_card.tsx
around lines 30 - 31, CURRENT_SENATE and CURRENT_HOUSE are hardcoded in
chamber_control_card.tsx and will drift; remove these inline constants and
instead import shared baseline seat counts from a central source (e.g., export
constants from data.ts) or replace them with values fetched from a small
service/API; update chamber_control_card.tsx to reference the new exported
symbols (e.g., DEFAULT_SENATE_BASELINE, DEFAULT_HOUSE_BASELINE) or to call the
fetch function (e.g., getCurrentSeatCounts) and use the returned object for all
computations and labels so the "Current D N — R M" row and derived messages
remain accurate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front_end/src/app/`(main)/midterms-2026/helpers/fetch_dashboard_data.ts:
- Around line 186-207: The code passes an empty array for the range when
interval bounds are incomplete, causing getPredictionDisplayValue to treat
range[0]/[1] as undefined and return the emptyLabel; change the range argument
in the call to getPredictionDisplayValue inside the numeric branch so it passes
undefined when lo or hi are missing (i.e., use range: lo != null && hi != null ?
[lo, hi] : undefined) so getPredictionDisplayValue will render the formatted
median only; update the call site around the median/display handling where
getPredictionDisplayValue is invoked and keep the subsequent split logic the
same.

---

Duplicate comments:
In `@front_end/src/app/`(main)/midterms-2026/components/chamber_control_card.tsx:
- Around line 173-237: ChamberRow currently normalizes demShare/repShare for the
bar widths but computes demPct/repPct from raw probabilities, causing text to
disagree with the bars; fix by deriving the displayed percentages from the
normalized shares (demShare and repShare) instead of demProb/repProb, keeping
the same rounding/formatting and null checks (i.e., replace the demPct and
repPct calculations with computations based on demShare/repShare * 100 so the
text and CvBar geometry match).
- Around line 30-31: CURRENT_SENATE and CURRENT_HOUSE are hardcoded in
chamber_control_card.tsx and will drift; remove these inline constants and
instead import shared baseline seat counts from a central source (e.g., export
constants from data.ts) or replace them with values fetched from a small
service/API; update chamber_control_card.tsx to reference the new exported
symbols (e.g., DEFAULT_SENATE_BASELINE, DEFAULT_HOUSE_BASELINE) or to call the
fetch function (e.g., getCurrentSeatCounts) and use the returned object for all
computations and labels so the "Current D N — R M" row and derived messages
remain accurate.

In `@front_end/src/app/`(main)/midterms-2026/helpers/fetch_dashboard_data.ts:
- Around line 258-283: fetchChamberData currently lets
ServerPostsApi.getPostsWithCP errors bubble up; wrap the API call (the await
ServerPostsApi.getPostsWithCP({...}) inside fetchChamberData) in a try/catch,
log or report the caught error, and on failure return the same graceful shape as
when ids is empty (i.e., an object with senateControl, houseControl,
congressOutcome, voterTurnout, electionIntegrity all set to null) so consumers
receive nulls like fetchSenateRaces/fetchGovernorRaces/fetchSeatDistributions
do; keep using CHAMBER_QUESTIONS and byId mapping logic in the success path.
🪄 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: CHILL

Plan: Pro

Run ID: 699a54e0-4934-4f30-a8ba-3834bc5045f7

📥 Commits

Reviewing files that changed from the base of the PR and between f59a46d and 3aefc39.

📒 Files selected for processing (12)
  • front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx
  • front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/consequence_gauge.tsx
  • front_end/src/app/(main)/midterms-2026/components/consequence_grid.tsx
  • front_end/src/app/(main)/midterms-2026/components/seat_distribution_chart.tsx
  • front_end/src/app/(main)/midterms-2026/data.ts
  • front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts
  • front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx
  • front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx
  • front_end/src/components/consumer_post_card/binary_cp_bar.tsx
  • front_end/src/utils/charts/arc.ts
✅ Files skipped from review due to trivial changes (1)
  • front_end/src/app/(main)/midterms-2026/data.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx
  • front_end/src/app/(main)/midterms-2026/components/consequence_gauge.tsx
  • front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/seat_distribution_chart.tsx
  • front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx

Comment thread front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts Outdated
aseckin and others added 11 commits June 3, 2026 16:38
- Drop the numeric/discrete conditionals (NSF budget, Threat Index, Article
  III judges) from the section and revert the fetch + grid to binary-only
  probability gauges. Rows now keep rendering when a branch's CP is not yet
  revealed (shown as "—") instead of being filtered out.
- Underline the question title on row hover to signal the row links to its
  question.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Halve the extra mobile bottom padding added for the party-advantage labels
  (76 -> 66) so the gap above them isn't excessive.
- Render the SENATE/HOUSE chart title inside the SVG as a VictoryLabel
  instead of an HTML overlay, so the hover tooltip paints above it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iew)

The 3-column gauges rendered blank on the deployed dashboard even though
the underlying questions were public. The section fetched each conditional
group through the single-post getPost endpoint (five parallel calls);
switch to the list endpoint getPostsWithCP — the same one Chamber Control
and Seat Distribution use and which reliably returns the group
subquestions' community prediction — collapsing the five requests into one
(also easing the SSR fetch fan-out).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…over

- Electoral Consequences gauges rendered blank because the prod data renamed
  the conditional subquestion labels (Democratic/Mixed -> Democratic
  Congress/Split Congress). Match the three scenarios by substring
  (democrat / split|mixed / republican) so wording changes don't blank them.
- Seat Distribution: remove the leftover HTML title span that duplicated the
  in-SVG VictoryLabel (two overlapping SENATE/HOUSE titles).
- Electoral Consequences: fade the row hover highlight in from the left edge
  (leftmost 10% of the question cell) and soften the hover underline color
  (blue-400/50).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tuning

- Mobile map: tapped state tile gets a thick contrast outline
  (blue-900 light / blue-100 dark) until its tooltip is dismissed.
- Mobile Chamber Control: fix the tooltip "X" not dismissing (sticky touch
  :hover kept the hover fallback visible) by gating it to non-touch.
- Seat Distribution tooltip: tighter vertical padding + a bit more line gap
  (flyoutPadding 10, lineHeight 1.3).
- Electoral Consequences: richer hovered-cell highlight, and the gauge in
  the hovered cell shifts darker (light) / lighter (dark) via a new
  shadeHex helper; darker row/column lines in light mode and dimmer in
  dark mode; hover underline -> blue-600 (light) / blue-400 (dark).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Key Drivers (Things to Watch):
- Voter-turnout slot -> #43609 "national emergency over election integrity"
  (reuses the existing, already-translated midtermsHubElectionEmergency copy).
- Court-changes-winner slot -> #43490 "Missouri Amendment 3 abortion-rights
  repeal" with new copy across 6 locales.
- Rename CHAMBER_QUESTIONS keys + ChamberData fields (electionEmergency /
  abortionAmendment); drop 4 now-unused i18n keys.

Copy:
- Seat Distributions subtitle reworded to "...how large an advantage either
  party could end up with..." (6 locales).

Storefront:
- Add "US Midterms" with US flag as the first Staff Pick.

PR review:
- fetchChamberData wraps the API call in try/catch and returns a graceful
  null shape (EMPTY_CHAMBER_DATA), matching fetchSeatDistributions.
- ChamberRow displays the same normalized shares the bars use, so text and
  bar geometry stay locked together.
- Move CURRENT_SENATE / CURRENT_HOUSE baselines into data.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the editorializing tail after the em-dash on both new Things-to-Watch
context blurbs (national emergency + Missouri abortion amendment), across all
6 locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Seat Distributions subtitle -> "...the final seat advantage in each chamber."
- Drop the Key Drivers section subtitle ("Indicators worth watching...") and
  its now-unused i18n key; widen the header bottom margin.
- Electoral Consequences headers: drop the donkey+elephant icons from the
  Split column and center its text; reword Dem/Rep subtitles to "If Dems
  control" / "If GOP controls" (no "majority").
- Community Insights: move the single-card mobile carousel breakpoint from lg
  to xs (480px) so the multi-card carousel handles tablet widths.
- Map state tooltip: style the "Click to view question" line as gray-700 with
  a gray-400 offset underline; drop "full" from the copy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@front_end/src/utils/core/colors.ts`:
- Around line 72-83: In shadeHex, sanitize and clamp the incoming amount before
using it in per-channel math: validate amount is finite, coerce non-finite to a
safe default (e.g., 0), and clamp it to the expected range (e.g., -1 to 1) so
NaN/Infinity can't propagate into shade or rgbToHex; modify shadeHex to compute
a sanitizedAmount from the input and use that in the shade callback (referencing
shadeHex, parseHexColor, and rgbToHex) and return the original hex or a safe
fallback if parseHexColor fails.
🪄 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: CHILL

Plan: Pro

Run ID: bbe953e2-0718-4372-99a7-c5ecaf37b7e5

📥 Commits

Reviewing files that changed from the base of the PR and between f59a46d and 76294ce.

📒 Files selected for processing (24)
  • front_end/messages/cs.json
  • front_end/messages/en.json
  • front_end/messages/es.json
  • front_end/messages/pt.json
  • front_end/messages/zh-TW.json
  • front_end/messages/zh.json
  • front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx
  • front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx
  • front_end/src/app/(main)/midterms-2026/components/consequence_gauge.tsx
  • front_end/src/app/(main)/midterms-2026/components/consequence_grid.tsx
  • front_end/src/app/(main)/midterms-2026/components/seat_distribution_chart.tsx
  • front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx
  • front_end/src/app/(main)/midterms-2026/components/tile_map.tsx
  • front_end/src/app/(main)/midterms-2026/data.ts
  • front_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.ts
  • front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts
  • front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx
  • front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx
  • front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx
  • front_end/src/app/(storefront)/page.tsx
  • front_end/src/components/consumer_post_card/binary_cp_bar.tsx
  • front_end/src/utils/charts/arc.ts
  • front_end/src/utils/core/colors.ts
✅ Files skipped from review due to trivial changes (3)
  • front_end/src/app/(storefront)/page.tsx
  • front_end/messages/cs.json
  • front_end/messages/en.json
🚧 Files skipped from review as they are similar to previous changes (14)
  • front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx
  • front_end/src/utils/charts/arc.ts
  • front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx
  • front_end/src/app/(main)/midterms-2026/components/consequence_gauge.tsx
  • front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx
  • front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx
  • front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx
  • front_end/src/app/(main)/midterms-2026/data.ts
  • front_end/messages/pt.json
  • front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx
  • front_end/src/app/(main)/midterms-2026/components/seat_distribution_chart.tsx
  • front_end/src/components/consumer_post_card/binary_cp_bar.tsx
  • front_end/src/app/(main)/midterms-2026/components/tile_map.tsx
  • front_end/messages/zh.json

Comment thread front_end/src/utils/core/colors.ts
Coerce non-finite input to 0 and clamp to [-1, 1] before the per-channel math,
so a NaN amount can't propagate into a malformed "#NaNNaNNaN" color. Defensive
hardening of the shared util (current callers only pass +/-0.3); enforces the
contract already stated in the doc comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aseckin aseckin merged commit a199851 into main Jun 4, 2026
15 checks passed
@aseckin aseckin deleted the 2026-midterms branch June 4, 2026 18:13
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.

4 participants