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
| "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.", |
There was a problem hiding this comment.
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.
| 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), | ||
| }; |
There was a problem hiding this comment.
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.
| onMouseEnter={(e) => | ||
| isContested && abbr && handleEnter(abbr, e) | ||
| } | ||
| onMouseLeave={() => setHovered(null)} | ||
| onClick={() => isContested && handleClick(race)} |
There was a problem hiding this comment.
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)} |
There was a problem hiding this comment.
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.
| 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]); |
There was a problem hiding this comment.
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.
| 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]); |
There was a problem hiding this comment.
🧩 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" -C2Repository: 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 -100Repository: 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 -50Repository: 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.tsxRepository: 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.
| <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> |
There was a problem hiding this comment.
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.
| if (!r.ok) { | ||
| const text = await r.text(); | ||
| return NextResponse.json({ error: text }, { status: r.status }); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
Summary by CodeRabbit
New Features
Internationalization