2026 US Midterms Dashboard#4693
Conversation
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>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🚀 Preview EnvironmentYour preview environment is ready!
Details
ℹ️ Preview Environment InfoIsolation:
Limitations:
Cleanup:
|
There was a problem hiding this comment.
Actionable comments posted: 18
🧹 Nitpick comments (5)
front_end/src/app/og/midterms-2026/page.tsx (1)
27-41: ⚡ Quick winMove 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 winUse a container ref instead of
closest(".tile-map-container").
handleEnterresolves the positioning origin viae.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. AuseRefon 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 winReplace custom regex markdown stripper with existing
strip-markdownutility.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 (), 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-markdowninfront_end/src/utils/markdown.tsvia the remark parser. Consider either:
- Importing
strip-markdowndirectly for simple 320-character truncation- Creating a simple wrapper function in
utils/markdown.tsto standardize markdown stripping across the appThis 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: NoSuspenseboundaries — 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
projectiontype is incomplete — forces an unsafe double-cast ingeographic_map.tsx.The current union
string | ((opts: { width: number; height: number }) => unknown)omits the D3GeoProjectionobject variant that react-simple-maps v3 accepts directly. This is whygeographic_map.tsx(line 139) needsprojection as unknown as string— a cast that fully disables type-checking for that prop, even thoughprojectionis created fromgeoAlbersUsa().scale(...).translate(...).Since
d3is already a project dependency,GeoProjectionis 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.tsxcan 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
⛔ Files ignored due to path filters (1)
front_end/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (39)
front_end/declarations/react-simple-maps.d.tsfront_end/global.d.tsfront_end/messages/cs.jsonfront_end/messages/en.jsonfront_end/messages/es.jsonfront_end/messages/pt.jsonfront_end/messages/zh-TW.jsonfront_end/messages/zh.jsonfront_end/package.jsonfront_end/public/us-states-10m.jsonfront_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsxfront_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsxfront_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsxfront_end/src/app/(main)/midterms-2026/components/consequence_row.tsxfront_end/src/app/(main)/midterms-2026/components/consumer_tile_client.tsxfront_end/src/app/(main)/midterms-2026/components/geographic_map.tsxfront_end/src/app/(main)/midterms-2026/components/insight_card.tsxfront_end/src/app/(main)/midterms-2026/components/insights_carousel.tsxfront_end/src/app/(main)/midterms-2026/components/live_badge.tsxfront_end/src/app/(main)/midterms-2026/components/map_legend.tsxfront_end/src/app/(main)/midterms-2026/components/responsive_map.tsxfront_end/src/app/(main)/midterms-2026/components/state_tooltip.tsxfront_end/src/app/(main)/midterms-2026/components/tile_map.tsxfront_end/src/app/(main)/midterms-2026/components/watch_card.tsxfront_end/src/app/(main)/midterms-2026/constants.tsfront_end/src/app/(main)/midterms-2026/data.tsfront_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.tsfront_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.tsfront_end/src/app/(main)/midterms-2026/helpers/post_utils.tsfront_end/src/app/(main)/midterms-2026/helpers/state_color.tsfront_end/src/app/(main)/midterms-2026/page.tsxfront_end/src/app/(main)/midterms-2026/sections/community_insights.tsxfront_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsxfront_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsxfront_end/src/app/(main)/midterms-2026/sections/footer.tsxfront_end/src/app/(main)/midterms-2026/sections/hero.tsxfront_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsxfront_end/src/app/og/midterms-2026/page.tsxfront_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>
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (1)
front_end/src/app/og/midterms-2026/route/route.ts (1)
41-45:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMissing request timeout — screenshot call can hang indefinitely.
This
fetchstill has noAbortController/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 winHardcoded 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.),getMultipleChoiceOptionProbabilityreturnsnullfor 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
📒 Files selected for processing (24)
front_end/messages/cs.jsonfront_end/messages/en.jsonfront_end/messages/es.jsonfront_end/messages/pt.jsonfront_end/messages/zh-TW.jsonfront_end/messages/zh.jsonfront_end/src/app/(main)/labor-hub/components/activity_card.tsxfront_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsxfront_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsxfront_end/src/app/(main)/midterms-2026/components/consequence_row.tsxfront_end/src/app/(main)/midterms-2026/components/cv_bar.tsxfront_end/src/app/(main)/midterms-2026/components/geographic_map.tsxfront_end/src/app/(main)/midterms-2026/components/insight_card.tsxfront_end/src/app/(main)/midterms-2026/components/map_legend.tsxfront_end/src/app/(main)/midterms-2026/components/map_tooltip_portal.tsxfront_end/src/app/(main)/midterms-2026/components/responsive_map.tsxfront_end/src/app/(main)/midterms-2026/components/tile_map.tsxfront_end/src/app/(main)/midterms-2026/constants.tsfront_end/src/app/(main)/midterms-2026/helpers/state_color.tsfront_end/src/app/(main)/midterms-2026/sections/community_insights.tsxfront_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsxfront_end/src/app/(main)/midterms-2026/sections/footer.tsxfront_end/src/app/(main)/midterms-2026/sections/hero.tsxfront_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
- 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>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx (2)
268-274: 💤 Low value
pressedstyle for uncontested states is unreachable.Uncontested geographies receive
{ tabIndex: -1 }only (noonMouseDown/onClick/role), so react-simple-maps will never apply thepressedvariant to them. TheisContested ? … : UNCONTESTED_OPACITY_HOVERandstrokeWidth: 1.5branches 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 valueConsider memoizing the per-geometry event handlers.
handleEnter,handleClick, andhandleKeyDownare recreated on every render and are spread into ~50<Geography>children via inline arrow wrappers (Lines 227-235).handleLeaveandhandleTooltipHoverChangeare alreadyuseCallback-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 winConsider validating the upstream response Content-Type.
The code assumes the screenshot service returns a PNG but doesn't verify the
Content-Typeheader from the upstream response. If the service returns an error page or JSON, it will be served to clients asimage/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
⛔ Files ignored due to path filters (1)
front_end/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
front_end/messages/cs.jsonfront_end/messages/en.jsonfront_end/messages/es.jsonfront_end/messages/pt.jsonfront_end/messages/zh-TW.jsonfront_end/messages/zh.jsonfront_end/package.jsonfront_end/src/app/(main)/midterms-2026/components/geographic_map.tsxfront_end/src/app/(main)/midterms-2026/components/map_tooltip_portal.tsxfront_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
- 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>
This reverts commit b6e6095.
- geographic_map: contested state paths previously exposed role="button" and tabIndex=0 with no accessible name. Add an aria-label derived from STATE_NAMES[abbr] (falling back to the abbreviation) so screen readers announce the state, and aria-haspopup="dialog" so users know activation opens the forecast question rather than mutating the current view. - og route: encodeURIComponent the user-controlled `theme` query param when interpolating it back into the screenshot pageUrl, so special characters can't break out of the query slot or inject additional params. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
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/app/`(main)/midterms-2026/components/chamber_row_tooltip.tsx:
- Around line 107-110: The Close button's aria-label is hardcoded as "Close";
update the component in chamber_row_tooltip (the button inside
ChamberRowTooltip) to use the i18n hook: import and call useTranslations()
(e.g., const t = useTranslations()) and replace aria-label="Close" with
aria-label={t('close') } (or your project's chosen key such as
'buttons.close'/'tooltip.close') so the accessibility label is localized; ensure
the translation key exists in the locale files.
🪄 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: 5bdf9799-4a58-4cde-b4fc-b60ecad54e8c
⛔ Files ignored due to path filters (2)
front_end/public/images/party-donkey.svgis excluded by!**/*.svgfront_end/public/images/party-elephant.svgis excluded by!**/*.svg
📒 Files selected for processing (18)
front_end/messages/cs.jsonfront_end/messages/en.jsonfront_end/messages/es.jsonfront_end/messages/pt.jsonfront_end/messages/zh-TW.jsonfront_end/messages/zh.jsonfront_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsxfront_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsxfront_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsxfront_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsxfront_end/src/app/(main)/midterms-2026/components/consequence_grid.tsxfront_end/src/app/(main)/midterms-2026/components/cv_bar.tsxfront_end/src/app/(main)/midterms-2026/components/geographic_map.tsxfront_end/src/app/(main)/midterms-2026/components/party_icons.tsxfront_end/src/app/(main)/midterms-2026/helpers/post_utils.tsfront_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsxfront_end/src/app/(main)/midterms-2026/sections/footer.tsxfront_end/src/app/og/midterms-2026/route/route.ts
✅ Files skipped from review due to trivial changes (2)
- front_end/src/app/(main)/midterms-2026/components/party_icons.tsx
- front_end/messages/zh-TW.json
🚧 Files skipped from review as they are similar to previous changes (6)
- front_end/messages/cs.json
- front_end/messages/zh.json
- front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx
- front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx
- front_end/src/app/og/midterms-2026/route/route.ts
- front_end/messages/pt.json
| <button | ||
| type="button" | ||
| aria-label="Close" | ||
| onClick={(e) => { |
There was a problem hiding this comment.
Localize the close button aria-label instead of hardcoding English.
aria-label="Close" should come from useTranslations() to keep accessibility text localized consistently.
Based on learnings: Do not hardcode English strings in TSX components; prefer useTranslations() and i18n strings for UI text.
🤖 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_row_tooltip.tsx
around lines 107 - 110, The Close button's aria-label is hardcoded as "Close";
update the component in chamber_row_tooltip (the button inside
ChamberRowTooltip) to use the i18n hook: import and call useTranslations()
(e.g., const t = useTranslations()) and replace aria-label="Close" with
aria-label={t('close') } (or your project's chosen key such as
'buttons.close'/'tooltip.close') so the accessibility label is localized; ensure
the translation key exists in the locale files.
Summary by CodeRabbit
Release Notes
New Features
Localization
Dependencies