Skip to content

2026 US Midterms Dashboard#4693

Draft
aseckin wants to merge 4 commits intomainfrom
2026-midterms
Draft

2026 US Midterms Dashboard#4693
aseckin wants to merge 4 commits intomainfrom
2026-midterms

Conversation

@aseckin
Copy link
Copy Markdown
Contributor

@aseckin aseckin commented May 5, 2026

Summary by CodeRabbit

  • New Features

    • Introduced a 2026 US Midterms Hub featuring interactive maps, chamber control forecasts, electoral outcome probabilities, and voter turnout data.
    • Added community insights carousel highlighting top discussions and predictions.
    • Implemented state-by-state Senate race visualizations with real-time forecast updates.
    • Added electoral consequences analysis covering policy impacts across climate, minimum wage, immigration, and government shutdown scenarios.
  • Internationalization

    • Expanded translation support for the Midterms Hub across Czech, English, Spanish, Portuguese, Simplified Chinese, and Traditional Chinese.

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

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7110842-8e3b-47b8-82cf-6fed6cf1f1d6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 2026-midterms

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.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4693-2026-midterms-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:2026-midterms-d1f100b
🗄️ PostgreSQL NeonDB branch preview/pr-4693-2026-midterms
Redis Fly Redis mtc-redis-pr-4693-2026-midterms

Details

  • Commit: 3fdab034d6ebfd482bbcf8b8ead196c2922a8f4b
  • Branch: 2026-midterms
  • Fly App: metaculus-pr-4693-2026-midterms

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

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
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

"midtermsHubOutcomeRepDem": "Senado Rep / Câmara Dem",
"midtermsHubOutcomeDemRep": "Senado Dem / Câmara Rep",
"midtermsHubOutcomeDemDem": "Senado Dem / Câmara Dem",
"midtermsHubCongressSummary": "Os previsores esperam um Congresso dividido, com os republicanos provavelmente mantendo o Senado e os democratas favoritos para recuperar a Câmara.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid hardcoded forecast outcome text in a real-time dashboard

Line 2200 hardcodes a specific political outcome (“Congresso dividido… republicanos… democratas…”). This will go stale as forecast data changes and can contradict the live charts.

💡 Suggested fix (neutral, non-stale copy)
-  "midtermsHubCongressSummary": "Os previsores esperam um Congresso dividido, com os republicanos provavelmente mantendo o Senado e os democratas favoritos para recuperar a Câmara.",
+  "midtermsHubCongressSummary": "As previsões da comunidade sobre o controle do Congresso são atualizadas em tempo real à medida que o ciclo eleitoral evolui.",
🤖 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/messages/pt.json` at 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.

Comment on lines +47 to +52
const style: CSSProperties = {
width: `${Math.max(pct, 1)}%`,
borderColor: resolvedBorder,
backgroundColor: addOpacityToHex(color, BG_OPACITY_DEFAULT),
["--cv-bar-hover-bg" as string]: addOpacityToHex(color, BG_OPACITY_HOVER),
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Math.max(pct, 1) misrepresents zero/null probabilities at the bar primitive.

Every consumer (e.g. congress_outcome_card.tsx, where the past review flagged the same problem and pushed the fix down here, and chamber_control_card.tsx) renders a visible 1% sliver for outcomes that are actually 0% or unknown. Push the floor decision out to callers and let pct === 0 render as a 0-width bar.

🩹 Suggested fix
   const style: CSSProperties = {
-    width: `${Math.max(pct, 1)}%`,
+    width: `${Math.max(pct, 0)}%`,
     borderColor: resolvedBorder,
     backgroundColor: addOpacityToHex(color, BG_OPACITY_DEFAULT),
     ["--cv-bar-hover-bg" as string]: addOpacityToHex(color, BG_OPACITY_HOVER),
   };

Callers that genuinely want a minimum-visible track (e.g. for legibility on tiny shares) can opt in explicitly via their own Math.max(pct, MIN_VISIBLE) or by skipping render entirely when pct === 0.

🤖 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/cv_bar.tsx around lines 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.

Comment on lines +184 to +188
onMouseEnter={(e) =>
isContested && abbr && handleEnter(abbr, e)
}
onMouseLeave={() => setHovered(null)}
onClick={() => isContested && handleClick(race)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make contested states keyboard reachable.

These map regions behave like links, but they only respond to pointer hover/click right now. Keyboard users cannot focus a contested state or open its question from the map, which makes this interaction inaccessible.

Also applies to: 195-210

🤖 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 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.

onMouseEnter={(e) =>
isContested && abbr && handleEnter(abbr, e)
}
onMouseLeave={() => setHovered(null)}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The tooltip is dismissed before its own handlers can run.

Line 187 clears hovered as soon as the pointer leaves the SVG path, so the tooltip portal at Lines 229-235 is unmounted before users can click or dismiss it. If the tooltip is meant to be interactive, keep the hover state alive while the pointer is over the tooltip; otherwise its onClick/onDismiss props are effectively dead.

Also applies to: 229-235

🤖 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 at 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.

Comment on lines +55 to +67
useLayoutEffect(() => {
const node = tooltipRef.current;
if (!node) return;
const rect = node.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const overflowRight = rect.right - (viewportWidth - VIEWPORT_PADDING);
const overflowLeft = VIEWPORT_PADDING - rect.left;
let leftOffset = 0;
if (overflowRight > 0) leftOffset = -overflowRight;
if (overflowLeft > 0) leftOffset = overflowLeft;
const placeBelow = rect.top - VIEWPORT_PADDING < window.scrollY;
setAdjustment({ leftOffset, placeBelow });
}, [x, y, mounted]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

placeBelow mixes viewport-relative rect.top with window.scrollY.

getBoundingClientRect().top is viewport-relative, but window.scrollY measures document scroll. The current condition rect.top - VIEWPORT_PADDING < window.scrollY becomes true for almost any state once the page is scrolled (e.g. rect.top = 500, scrollY = 1000 → flips below), so tooltips will jump below their anchor even when the entire viewport above the anchor is empty.

The intended "clipped at viewport top" check is purely viewport-local.

🩹 Suggested fix
-    const placeBelow = rect.top - VIEWPORT_PADDING < window.scrollY;
+    const placeBelow = rect.top < VIEWPORT_PADDING;
     setAdjustment({ leftOffset, placeBelow });
🤖 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/map_tooltip_portal.tsx
around lines 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.

Comment on lines +69 to +80
useEffect(() => {
if (!onDismiss) return;
const handle = (e: globalThis.MouseEvent) => {
const target = e.target as Node | null;
if (!target) return;
if (insideRef?.current?.contains(target)) return;
if (tooltipRef.current?.contains(target)) return;
onDismiss();
};
document.addEventListener("click", handle);
return () => document.removeEventListener("click", handle);
}, [onDismiss, insideRef]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether other click-outside handlers in the repo standardize on mousedown.
rg -nP --type=ts --type=tsx -C2 "addEventListener\(\s*['\"](click|mousedown|pointerdown)['\"]"

Repository: Metaculus/metaculus

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for addEventListener with click/mousedown/pointerdown in TypeScript and TSX files
# Use glob patterns instead of file types
rg -nP "addEventListener\(\s*['\"](?:click|mousedown|pointerdown)['\"]" --glob="*.ts" --glob="*.tsx" -C2

Repository: Metaculus/metaculus

Length of output: 3855


🏁 Script executed:

#!/bin/bash
# Read the map_tooltip_portal.tsx file to see the full context
cat -n front_end/src/app/\(main\)/midterms-2026/components/map_tooltip_portal.tsx | head -100

Repository: Metaculus/metaculus

Length of output: 3806


🏁 Script executed:

#!/bin/bash
# Search for guard patterns like skip, ignore, first, or similar in click-outside handlers
rg -nP "(skip|ignore|first|once)" --glob="*.ts" --glob="*.tsx" -C2 -B2 | grep -A5 -B5 "(click|dismiss|outside)" | head -50

Repository: Metaculus/metaculus

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Look at the gradient-carousel.tsx implementation to see how it handles the similar issue
sed -n '140,160p' front_end/src/components/gradient-carousel.tsx

Repository: Metaculus/metaculus

Length of output: 786


Switch from click to mousedown for the click-outside listener.

The click event can fire on the same event cycle as the onClick handler that opens the tooltip, causing the listener to immediately dismiss it. Other click-outside handlers in the codebase (e.g., mobile_menu.tsx, storefront_navbar.tsx, use_vote_panel.ts) standardize on mousedown, which fires before React commits and prevents this race condition.

🤖 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/map_tooltip_portal.tsx
around lines 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.

Comment on lines +18 to +24
<div className="hidden h-full lg:block">
<GeographicMap races={races} tabsSlot={<ChamberTabs />} />
</div>
<div className="flex h-full items-center p-5 lg:hidden">
<div className="w-full">
<TileMap races={races} />
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the chamber tabs visible on the mobile map too.

The lg:hidden branch drops ChamberTabs, so small-screen users lose the cue that this view is Senate-only and that House/Governor are coming soon. Reusing the same tabs above TileMap would keep the experience consistent across breakpoints.

🤖 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/responsive_map.tsx around
lines 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.

Comment on lines +47 to +50
if (!r.ok) {
const text = await r.text();
return NextResponse.json({ error: text }, { status: r.status });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

r.status from the screenshot service is forwarded verbatim to the client.

This is a minor info-leak: a 401 or 403 from the screenshot backend reveals that an API key is in use and is invalid/rejected. Consider normalizing upstream 4xx/5xx to a generic 502 for external callers.

🛡️ Proposed fix
-      return NextResponse.json({ error: text }, { status: r.status });
+      return NextResponse.json({ error: "screenshot service error" }, { status: 502 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!r.ok) {
const text = await r.text();
return NextResponse.json({ error: text }, { status: r.status });
}
if (!r.ok) {
const text = await r.text();
return NextResponse.json({ error: "screenshot service error" }, { status: 502 });
}
🤖 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 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.

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.

1 participant