From 81929f38f4cbd1412c4c1b402d2d9470d14bb73e Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Tue, 5 May 2026 16:35:45 +0200 Subject: [PATCH 01/18] feat: scaffold 2026 US Midterms dashboard infrastructure 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 --- front_end/messages/cs.json | 60 +++++++- front_end/messages/en.json | 60 +++++++- front_end/messages/es.json | 60 +++++++- front_end/messages/pt.json | 60 +++++++- front_end/messages/zh-TW.json | 60 +++++++- front_end/messages/zh.json | 60 +++++++- .../components/chamber_control_card.tsx | 119 +++++++++++++++ .../midterms-2026/components/chamber_tabs.tsx | 33 ++++ .../components/congress_outcome_card.tsx | 126 ++++++++++++++++ .../components/consequence_row.tsx | 70 +++++++++ .../components/distribution_curve.tsx | 64 ++++++++ .../components/geographic_map.tsx | 110 ++++++++++++++ .../midterms-2026/components/insight_card.tsx | 90 +++++++++++ .../components/insights_carousel.tsx | 70 +++++++++ .../components/responsive_map.tsx | 24 +++ .../components/semicircle_gauge.tsx | 50 +++++++ .../components/state_tooltip.tsx | 76 ++++++++++ .../midterms-2026/components/tile_map.tsx | 141 ++++++++++++++++++ .../components/updated_badge.tsx | 51 +++++++ .../src/app/(main)/midterms-2026/constants.ts | 66 ++++++++ .../src/app/(main)/midterms-2026/data.ts | 133 +++++++++++++++++ .../helpers/fetch_community_insights.ts | 80 ++++++++++ .../helpers/fetch_dashboard_data.ts | 64 ++++++++ .../midterms-2026/helpers/post_utils.ts | 65 ++++++++ .../midterms-2026/helpers/state_color.ts | 31 ++++ .../src/app/(main)/midterms-2026/page.tsx | 48 ++++++ .../sections/community_insights.tsx | 35 +++++ .../sections/elections_map_section.tsx | 53 +++++++ .../sections/electoral_consequences.tsx | 33 ++++ .../(main)/midterms-2026/sections/footer.tsx | 43 ++++++ .../(main)/midterms-2026/sections/hero.tsx | 36 +++++ .../sections/things_to_watch.tsx | 113 ++++++++++++++ front_end/src/app/og/midterms-2026/page.tsx | 46 ++++++ .../src/app/og/midterms-2026/route/route.ts | 52 +++++++ 34 files changed, 2276 insertions(+), 6 deletions(-) create mode 100644 front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/distribution_curve.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/insight_card.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/insights_carousel.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/semicircle_gauge.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/tile_map.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/updated_badge.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/constants.ts create mode 100644 front_end/src/app/(main)/midterms-2026/data.ts create mode 100644 front_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.ts create mode 100644 front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts create mode 100644 front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts create mode 100644 front_end/src/app/(main)/midterms-2026/helpers/state_color.ts create mode 100644 front_end/src/app/(main)/midterms-2026/page.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/sections/footer.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/sections/hero.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx create mode 100644 front_end/src/app/og/midterms-2026/page.tsx create mode 100644 front_end/src/app/og/midterms-2026/route/route.ts diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index 5a1171d0b2..bf8320ff70 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -2185,5 +2185,63 @@ "whyTrustMetaculusLessNoiseTitle": "Méně šumu, více pravdy", "whyTrustMetaculusLessNoise": "Předpovědi Metaculus, poháněné davem, prořezávají šum tím, že zakládají každou předpověď na transparentních důkazech, odpovědném skóre a desetiletí doložené přesnosti. Metaculus vybavuje politiky, výzkumníky, novináře a korporátní organizace předpověďmi založenými na důkazech, které poskytují jasný, kvantifikovatelný vhled do nejkritičtějších nejistot světa. Prozkoumejte naši nabídku Business řešení a zjistěte, jak může Metaculus zlepšit rozhodování vaší organizace.", "publishTimeLockedDescription": "Čas publikace nelze po vytvoření změnit.", - "thousandsOfOpenQuestions": "20 000+ otevřených otázek" + "thousandsOfOpenQuestions": "20 000+ otevřených otázek", + "midtermsHubPageTitle": "2026 US Midterm Elections", + "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", + "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubUpdatedAgo": "Updated {time} ago", + "midtermsHubLastUpdated": "Last updated: {date}", + "midtermsHubLastUpdatedFull": "Last updated {date}", + "midtermsHubMapHeading": "2026 Elections Map", + "midtermsHubChamberSenate": "Senate", + "midtermsHubChamberHouse": "House", + "midtermsHubChamberGovernor": "Governor", + "midtermsHubChamberControl": "Chamber control", + "midtermsHubCongressForecast": "Congress control forecast", + "midtermsHubDemsNeed": "Dems need +{count}", + "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", + "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", + "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", + "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", + "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", + "midtermsHubThingsToWatch": "Things to Watch", + "midtermsHubVoterTurnout": "Voter turnout forecast", + "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", + "midtermsHubElectionIntegrity": "Election integrity watch", + "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", + "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", + "midtermsHubIntegrityFactor1": "Active redistricting litigation", + "midtermsHubIntegrityFactor2": "Voting rule changes pending", + "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", + "midtermsHubChance": "Chance", + "midtermsHubElectoralConsequences": "Electoral Consequences", + "midtermsHubConsequenceQuestion": "Question", + "midtermsHubConsequenceIfRep": "if Rep Congress", + "midtermsHubConsequenceIfDem": "if Dem Congress", + "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", + "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", + "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", + "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", + "midtermsHubCommunityInsights": "Community Insights", + "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", + "midtermsHubViewAllQuestions": "View all questions", + "midtermsHubKeyFactor": "KEY FACTOR", + "midtermsHubTopComment": "TOP COMMENT", + "midtermsHubScrollLeft": "Scroll insights left", + "midtermsHubScrollRight": "Scroll insights right", + "midtermsHubForecastersCount": "{count} forecasters", + "midtermsHubViewQuestion": "View full question", + "midtermsHubDemocrat": "Democrat", + "midtermsHubRepublican": "Republican", + "midtermsHubNotContested": "Not contested", + "midtermsHubDemPct": "{pct}% Democrat", + "midtermsHubRepPct": "{pct}% Republican", + "midtermsHubNoForecast": "No forecast", + "midtermsHubTierLikelyD": "Likely D", + "midtermsHubTierLeanD": "Lean D", + "midtermsHubTierTossUp": "Toss-up", + "midtermsHubTierLeanR": "Lean R", + "midtermsHubTierLikelyR": "Likely R", + "midtermsHubTierNotContested": "Not contested", + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." } diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 392a5a4764..7ab658e7f8 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2174,5 +2174,63 @@ "whyTrustMetaculusLessNoiseTitle": "Less noise, more truth", "whyTrustMetaculusLessNoise": "Metaculus's crowd-powered forecasts cut through the noise by grounding every prediction in transparent evidence, accountable scoring, and a decade of demonstrated accuracy. Metaculus equips policymakers, researchers, journalists, and corporate organizations with evidence-based forecasts that surface clear, quantifiable insight into the world's most critical uncertainties. Explore our suite of Business Solutions to learn more about how Metaculus can improve your organization's decision-making.", "publishTimeLockedDescription": "Publish time cannot be changed after creation.", - "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)" + "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)", + "midtermsHubPageTitle": "2026 US Midterm Elections", + "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", + "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubUpdatedAgo": "Updated {time} ago", + "midtermsHubLastUpdated": "Last updated: {date}", + "midtermsHubLastUpdatedFull": "Last updated {date}", + "midtermsHubMapHeading": "2026 Elections Map", + "midtermsHubChamberSenate": "Senate", + "midtermsHubChamberHouse": "House", + "midtermsHubChamberGovernor": "Governor", + "midtermsHubChamberControl": "Chamber control", + "midtermsHubCongressForecast": "Congress control forecast", + "midtermsHubDemsNeed": "Dems need +{count}", + "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", + "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", + "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", + "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", + "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", + "midtermsHubThingsToWatch": "Things to Watch", + "midtermsHubVoterTurnout": "Voter turnout forecast", + "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", + "midtermsHubElectionIntegrity": "Election integrity watch", + "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", + "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", + "midtermsHubIntegrityFactor1": "Active redistricting litigation", + "midtermsHubIntegrityFactor2": "Voting rule changes pending", + "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", + "midtermsHubChance": "Chance", + "midtermsHubElectoralConsequences": "Electoral Consequences", + "midtermsHubConsequenceQuestion": "Question", + "midtermsHubConsequenceIfRep": "if Rep Congress", + "midtermsHubConsequenceIfDem": "if Dem Congress", + "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", + "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", + "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", + "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", + "midtermsHubCommunityInsights": "Community Insights", + "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", + "midtermsHubViewAllQuestions": "View all questions", + "midtermsHubKeyFactor": "KEY FACTOR", + "midtermsHubTopComment": "TOP COMMENT", + "midtermsHubScrollLeft": "Scroll insights left", + "midtermsHubScrollRight": "Scroll insights right", + "midtermsHubForecastersCount": "{count} forecasters", + "midtermsHubViewQuestion": "View full question", + "midtermsHubDemocrat": "Democrat", + "midtermsHubRepublican": "Republican", + "midtermsHubNotContested": "Not contested", + "midtermsHubDemPct": "{pct}% Democrat", + "midtermsHubRepPct": "{pct}% Republican", + "midtermsHubNoForecast": "No forecast", + "midtermsHubTierLikelyD": "Likely D", + "midtermsHubTierLeanD": "Lean D", + "midtermsHubTierTossUp": "Toss-up", + "midtermsHubTierLeanR": "Lean R", + "midtermsHubTierLikelyR": "Likely R", + "midtermsHubTierNotContested": "Not contested", + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." } diff --git a/front_end/messages/es.json b/front_end/messages/es.json index ac46d8796e..7586a4353d 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -2185,5 +2185,63 @@ "whyTrustMetaculusLessNoiseTitle": "Menos ruido, más verdad", "whyTrustMetaculusLessNoise": "Los pronósticos impulsados por la multitud de Metaculus cortan el ruido al basar cada predicción en evidencia transparente, puntuaciones responsables y una década de precisión demostrada. Metaculus equipa a los responsables de políticas, investigadores, periodistas y organizaciones corporativas con pronósticos basados en evidencia que ofrecen una visión clara y cuantificable de las incertidumbres más críticas del mundo. Explora nuestra suite de Soluciones Empresariales para aprender cómo Metaculus puede mejorar la toma de decisiones de tu organización.", "publishTimeLockedDescription": "La hora de publicación no puede cambiarse después de la creación.", - "thousandsOfOpenQuestions": "20,000+ preguntas abiertas" + "thousandsOfOpenQuestions": "20,000+ preguntas abiertas", + "midtermsHubPageTitle": "2026 US Midterm Elections", + "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", + "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubUpdatedAgo": "Updated {time} ago", + "midtermsHubLastUpdated": "Last updated: {date}", + "midtermsHubLastUpdatedFull": "Last updated {date}", + "midtermsHubMapHeading": "2026 Elections Map", + "midtermsHubChamberSenate": "Senate", + "midtermsHubChamberHouse": "House", + "midtermsHubChamberGovernor": "Governor", + "midtermsHubChamberControl": "Chamber control", + "midtermsHubCongressForecast": "Congress control forecast", + "midtermsHubDemsNeed": "Dems need +{count}", + "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", + "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", + "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", + "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", + "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", + "midtermsHubThingsToWatch": "Things to Watch", + "midtermsHubVoterTurnout": "Voter turnout forecast", + "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", + "midtermsHubElectionIntegrity": "Election integrity watch", + "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", + "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", + "midtermsHubIntegrityFactor1": "Active redistricting litigation", + "midtermsHubIntegrityFactor2": "Voting rule changes pending", + "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", + "midtermsHubChance": "Chance", + "midtermsHubElectoralConsequences": "Electoral Consequences", + "midtermsHubConsequenceQuestion": "Question", + "midtermsHubConsequenceIfRep": "if Rep Congress", + "midtermsHubConsequenceIfDem": "if Dem Congress", + "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", + "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", + "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", + "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", + "midtermsHubCommunityInsights": "Community Insights", + "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", + "midtermsHubViewAllQuestions": "View all questions", + "midtermsHubKeyFactor": "KEY FACTOR", + "midtermsHubTopComment": "TOP COMMENT", + "midtermsHubScrollLeft": "Scroll insights left", + "midtermsHubScrollRight": "Scroll insights right", + "midtermsHubForecastersCount": "{count} forecasters", + "midtermsHubViewQuestion": "View full question", + "midtermsHubDemocrat": "Democrat", + "midtermsHubRepublican": "Republican", + "midtermsHubNotContested": "Not contested", + "midtermsHubDemPct": "{pct}% Democrat", + "midtermsHubRepPct": "{pct}% Republican", + "midtermsHubNoForecast": "No forecast", + "midtermsHubTierLikelyD": "Likely D", + "midtermsHubTierLeanD": "Lean D", + "midtermsHubTierTossUp": "Toss-up", + "midtermsHubTierLeanR": "Lean R", + "midtermsHubTierLikelyR": "Likely R", + "midtermsHubTierNotContested": "Not contested", + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." } diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index 8895cd7cc4..30747d48db 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -2183,5 +2183,63 @@ "whyTrustMetaculusLessNoiseTitle": "Menos ruído, mais verdade", "whyTrustMetaculusLessNoise": "As previsões baseadas na multidão do Metaculus cortam o ruído ao basear cada previsão em evidências transparentes, pontuações responsáveis e uma década de precisão demonstrada. O Metaculus equipa formuladores de políticas, pesquisadores, jornalistas e organizações corporativas com previsões baseadas em evidências que oferecem insights claros e quantificáveis sobre as incertezas mais críticas do mundo. Explore nosso conjunto de Soluções Empresariais para saber mais sobre como o Metaculus pode melhorar a tomada de decisões da sua organização.", "publishTimeLockedDescription": "O horário de publicação não pode ser alterado após a criação.", - "thousandsOfOpenQuestions": "20.000+ perguntas abertas" + "thousandsOfOpenQuestions": "20.000+ perguntas abertas", + "midtermsHubPageTitle": "2026 US Midterm Elections", + "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", + "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubUpdatedAgo": "Updated {time} ago", + "midtermsHubLastUpdated": "Last updated: {date}", + "midtermsHubLastUpdatedFull": "Last updated {date}", + "midtermsHubMapHeading": "2026 Elections Map", + "midtermsHubChamberSenate": "Senate", + "midtermsHubChamberHouse": "House", + "midtermsHubChamberGovernor": "Governor", + "midtermsHubChamberControl": "Chamber control", + "midtermsHubCongressForecast": "Congress control forecast", + "midtermsHubDemsNeed": "Dems need +{count}", + "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", + "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", + "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", + "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", + "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", + "midtermsHubThingsToWatch": "Things to Watch", + "midtermsHubVoterTurnout": "Voter turnout forecast", + "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", + "midtermsHubElectionIntegrity": "Election integrity watch", + "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", + "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", + "midtermsHubIntegrityFactor1": "Active redistricting litigation", + "midtermsHubIntegrityFactor2": "Voting rule changes pending", + "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", + "midtermsHubChance": "Chance", + "midtermsHubElectoralConsequences": "Electoral Consequences", + "midtermsHubConsequenceQuestion": "Question", + "midtermsHubConsequenceIfRep": "if Rep Congress", + "midtermsHubConsequenceIfDem": "if Dem Congress", + "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", + "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", + "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", + "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", + "midtermsHubCommunityInsights": "Community Insights", + "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", + "midtermsHubViewAllQuestions": "View all questions", + "midtermsHubKeyFactor": "KEY FACTOR", + "midtermsHubTopComment": "TOP COMMENT", + "midtermsHubScrollLeft": "Scroll insights left", + "midtermsHubScrollRight": "Scroll insights right", + "midtermsHubForecastersCount": "{count} forecasters", + "midtermsHubViewQuestion": "View full question", + "midtermsHubDemocrat": "Democrat", + "midtermsHubRepublican": "Republican", + "midtermsHubNotContested": "Not contested", + "midtermsHubDemPct": "{pct}% Democrat", + "midtermsHubRepPct": "{pct}% Republican", + "midtermsHubNoForecast": "No forecast", + "midtermsHubTierLikelyD": "Likely D", + "midtermsHubTierLeanD": "Lean D", + "midtermsHubTierTossUp": "Toss-up", + "midtermsHubTierLeanR": "Lean R", + "midtermsHubTierLikelyR": "Likely R", + "midtermsHubTierNotContested": "Not contested", + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." } diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index 31dc25025c..b8e620e60b 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -2182,5 +2182,63 @@ "whyTrustMetaculusLessNoiseTitle": "更少的噪音,更多的真相", "whyTrustMetaculusLessNoise": "Metaculus 的群眾驅動預測通過將每一個預測植根於透明的證據、負責的計分和十年的已證準確性,抑制了噪音。Metaculus 為政策制定者、研究人員、記者和企業組織提供基於證據的預測,為全球最重要的不確定性提供清晰、可量化的洞察。探索我們的 企業解決方案,了解 Metaculus 如何改善貴組織的決策。", "publishTimeLockedDescription": "建立後將無法更改發佈時間。", - "thousandsOfOpenQuestions": "20,000+ 開放問題" + "thousandsOfOpenQuestions": "20,000+ 開放問題", + "midtermsHubPageTitle": "2026 US Midterm Elections", + "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", + "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubUpdatedAgo": "Updated {time} ago", + "midtermsHubLastUpdated": "Last updated: {date}", + "midtermsHubLastUpdatedFull": "Last updated {date}", + "midtermsHubMapHeading": "2026 Elections Map", + "midtermsHubChamberSenate": "Senate", + "midtermsHubChamberHouse": "House", + "midtermsHubChamberGovernor": "Governor", + "midtermsHubChamberControl": "Chamber control", + "midtermsHubCongressForecast": "Congress control forecast", + "midtermsHubDemsNeed": "Dems need +{count}", + "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", + "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", + "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", + "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", + "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", + "midtermsHubThingsToWatch": "Things to Watch", + "midtermsHubVoterTurnout": "Voter turnout forecast", + "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", + "midtermsHubElectionIntegrity": "Election integrity watch", + "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", + "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", + "midtermsHubIntegrityFactor1": "Active redistricting litigation", + "midtermsHubIntegrityFactor2": "Voting rule changes pending", + "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", + "midtermsHubChance": "Chance", + "midtermsHubElectoralConsequences": "Electoral Consequences", + "midtermsHubConsequenceQuestion": "Question", + "midtermsHubConsequenceIfRep": "if Rep Congress", + "midtermsHubConsequenceIfDem": "if Dem Congress", + "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", + "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", + "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", + "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", + "midtermsHubCommunityInsights": "Community Insights", + "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", + "midtermsHubViewAllQuestions": "View all questions", + "midtermsHubKeyFactor": "KEY FACTOR", + "midtermsHubTopComment": "TOP COMMENT", + "midtermsHubScrollLeft": "Scroll insights left", + "midtermsHubScrollRight": "Scroll insights right", + "midtermsHubForecastersCount": "{count} forecasters", + "midtermsHubViewQuestion": "View full question", + "midtermsHubDemocrat": "Democrat", + "midtermsHubRepublican": "Republican", + "midtermsHubNotContested": "Not contested", + "midtermsHubDemPct": "{pct}% Democrat", + "midtermsHubRepPct": "{pct}% Republican", + "midtermsHubNoForecast": "No forecast", + "midtermsHubTierLikelyD": "Likely D", + "midtermsHubTierLeanD": "Lean D", + "midtermsHubTierTossUp": "Toss-up", + "midtermsHubTierLeanR": "Lean R", + "midtermsHubTierLikelyR": "Likely R", + "midtermsHubTierNotContested": "Not contested", + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." } diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index a4586eedec..39047dd448 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -2187,5 +2187,63 @@ "whyTrustMetaculusLessNoiseTitle": "少噪音,多真相", "whyTrustMetaculusLessNoise": "Metaculus 的众包预测通过以透明证据、可追溯的评分以及十年证明的准确性为基础来削减预测中的噪音。Metaculus 为政策制定者、研究人员、记者和企业组织提供基于证据的预测,为世界上最重要的不确定性提供清晰、可量化的洞察。了解我们的企业解决方案,了解 Metaculus 如何改善贵组织的决策。", "publishTimeLockedDescription": "创建后将无法更改发布时间。", - "thousandsOfOpenQuestions": "20,000+ 开放问题" + "thousandsOfOpenQuestions": "20,000+ 开放问题", + "midtermsHubPageTitle": "2026 US Midterm Elections", + "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", + "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubUpdatedAgo": "Updated {time} ago", + "midtermsHubLastUpdated": "Last updated: {date}", + "midtermsHubLastUpdatedFull": "Last updated {date}", + "midtermsHubMapHeading": "2026 Elections Map", + "midtermsHubChamberSenate": "Senate", + "midtermsHubChamberHouse": "House", + "midtermsHubChamberGovernor": "Governor", + "midtermsHubChamberControl": "Chamber control", + "midtermsHubCongressForecast": "Congress control forecast", + "midtermsHubDemsNeed": "Dems need +{count}", + "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", + "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", + "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", + "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", + "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", + "midtermsHubThingsToWatch": "Things to Watch", + "midtermsHubVoterTurnout": "Voter turnout forecast", + "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", + "midtermsHubElectionIntegrity": "Election integrity watch", + "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", + "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", + "midtermsHubIntegrityFactor1": "Active redistricting litigation", + "midtermsHubIntegrityFactor2": "Voting rule changes pending", + "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", + "midtermsHubChance": "Chance", + "midtermsHubElectoralConsequences": "Electoral Consequences", + "midtermsHubConsequenceQuestion": "Question", + "midtermsHubConsequenceIfRep": "if Rep Congress", + "midtermsHubConsequenceIfDem": "if Dem Congress", + "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", + "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", + "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", + "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", + "midtermsHubCommunityInsights": "Community Insights", + "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", + "midtermsHubViewAllQuestions": "View all questions", + "midtermsHubKeyFactor": "KEY FACTOR", + "midtermsHubTopComment": "TOP COMMENT", + "midtermsHubScrollLeft": "Scroll insights left", + "midtermsHubScrollRight": "Scroll insights right", + "midtermsHubForecastersCount": "{count} forecasters", + "midtermsHubViewQuestion": "View full question", + "midtermsHubDemocrat": "Democrat", + "midtermsHubRepublican": "Republican", + "midtermsHubNotContested": "Not contested", + "midtermsHubDemPct": "{pct}% Democrat", + "midtermsHubRepPct": "{pct}% Republican", + "midtermsHubNoForecast": "No forecast", + "midtermsHubTierLikelyD": "Likely D", + "midtermsHubTierLeanD": "Lean D", + "midtermsHubTierTossUp": "Toss-up", + "midtermsHubTierLeanR": "Lean R", + "midtermsHubTierLikelyR": "Likely R", + "midtermsHubTierNotContested": "Not contested", + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." } diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx new file mode 100644 index 0000000000..7383757647 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx @@ -0,0 +1,119 @@ +import { getTranslations } from "next-intl/server"; + +import { MIDTERMS_COLORS } from "../constants"; +import { ChamberData } from "../helpers/fetch_dashboard_data"; +import { getBinaryProbability } from "../helpers/post_utils"; + +const CURRENT_SENATE = { dem: 47, rep: 53 }; +const CURRENT_HOUSE = { dem: 215, rep: 220 }; +const SENATE_TOTAL = 100; +const HOUSE_TOTAL = 435; + +type Props = { + data: ChamberData; +}; + +export default async function ChamberControlCard({ data }: Props) { + const t = await getTranslations(); + + const senateDemProb = getBinaryProbability(data.senateControl); + const houseDemProb = getBinaryProbability(data.houseControl); + + const senateDemNeeded = Math.floor(SENATE_TOTAL / 2) + 1 - CURRENT_SENATE.dem; + const houseDemNeeded = Math.floor(HOUSE_TOTAL / 2) + 1 - CURRENT_HOUSE.dem; + + return ( +
+

+ {t("midtermsHubChamberControl")} +

+
+ + +
+
+ ); +} + +type RowProps = { + chamberLabel: string; + demProb: number | null; + currentDem: number; + currentRep: number; + demNeeded: number; + demNeededLabel: string; +}; + +function ChamberRow({ + chamberLabel, + demProb, + currentDem, + currentRep, + demNeededLabel, +}: RowProps) { + const demPct = demProb == null ? null : Math.round(demProb * 1000) / 10; + const repPct = demPct == null ? null : Math.round((100 - demPct) * 10) / 10; + + return ( +
+
+ + {chamberLabel} + + {demPct != null && repPct != null && ( + + {demPct}% + {" / "} + {repPct}% + + )} +
+
+ {demPct != null && ( + <> +
+
+ + )} +
+
+ + + R {currentRep} + + {" — "} + + D {currentDem} + + + {demNeededLabel} +
+
+ ); +} diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx new file mode 100644 index 0000000000..8047b55b40 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx @@ -0,0 +1,33 @@ +import { getTranslations } from "next-intl/server"; + +import cn from "@/utils/core/cn"; + +export default async function ChamberTabs() { + const t = await getTranslations(); + const tabs: { key: string; label: string; active: boolean }[] = [ + { key: "senate", label: t("midtermsHubChamberSenate"), active: true }, + { key: "house", label: t("midtermsHubChamberHouse"), active: false }, + { key: "governor", label: t("midtermsHubChamberGovernor"), active: false }, + ]; + + return ( +
+ {tabs.map((tab) => ( + + ))} +
+ ); +} diff --git a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx new file mode 100644 index 0000000000..32024968b8 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx @@ -0,0 +1,126 @@ +import { faUsers } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { getTranslations } from "next-intl/server"; + +import { PostWithForecasts } from "@/types/post"; +import { QuestionWithNumericForecasts } from "@/types/question"; + +import { MIDTERMS_COLORS } from "../constants"; +import { getForecastersCount } from "../helpers/post_utils"; + +type Outcome = { + key: "RR" | "RD" | "DR" | "DD"; + pct: number | null; + borderColor: string; + bgColor: string; +}; + +const PLACEHOLDER_OUTCOMES: Record = { + RR: 6.2, + RD: 61.5, + DR: 0.1, + DD: 32.3, +}; + +type Props = { + post: PostWithForecasts | null; +}; + +export default async function CongressOutcomeCard({ post }: Props) { + const t = await getTranslations(); + + const outcomes: Outcome[] = [ + { + key: "RR", + pct: extractPct(post, "RR") ?? PLACEHOLDER_OUTCOMES.RR, + borderColor: MIDTERMS_COLORS.repPrimary, + bgColor: MIDTERMS_COLORS.repLight, + }, + { + key: "RD", + pct: extractPct(post, "RD") ?? PLACEHOLDER_OUTCOMES.RD, + borderColor: MIDTERMS_COLORS.repPrimary, + bgColor: MIDTERMS_COLORS.repLight, + }, + { + key: "DR", + pct: extractPct(post, "DR") ?? PLACEHOLDER_OUTCOMES.DR, + borderColor: MIDTERMS_COLORS.demPrimary, + bgColor: MIDTERMS_COLORS.demLight, + }, + { + key: "DD", + pct: extractPct(post, "DD") ?? PLACEHOLDER_OUTCOMES.DD, + borderColor: MIDTERMS_COLORS.demPrimary, + bgColor: MIDTERMS_COLORS.demLight, + }, + ]; + + const forecasters = getForecastersCount(post); + + const labels: Record = { + RR: t("midtermsHubOutcomeRepRep"), + RD: t("midtermsHubOutcomeRepDem"), + DR: t("midtermsHubOutcomeDemRep"), + DD: t("midtermsHubOutcomeDemDem"), + }; + + return ( +
+
+

+ {t("midtermsHubCongressForecast")} +

+ {forecasters > 0 && ( + + + {forecasters} + + )} +
+
+ {outcomes.map((o) => ( +
+ + {labels[o.key]} + +
+
+
+ + {o.pct?.toFixed(1)}% + +
+ ))} +
+

+ {t("midtermsHubCongressSummary")} +

+
+ ); +} + +function extractPct( + post: PostWithForecasts | null, + outcomeKey: Outcome["key"] +): number | null { + if (!post?.group_of_questions) return null; + const questions = post.group_of_questions.questions as + | QuestionWithNumericForecasts[] + | undefined; + if (!questions) return null; + const question = questions.find((q) => q.label === outcomeKey); + if (!question) return null; + const center = + question.aggregations[question.default_aggregation_method]?.latest + ?.centers?.[0]; + if (center == null) return null; + return Math.round(center * 1000) / 10; +} diff --git a/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx b/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx new file mode 100644 index 0000000000..78f73f0f60 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx @@ -0,0 +1,70 @@ +import { getTranslations } from "next-intl/server"; + +import { MIDTERMS_COLORS } from "../constants"; +import { ConsequenceRow as ConsequenceRowData } from "../data"; + +type Props = { + row: ConsequenceRowData; +}; + +export default async function ConsequenceRow({ row }: Props) { + const t = await getTranslations(); + const question = (() => { + switch (row.questionKey) { + case "climate": + return t("midtermsHubConsequenceClimate"); + case "minWage": + return t("midtermsHubConsequenceMinWage"); + case "immigration": + return t("midtermsHubConsequenceImmigration"); + case "shutdown": + return t("midtermsHubConsequenceShutdown"); + } + })(); + + return ( +
+

+ {question} +

+ + +
+ ); +} + +function ConsequenceBar({ + pct, + color, + bg, +}: { + pct: number; + color: string; + bg: string; +}) { + return ( +
+
+
+
+ + {pct}% + +
+ ); +} diff --git a/front_end/src/app/(main)/midterms-2026/components/distribution_curve.tsx b/front_end/src/app/(main)/midterms-2026/components/distribution_curve.tsx new file mode 100644 index 0000000000..985a5de463 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/distribution_curve.tsx @@ -0,0 +1,64 @@ +import { FC } from "react"; + +import { MIDTERMS_COLORS } from "../constants"; + +type Props = { + value: number; + color?: string; +}; + +const DistributionCurve: FC = ({ + value, + color = MIDTERMS_COLORS.demPrimary, +}) => { + return ( +
+ +
+ + {value}% + +
+
+ ); +}; + +export default DistributionCurve; diff --git a/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx b/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx new file mode 100644 index 0000000000..ed6309b872 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { FC, useMemo } from "react"; + +import ExperimentMap from "@/app/(main)/experiments/components/experiment_map"; +import { BaseMapArea, MapType } from "@/types/experiments"; + +import { MIDTERMS_COLORS, STATE_NAMES } from "../constants"; +import StateTooltipContent from "./state_tooltip"; +import { + getCommentsCount, + getDemWinPct, + getForecastersCount, + SenateRaceWithPost, +} from "../helpers/post_utils"; +import { getStateColor } from "../helpers/state_color"; + +type Props = { + races: SenateRaceWithPost[]; +}; + +type StateMapArea = BaseMapArea & { + race: SenateRaceWithPost | null; +}; + +const GeographicMap: FC = ({ races }) => { + const racesByState = useMemo( + () => new Map(races.map((r) => [r.state, r])), + [races] + ); + + const mapAreas: StateMapArea[] = useMemo( + () => + Object.keys(STATE_NAMES).map((abbr) => ({ + abbreviation: abbr, + name: STATE_NAMES[abbr] ?? abbr, + x_adjust: 0, + y_adjust: 0, + race: racesByState.get(abbr) ?? null, + })), + [racesByState] + ); + + const getMapAreaColor = (area: StateMapArea) => { + if (!area.race) return MIDTERMS_COLORS.notContested; + return getStateColor(getDemWinPct(area.race.post)); + }; + + return ( +
+ { + if (!mapArea?.race) return null; + return ( +
+ +
+ ); + }} + /> + +
+ ); +}; + +const Legend: FC = () => { + const t = useTranslations(); + return ( +
+ + + {t("midtermsHubDemocrat")} + + + + {t("midtermsHubRepublican")} + + + + {t("midtermsHubNotContested")} + +
+ ); +}; + +export default GeographicMap; diff --git a/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx b/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx new file mode 100644 index 0000000000..5cdd0e5505 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx @@ -0,0 +1,90 @@ +"use client"; + +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { FC } from "react"; + +import { CommentType, KeyFactor } from "@/types/comment"; +import cn from "@/utils/core/cn"; + +import { CommunityInsight } from "../helpers/fetch_community_insights"; + +type Props = { + insight: CommunityInsight; +}; + +const InsightCard: FC = ({ insight }) => { + const t = useTranslations(); + + const isKeyFactor = insight.type === "key-factor"; + const text = isKeyFactor + ? extractKeyFactorText(insight.keyFactor) + : extractCommentText(insight.comment); + const forecasters = insight.race.post?.forecasts_count ?? 0; + const sourceTitle = insight.race.post?.title ?? insight.race.name; + const link = insight.race.post ? `/questions/${insight.race.post.id}` : "#"; + + return ( + + + {isKeyFactor ? t("midtermsHubKeyFactor") : t("midtermsHubTopComment")} + +

+ {text} +

+
+ {sourceTitle} + + {" · "} + {t("midtermsHubForecastersCount", { count: forecasters })} + +
+ + ); +}; + +function extractKeyFactorText(kf: KeyFactor): string { + if (kf.driver?.text) return kf.driver.text; + if (kf.news?.title) return kf.news.title; + if (kf.base_rate?.reference_class) { + const projected = kf.base_rate.projected_value; + const unit = kf.base_rate.unit; + if (projected != null && unit) { + return `${kf.base_rate.reference_class}: ${projected}${unit}`; + } + return kf.base_rate.reference_class; + } + return ""; +} + +function extractCommentText(comment: CommentType): string { + return stripMarkdown(comment.text).slice(0, 280); +} + +function stripMarkdown(text: string): string { + return text + .replace(/\*\*(.+?)\*\*/g, "$1") + .replace(/\*(.+?)\*/g, "$1") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/`([^`]+)`/g, "$1") + .replace(/^>\s+/gm, "") + .replace(/\n+/g, " ") + .trim(); +} + +export default InsightCard; diff --git a/front_end/src/app/(main)/midterms-2026/components/insights_carousel.tsx b/front_end/src/app/(main)/midterms-2026/components/insights_carousel.tsx new file mode 100644 index 0000000000..1861e34dd9 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/insights_carousel.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { + faChevronLeft, + faChevronRight, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useTranslations } from "next-intl"; +import { FC, useRef } from "react"; + +import cn from "@/utils/core/cn"; + +import InsightCard from "./insight_card"; +import { CommunityInsight } from "../helpers/fetch_community_insights"; + +const SCROLL_DELTA = 320; + +type Props = { + insights: CommunityInsight[]; +}; + +const InsightsCarousel: FC = ({ insights }) => { + const t = useTranslations(); + const scrollRef = useRef(null); + + const scroll = (direction: "left" | "right") => { + const el = scrollRef.current; + if (!el) return; + el.scrollBy({ + left: direction === "left" ? -SCROLL_DELTA : SCROLL_DELTA, + behavior: "smooth", + }); + }; + + return ( +
+
+ + +
+
+ {insights.map((insight, i) => ( + + ))} +
+
+ ); +}; + +const scrollButtonClass = + "flex h-8 w-8 items-center justify-center rounded-full border border-gray-300 bg-gray-0 text-gray-700 transition-colors hover:border-gray-400 hover:text-gray-900 dark:border-gray-300-dark dark:bg-gray-0-dark dark:text-gray-700-dark dark:hover:border-gray-400-dark dark:hover:text-gray-900-dark"; + +export default InsightsCarousel; diff --git a/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx b/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx new file mode 100644 index 0000000000..94cfcf5ee7 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx @@ -0,0 +1,24 @@ +import { FC } from "react"; + +import GeographicMap from "./geographic_map"; +import TileMap from "./tile_map"; +import { SenateRaceWithPost } from "../helpers/post_utils"; + +type Props = { + races: SenateRaceWithPost[]; +}; + +const ResponsiveMap: FC = ({ races }) => { + return ( + <> +
+ +
+
+ +
+ + ); +}; + +export default ResponsiveMap; diff --git a/front_end/src/app/(main)/midterms-2026/components/semicircle_gauge.tsx b/front_end/src/app/(main)/midterms-2026/components/semicircle_gauge.tsx new file mode 100644 index 0000000000..0f968dfaeb --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/semicircle_gauge.tsx @@ -0,0 +1,50 @@ +import { getTranslations } from "next-intl/server"; + +import { MIDTERMS_COLORS } from "../constants"; + +type Props = { + value: number; + color?: string; +}; + +const RADIUS = 45; +const STROKE_WIDTH = 10; +const CIRCUMFERENCE = Math.PI * RADIUS; + +export default async function SemicircleGauge({ + value, + color = MIDTERMS_COLORS.repPrimary, +}: Props) { + const t = await getTranslations(); + const fillLength = (value / 100) * CIRCUMFERENCE; + + return ( +
+ +
+ + {value}% + + + {t("midtermsHubChance")} + +
+
+ ); +} diff --git a/front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx b/front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx new file mode 100644 index 0000000000..5e2008c913 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { faComment, faUsers } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Link from "next/link"; +import { useTranslations } from "next-intl"; +import { FC } from "react"; + +import { MIDTERMS_COLORS, STATE_NAMES } from "../constants"; +import { SenateRaceWithPost } from "../helpers/post_utils"; + +type Props = { + race: SenateRaceWithPost; + demWinPct: number | null; + forecasters: number; + comments: number; +}; + +const StateTooltipContent: FC = ({ + race, + demWinPct, + forecasters, + comments, +}) => { + const t = useTranslations(); + const stateName = STATE_NAMES[race.state] ?? race.name; + + const isDem = demWinPct != null && demWinPct >= 50; + const probLabel = + demWinPct == null + ? t("midtermsHubNoForecast") + : isDem + ? t("midtermsHubDemPct", { pct: demWinPct }) + : t("midtermsHubRepPct", { pct: 100 - demWinPct }); + + return ( +
+

+ {stateName} +

+
+ + {probLabel} + +
+
+ + + {comments} + + + + {forecasters} + +
+ {race.post && ( + + {t("midtermsHubViewQuestion")} + + )} +
+ ); +}; + +export default StateTooltipContent; diff --git a/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx b/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx new file mode 100644 index 0000000000..143e73f546 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { FC, MouseEvent, useState } from "react"; + +import cn from "@/utils/core/cn"; + +import { MIDTERMS_COLORS, STATE_NAMES } from "../constants"; +import { US_TILE_GRID } from "../data"; +import StateTooltipContent from "./state_tooltip"; +import { + getCommentsCount, + getDemWinPct, + getForecastersCount, + SenateRaceWithPost, +} from "../helpers/post_utils"; +import { getStateColor } from "../helpers/state_color"; + +type Props = { + races: SenateRaceWithPost[]; +}; + +type HoverState = { + abbr: string; + x: number; + y: number; +} | null; + +const MAX_COL = Math.max(...US_TILE_GRID.map((c) => c.col)); +const MAX_ROW = Math.max(...US_TILE_GRID.map((c) => c.row)); + +const TileMap: FC = ({ races }) => { + const [hovered, setHovered] = useState(null); + const racesByState = new Map(races.map((r) => [r.state, r])); + + const handleEnter = (abbr: string, e: MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const parent = e.currentTarget.closest(".tile-map-container"); + if (!parent) return; + const parentRect = parent.getBoundingClientRect(); + setHovered({ + abbr, + x: rect.left - parentRect.left + rect.width / 2, + y: rect.top - parentRect.top, + }); + }; + + const hoveredRace = hovered ? racesByState.get(hovered.abbr) : null; + + return ( +
+
+ {US_TILE_GRID.map(({ abbr, row, col }) => { + const race = racesByState.get(abbr); + const demWinPct = getDemWinPct(race?.post ?? null); + const fillColor = getStateColor(demWinPct); + const isContested = race !== undefined; + + return ( + + ); + })} +
+ + {hoveredRace && hovered && ( +
+ +
+ )} + + +
+ ); +}; + +const Legend: FC = () => { + const t = useTranslations(); + return ( +
+ + + {t("midtermsHubDemocrat")} + + + + {t("midtermsHubRepublican")} + + + + {t("midtermsHubNotContested")} + +
+ ); +}; + +export default TileMap; diff --git a/front_end/src/app/(main)/midterms-2026/components/updated_badge.tsx b/front_end/src/app/(main)/midterms-2026/components/updated_badge.tsx new file mode 100644 index 0000000000..09f46fb19e --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/updated_badge.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { FC, useEffect, useState } from "react"; + +type Props = { + /** Latest update timestamp (ISO or epoch ms) */ + timestamp: string | number | null; +}; + +const UpdatedBadge: FC = ({ timestamp }) => { + const t = useTranslations(); + const [label, setLabel] = useState(() => formatRelative(timestamp)); + + useEffect(() => { + if (!timestamp) return; + const id = setInterval(() => setLabel(formatRelative(timestamp)), 60_000); + return () => clearInterval(id); + }, [timestamp]); + + if (!timestamp) { + return null; + } + + return ( + + + + + + {t("midtermsHubUpdatedAgo", { time: label })} + + ); +}; + +function formatRelative(timestamp: string | number | null): string { + if (!timestamp) return ""; + const then = + typeof timestamp === "number" ? timestamp : Date.parse(timestamp); + if (Number.isNaN(then)) return ""; + const diffSec = Math.max(0, Math.round((Date.now() - then) / 1000)); + if (diffSec < 60) return `${diffSec}s`; + const diffMin = Math.round(diffSec / 60); + if (diffMin < 60) return `${diffMin}m`; + const diffHr = Math.round(diffMin / 60); + if (diffHr < 24) return `${diffHr}h`; + const diffDay = Math.round(diffHr / 24); + return `${diffDay}d`; +} + +export default UpdatedBadge; diff --git a/front_end/src/app/(main)/midterms-2026/constants.ts b/front_end/src/app/(main)/midterms-2026/constants.ts new file mode 100644 index 0000000000..045fdd6acc --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/constants.ts @@ -0,0 +1,66 @@ +export const MIDTERMS_COLORS = { + demPrimary: "#6B7AE8", + demLight: "#CECBF6", + repPrimary: "#E8827A", + repLight: "#FCCAC7", + likelyD: "#4A5CD4", + leanD: "#8B97EE", + tossUp: "#D3D1C7", + leanR: "#EDA8A4", + likelyR: "#D4504A", + notContested: "#FFFFFF", +} as const; + +export const STATE_NAMES: Record = { + AL: "Alabama", + AK: "Alaska", + AZ: "Arizona", + AR: "Arkansas", + CA: "California", + CO: "Colorado", + CT: "Connecticut", + DE: "Delaware", + DC: "District of Columbia", + FL: "Florida", + GA: "Georgia", + HI: "Hawaii", + ID: "Idaho", + IL: "Illinois", + IN: "Indiana", + IA: "Iowa", + KS: "Kansas", + KY: "Kentucky", + LA: "Louisiana", + ME: "Maine", + MD: "Maryland", + MA: "Massachusetts", + MI: "Michigan", + MN: "Minnesota", + MS: "Mississippi", + MO: "Missouri", + MT: "Montana", + NE: "Nebraska", + NV: "Nevada", + NH: "New Hampshire", + NJ: "New Jersey", + NM: "New Mexico", + NY: "New York", + NC: "North Carolina", + ND: "North Dakota", + OH: "Ohio", + OK: "Oklahoma", + OR: "Oregon", + PA: "Pennsylvania", + RI: "Rhode Island", + SC: "South Carolina", + SD: "South Dakota", + TN: "Tennessee", + TX: "Texas", + UT: "Utah", + VT: "Vermont", + VA: "Virginia", + WA: "Washington", + WV: "West Virginia", + WI: "Wisconsin", + WY: "Wyoming", +}; diff --git a/front_end/src/app/(main)/midterms-2026/data.ts b/front_end/src/app/(main)/midterms-2026/data.ts new file mode 100644 index 0000000000..b2328287e9 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/data.ts @@ -0,0 +1,133 @@ +export const MIDTERMS_PROJECT_ID = 32840; + +export type SenateRace = { + state: string; + name: string; + postId: number; +}; + +// Placeholder post IDs — real values supplied later from project 32840 +export const SENATE_RACES: SenateRace[] = [ + { state: "AL", name: "Alabama Senate", postId: 0 }, + { state: "AK", name: "Alaska Senate", postId: 0 }, + { state: "AR", name: "Arkansas Senate", postId: 0 }, + { state: "CO", name: "Colorado Senate", postId: 0 }, + { state: "DE", name: "Delaware Senate", postId: 0 }, + { state: "FL", name: "Florida Senate", postId: 0 }, + { state: "GA", name: "Georgia Senate", postId: 0 }, + { state: "ID", name: "Idaho Senate", postId: 0 }, + { state: "IL", name: "Illinois Senate", postId: 0 }, + { state: "IA", name: "Iowa Senate", postId: 0 }, + { state: "KS", name: "Kansas Senate", postId: 0 }, + { state: "KY", name: "Kentucky Senate", postId: 0 }, + { state: "LA", name: "Louisiana Senate", postId: 0 }, + { state: "ME", name: "Maine Senate", postId: 0 }, + { state: "MA", name: "Massachusetts Senate", postId: 0 }, + { state: "MI", name: "Michigan Senate", postId: 0 }, + { state: "MN", name: "Minnesota Senate", postId: 0 }, + { state: "MS", name: "Mississippi Senate", postId: 0 }, + { state: "MT", name: "Montana Senate", postId: 0 }, + { state: "NE", name: "Nebraska Senate", postId: 0 }, + { state: "NH", name: "New Hampshire Senate", postId: 0 }, + { state: "NJ", name: "New Jersey Senate", postId: 0 }, + { state: "NM", name: "New Mexico Senate", postId: 0 }, + { state: "NC", name: "North Carolina Senate", postId: 0 }, + { state: "OH", name: "Ohio Senate", postId: 0 }, + { state: "OK", name: "Oklahoma Senate", postId: 0 }, + { state: "OR", name: "Oregon Senate", postId: 0 }, + { state: "RI", name: "Rhode Island Senate", postId: 0 }, + { state: "SC", name: "South Carolina Senate", postId: 0 }, + { state: "SD", name: "South Dakota Senate", postId: 0 }, + { state: "TN", name: "Tennessee Senate", postId: 0 }, + { state: "TX", name: "Texas Senate", postId: 0 }, + { state: "VA", name: "Virginia Senate", postId: 0 }, + { state: "WV", name: "West Virginia Senate", postId: 0 }, + { state: "WY", name: "Wyoming Senate", postId: 0 }, +]; + +export type ChamberQuestionIds = { + senateControl: number; + houseControl: number; + congressOutcomeGroup: number; + voterTurnout: number; + electionIntegrity: number; +}; + +export const CHAMBER_QUESTIONS: ChamberQuestionIds = { + senateControl: 0, + houseControl: 0, + congressOutcomeGroup: 0, + voterTurnout: 0, + electionIntegrity: 0, +}; + +export type ConsequenceRow = { + questionKey: "climate" | "minWage" | "immigration" | "shutdown"; + repCongressPct: number; + demCongressPct: number; +}; + +// Hardcoded mock rows — swap with real conditional questions when ready +export const MOCK_CONSEQUENCES: ConsequenceRow[] = [ + { questionKey: "climate", repCongressPct: 12, demCongressPct: 67 }, + { questionKey: "minWage", repCongressPct: 8, demCongressPct: 52 }, + { questionKey: "immigration", repCongressPct: 35, demCongressPct: 28 }, + { questionKey: "shutdown", repCongressPct: 18, demCongressPct: 42 }, +]; + +export type TileCell = { abbr: string; row: number; col: number }; + +// 11-column × 8-row grid approximating US geography +export const US_TILE_GRID: TileCell[] = [ + { abbr: "AK", row: 0, col: 0 }, + { abbr: "ME", row: 0, col: 10 }, + { abbr: "VT", row: 1, col: 9 }, + { abbr: "NH", row: 1, col: 10 }, + { abbr: "WA", row: 2, col: 0 }, + { abbr: "ID", row: 2, col: 1 }, + { abbr: "MT", row: 2, col: 2 }, + { abbr: "ND", row: 2, col: 3 }, + { abbr: "MN", row: 2, col: 4 }, + { abbr: "WI", row: 2, col: 5 }, + { abbr: "MI", row: 2, col: 7 }, + { abbr: "NY", row: 2, col: 8 }, + { abbr: "MA", row: 2, col: 9 }, + { abbr: "RI", row: 2, col: 10 }, + { abbr: "OR", row: 3, col: 0 }, + { abbr: "NV", row: 3, col: 1 }, + { abbr: "WY", row: 3, col: 2 }, + { abbr: "SD", row: 3, col: 3 }, + { abbr: "IA", row: 3, col: 4 }, + { abbr: "IL", row: 3, col: 5 }, + { abbr: "IN", row: 3, col: 6 }, + { abbr: "OH", row: 3, col: 7 }, + { abbr: "PA", row: 3, col: 8 }, + { abbr: "NJ", row: 3, col: 9 }, + { abbr: "CT", row: 3, col: 10 }, + { abbr: "CA", row: 4, col: 0 }, + { abbr: "UT", row: 4, col: 1 }, + { abbr: "CO", row: 4, col: 2 }, + { abbr: "NE", row: 4, col: 3 }, + { abbr: "KS", row: 4, col: 4 }, + { abbr: "MO", row: 4, col: 5 }, + { abbr: "KY", row: 4, col: 6 }, + { abbr: "WV", row: 4, col: 7 }, + { abbr: "VA", row: 4, col: 8 }, + { abbr: "MD", row: 4, col: 9 }, + { abbr: "DE", row: 4, col: 10 }, + { abbr: "AZ", row: 5, col: 1 }, + { abbr: "NM", row: 5, col: 2 }, + { abbr: "OK", row: 5, col: 3 }, + { abbr: "AR", row: 5, col: 4 }, + { abbr: "TN", row: 5, col: 5 }, + { abbr: "NC", row: 5, col: 6 }, + { abbr: "SC", row: 5, col: 7 }, + { abbr: "DC", row: 5, col: 9 }, + { abbr: "TX", row: 6, col: 2 }, + { abbr: "LA", row: 6, col: 4 }, + { abbr: "MS", row: 6, col: 5 }, + { abbr: "AL", row: 6, col: 6 }, + { abbr: "GA", row: 6, col: 7 }, + { abbr: "FL", row: 6, col: 8 }, + { abbr: "HI", row: 7, col: 0 }, +]; diff --git a/front_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.ts b/front_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.ts new file mode 100644 index 0000000000..a5f9926673 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.ts @@ -0,0 +1,80 @@ +import "server-only"; +import { cache } from "react"; + +import ServerCommentsApi from "@/services/api/comments/comments.server"; +import { CommentType, KeyFactor } from "@/types/comment"; + +import { fetchSenateRaces } from "./fetch_dashboard_data"; +import { SenateRaceWithPost } from "./post_utils"; + +export type KeyFactorInsight = { + type: "key-factor"; + keyFactor: KeyFactor; + race: SenateRaceWithPost; +}; + +export type TopCommentInsight = { + type: "top-comment"; + comment: CommentType; + race: SenateRaceWithPost; +}; + +export type CommunityInsight = KeyFactorInsight | TopCommentInsight; + +const MAX_KEY_FACTORS = 4; +const MAX_TOP_COMMENTS = 4; + +export const fetchCommunityInsights = cache( + async (): Promise => { + const races = await fetchSenateRaces(); + const racesWithPosts = races.filter((r) => r.post); + + if (!racesWithPosts.length) return []; + + const keyFactorCards: KeyFactorInsight[] = racesWithPosts + .flatMap((race) => + (race.post?.key_factors ?? []).map((kf) => ({ + type: "key-factor" as const, + keyFactor: kf, + race, + })) + ) + .sort((a, b) => b.keyFactor.vote.score - a.keyFactor.vote.score) + .slice(0, MAX_KEY_FACTORS); + + const topCommentResults = await Promise.all( + racesWithPosts.map(async (race) => { + try { + const { results } = await ServerCommentsApi.getComments({ + post: race.postId, + sort: "-vote_score", + limit: 1, + parent_isnull: true, + }); + const comment = results[0]; + if (!comment) return null; + return { type: "top-comment" as const, comment, race }; + } catch { + return null; + } + }) + ); + + const topCommentCards: TopCommentInsight[] = topCommentResults + .filter((c): c is TopCommentInsight => c !== null) + .sort((a, b) => (b.comment.vote_score ?? 0) - (a.comment.vote_score ?? 0)) + .slice(0, MAX_TOP_COMMENTS); + + return interleave(keyFactorCards, topCommentCards); + } +); + +function interleave(a: T[], b: T[]): T[] { + const out: T[] = []; + const len = Math.max(a.length, b.length); + for (let i = 0; i < len; i++) { + if (i < a.length) out.push(a[i] as T); + if (i < b.length) out.push(b[i] as T); + } + return out; +} diff --git a/front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts b/front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts new file mode 100644 index 0000000000..7b353c9496 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts @@ -0,0 +1,64 @@ +import "server-only"; +import { cache } from "react"; + +import ServerPostsApi from "@/services/api/posts/posts.server"; +import { PostWithForecasts } from "@/types/post"; + +import { CHAMBER_QUESTIONS, SENATE_RACES } from "../data"; +import { SenateRaceWithPost } from "./post_utils"; + +export const fetchSenateRaces = cache( + async (): Promise => { + const ids = SENATE_RACES.map((r) => r.postId).filter((id) => id > 0); + if (!ids.length) { + return SENATE_RACES.map((r) => ({ ...r, post: null })); + } + + const { results } = await ServerPostsApi.getPostsWithCP({ + ids, + limit: ids.length, + }); + + const byId = new Map(results.map((p) => [p.id, p])); + return SENATE_RACES.map((r) => ({ + ...r, + post: byId.get(r.postId) ?? null, + })); + } +); + +export type ChamberData = { + senateControl: PostWithForecasts | null; + houseControl: PostWithForecasts | null; + congressOutcomeGroup: PostWithForecasts | null; + voterTurnout: PostWithForecasts | null; + electionIntegrity: PostWithForecasts | null; +}; + +export const fetchChamberData = cache(async (): Promise => { + const ids = Object.values(CHAMBER_QUESTIONS).filter((id) => id > 0); + if (!ids.length) { + return { + senateControl: null, + houseControl: null, + congressOutcomeGroup: null, + voterTurnout: null, + electionIntegrity: null, + }; + } + + const { results } = await ServerPostsApi.getPostsWithCP({ + ids, + limit: ids.length, + }); + const byId = new Map(results.map((p) => [p.id, p])); + + return { + senateControl: byId.get(CHAMBER_QUESTIONS.senateControl) ?? null, + houseControl: byId.get(CHAMBER_QUESTIONS.houseControl) ?? null, + congressOutcomeGroup: + byId.get(CHAMBER_QUESTIONS.congressOutcomeGroup) ?? null, + voterTurnout: byId.get(CHAMBER_QUESTIONS.voterTurnout) ?? null, + electionIntegrity: byId.get(CHAMBER_QUESTIONS.electionIntegrity) ?? null, + }; +}); diff --git a/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts b/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts new file mode 100644 index 0000000000..ddecb778d7 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts @@ -0,0 +1,65 @@ +import { PostWithForecasts } from "@/types/post"; +import { QuestionType, QuestionWithNumericForecasts } from "@/types/question"; +import { scaleInternalLocation } from "@/utils/math"; + +import { SenateRace } from "../data"; + +export type SenateRaceWithPost = SenateRace & { + post: PostWithForecasts | null; +}; + +export function getBinaryProbability( + post: PostWithForecasts | null +): number | null { + if (!post?.question) return null; + const q = post.question as QuestionWithNumericForecasts; + if (q.type !== QuestionType.Binary) return null; + const center = + q.aggregations[q.default_aggregation_method]?.latest?.centers?.[0]; + return center ?? null; +} + +export function getNumericForecast( + post: PostWithForecasts | null +): number | null { + if (!post?.question) return null; + const question = post.question as QuestionWithNumericForecasts; + const center = + question.aggregations[question.default_aggregation_method]?.latest + ?.centers?.[0]; + if (center == null) return null; + return scaleInternalLocation(center, question.scaling); +} + +export function getForecastersCount(post: PostWithForecasts | null): number { + if (!post) return 0; + return post.forecasts_count ?? 0; +} + +export function getCommentsCount(post: PostWithForecasts | null): number { + if (!post) return 0; + return post.comment_count ?? 0; +} + +export function getDemWinPct(post: PostWithForecasts | null): number | null { + if (!post) return null; + const prob = getBinaryProbability(post); + if (prob == null) return null; + return Math.round(prob * 100); +} + +export function getLatestUpdateTime( + posts: (PostWithForecasts | null)[] +): Date | null { + let latest: Date | null = null; + for (const post of posts) { + if (!post?.question) continue; + const q = post.question as QuestionWithNumericForecasts; + const startTime = + q.aggregations[q.default_aggregation_method]?.latest?.start_time; + if (!startTime) continue; + const d = new Date(startTime); + if (!latest || d > latest) latest = d; + } + return latest; +} diff --git a/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts b/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts new file mode 100644 index 0000000000..c7ff839d0b --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts @@ -0,0 +1,31 @@ +import { MIDTERMS_COLORS } from "../constants"; + +export type StateTier = + | "likelyD" + | "leanD" + | "tossUp" + | "leanR" + | "likelyR" + | "notContested"; + +export function getStateTier(demWinPct: number | null | undefined): StateTier { + if (demWinPct == null) return "notContested"; + if (demWinPct > 70) return "likelyD"; + if (demWinPct >= 55) return "leanD"; + if (demWinPct >= 45) return "tossUp"; + if (demWinPct >= 30) return "leanR"; + return "likelyR"; +} + +export function getStateColor(demWinPct: number | null | undefined): string { + return MIDTERMS_COLORS[getStateTier(demWinPct)]; +} + +export const STATE_TIER_LABEL_KEY: Record = { + likelyD: "midtermsHubTierLikelyD", + leanD: "midtermsHubTierLeanD", + tossUp: "midtermsHubTierTossUp", + leanR: "midtermsHubTierLeanR", + likelyR: "midtermsHubTierLikelyR", + notContested: "midtermsHubTierNotContested", +}; diff --git a/front_end/src/app/(main)/midterms-2026/page.tsx b/front_end/src/app/(main)/midterms-2026/page.tsx new file mode 100644 index 0000000000..cd5f056faf --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/page.tsx @@ -0,0 +1,48 @@ +import { Metadata } from "next"; +import { getTranslations } from "next-intl/server"; + +import { getPublicSettings } from "@/utils/public_settings.server"; + +import CommunityInsightsSection from "./sections/community_insights"; +import ElectionsMapSection from "./sections/elections_map_section"; +import ElectoralConsequencesSection from "./sections/electoral_consequences"; +import FooterSection from "./sections/footer"; +import HeroSection from "./sections/hero"; +import ThingsToWatchSection from "./sections/things_to_watch"; + +export async function generateMetadata(): Promise { + const t = await getTranslations(); + const { PUBLIC_APP_URL } = getPublicSettings(); + const title = t("midtermsHubMetaTitle"); + const description = t("midtermsHubMetaDescription"); + const img = `${PUBLIC_APP_URL}/og/midterms-2026/route?theme=dark`; + + return { + title, + description, + openGraph: { + title, + description, + images: [{ url: img, width: 1200, height: 630, alt: title }], + }, + twitter: { + card: "summary_large_image", + title, + description, + images: [img], + }, + }; +} + +export default function MidtermsHubPage() { + return ( +
+ + + + + + +
+ ); +} diff --git a/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx b/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx new file mode 100644 index 0000000000..a57564a6dc --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx @@ -0,0 +1,35 @@ +import Link from "next/link"; +import { getTranslations } from "next-intl/server"; + +import InsightsCarousel from "../components/insights_carousel"; +import { MIDTERMS_PROJECT_ID } from "../data"; +import { fetchCommunityInsights } from "../helpers/fetch_community_insights"; + +export default async function CommunityInsightsSection() { + const t = await getTranslations(); + const insights = await fetchCommunityInsights(); + + if (!insights.length) return null; + + return ( +
+
+
+

+ {t("midtermsHubCommunityInsights")} +

+

+ {t("midtermsHubInsightsSubtitle")} +

+
+ + {t("midtermsHubViewAllQuestions")} → + +
+ +
+ ); +} diff --git a/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx b/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx new file mode 100644 index 0000000000..8f6faa7b88 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx @@ -0,0 +1,53 @@ +import { format } from "date-fns"; +import { getTranslations } from "next-intl/server"; + +import ChamberControlCard from "../components/chamber_control_card"; +import ChamberTabs from "../components/chamber_tabs"; +import CongressOutcomeCard from "../components/congress_outcome_card"; +import ResponsiveMap from "../components/responsive_map"; +import { + fetchChamberData, + fetchSenateRaces, +} from "../helpers/fetch_dashboard_data"; +import { getLatestUpdateTime } from "../helpers/post_utils"; + +export default async function ElectionsMapSection() { + const t = await getTranslations(); + const [races, chamber] = await Promise.all([ + fetchSenateRaces(), + fetchChamberData(), + ]); + + const latest = getLatestUpdateTime([ + ...races.map((r) => r.post), + chamber.senateControl, + chamber.houseControl, + ]); + + return ( +
+
+ + {t("midtermsHubMapHeading")} + + {latest && ( + + {t("midtermsHubLastUpdated", { + date: format(latest, "MMM d, yyyy"), + })} + + )} +
+
+
+ + +
+
+ + +
+
+
+ ); +} diff --git a/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx b/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx new file mode 100644 index 0000000000..737cbbe33d --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx @@ -0,0 +1,33 @@ +import { getTranslations } from "next-intl/server"; + +import ConsequenceRow from "../components/consequence_row"; +import { MOCK_CONSEQUENCES } from "../data"; + +export default async function ElectoralConsequencesSection() { + const t = await getTranslations(); + return ( +
+
+

+ {t("midtermsHubElectoralConsequences")} +

+
+
+
+ + {t("midtermsHubConsequenceQuestion")} + + + {t("midtermsHubConsequenceIfRep")} + + + {t("midtermsHubConsequenceIfDem")} + +
+ {MOCK_CONSEQUENCES.map((row) => ( + + ))} +
+
+ ); +} diff --git a/front_end/src/app/(main)/midterms-2026/sections/footer.tsx b/front_end/src/app/(main)/midterms-2026/sections/footer.tsx new file mode 100644 index 0000000000..1739d582dd --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/sections/footer.tsx @@ -0,0 +1,43 @@ +import { format } from "date-fns"; +import Link from "next/link"; +import { getTranslations } from "next-intl/server"; + +import { + fetchChamberData, + fetchSenateRaces, +} from "../helpers/fetch_dashboard_data"; +import { getLatestUpdateTime } from "../helpers/post_utils"; + +export default async function FooterSection() { + const t = await getTranslations(); + const [races, chamber] = await Promise.all([ + fetchSenateRaces(), + fetchChamberData(), + ]); + const latest = getLatestUpdateTime([ + ...races.map((r) => r.post), + chamber.senateControl, + chamber.houseControl, + ]); + + return ( +
+
+

{t("midtermsHubFooterDisclaimer")}

+ + metaculus.com + +
+ {latest && ( +

+ {t("midtermsHubLastUpdatedFull", { + date: format(latest, "MMMM d, yyyy, HH:mm 'UTC'"), + })} +

+ )} +
+ ); +} diff --git a/front_end/src/app/(main)/midterms-2026/sections/hero.tsx b/front_end/src/app/(main)/midterms-2026/sections/hero.tsx new file mode 100644 index 0000000000..7af95acfe0 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/sections/hero.tsx @@ -0,0 +1,36 @@ +import { getTranslations } from "next-intl/server"; + +import UpdatedBadge from "../components/updated_badge"; +import { + fetchChamberData, + fetchSenateRaces, +} from "../helpers/fetch_dashboard_data"; +import { getLatestUpdateTime } from "../helpers/post_utils"; + +export default async function HeroSection() { + const t = await getTranslations(); + const [races, chamber] = await Promise.all([ + fetchSenateRaces(), + fetchChamberData(), + ]); + const latest = getLatestUpdateTime([ + ...races.map((r) => r.post), + chamber.senateControl, + chamber.houseControl, + ]); + + return ( +
+
+

+ + metaculus + + | + {t("midtermsHubPageTitle")} +

+ +
+
+ ); +} diff --git a/front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx b/front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx new file mode 100644 index 0000000000..5569d49ecb --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx @@ -0,0 +1,113 @@ +import { faUsers } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Link from "next/link"; +import { getTranslations } from "next-intl/server"; + +import DistributionCurve from "../components/distribution_curve"; +import SemicircleGauge from "../components/semicircle_gauge"; +import { MIDTERMS_COLORS } from "../constants"; +import { fetchChamberData } from "../helpers/fetch_dashboard_data"; +import { + getBinaryProbability, + getForecastersCount, + getNumericForecast, +} from "../helpers/post_utils"; + +const PLACEHOLDER_TURNOUT = 49.3; +const PLACEHOLDER_INTEGRITY = 9; + +export default async function ThingsToWatchSection() { + const t = await getTranslations(); + const chamber = await fetchChamberData(); + + const turnoutValue = + getNumericForecast(chamber.voterTurnout) ?? PLACEHOLDER_TURNOUT; + const turnoutForecasters = getForecastersCount(chamber.voterTurnout); + + const integrityProb = getBinaryProbability(chamber.electionIntegrity); + const integrityValue = + integrityProb != null + ? Math.round(integrityProb * 100) + : PLACEHOLDER_INTEGRITY; + const integrityForecasters = getForecastersCount(chamber.electionIntegrity); + const integrityLink = chamber.electionIntegrity + ? `/questions/${chamber.electionIntegrity.id}` + : null; + + const integrityKeyFactors: string[] = [ + t("midtermsHubIntegrityFactor1"), + t("midtermsHubIntegrityFactor2"), + t("midtermsHubIntegrityFactor3"), + ]; + + return ( +
+
+ + {t("midtermsHubThingsToWatch")} + +
+
+
+
+

+ {t("midtermsHubVoterTurnout")} +

+ + + {t("midtermsHubForecastersCount", { count: turnoutForecasters })} + +
+ +

+ {t("midtermsHubTurnoutContext")} +

+
+ +
+
+

+ {t("midtermsHubElectionIntegrity")} +

+ + + {t("midtermsHubForecastersCount", { + count: integrityForecasters, + })} + +
+
+ +
+

+ {t("midtermsHubIntegrityQuestion")} +

+

+ {t("midtermsHubIntegrityContext")} +

+
+
+
+ {integrityKeyFactors.map((factor, i) => ( + + {factor} + + ))} +
+ {integrityLink && ( + + {t("midtermsHubViewQuestion")} → + + )} +
+
+
+ ); +} diff --git a/front_end/src/app/og/midterms-2026/page.tsx b/front_end/src/app/og/midterms-2026/page.tsx new file mode 100644 index 0000000000..e75bb48dfb --- /dev/null +++ b/front_end/src/app/og/midterms-2026/page.tsx @@ -0,0 +1,46 @@ +export const revalidate = 3600; + +const BG = "#1d2733"; + +export default function OgMidtermsPage() { + return ( +
+
+
+
+ METACULUS +
+
+

+ 2026 US +
+ Midterm Elections +

+

+ Real-time forecasts from the Metaculus community on the 2026 US + midterm elections. +

+
+
+
+ ); +} diff --git a/front_end/src/app/og/midterms-2026/route/route.ts b/front_end/src/app/og/midterms-2026/route/route.ts new file mode 100644 index 0000000000..d24cff8080 --- /dev/null +++ b/front_end/src/app/og/midterms-2026/route/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { getPublicSettings } from "@/utils/public_settings.server"; + +export async function GET(req: NextRequest) { + const theme = req.nextUrl.searchParams.get("theme") ?? "dark"; + const { PUBLIC_APP_URL } = getPublicSettings(); + const pageUrl = `${PUBLIC_APP_URL}/og/midterms-2026?theme=${theme}&non-interactive=true`; + + const screenshotEndpoint = new URL( + "/api/screenshot/", + process.env.SCREENSHOT_SERVICE_API_URL + ).toString(); + + const payload = { + url: pageUrl, + selector: "#id-used-by-screenshot-donot-change", + selector_to_wait: "#id-logo-used-by-screenshot-donot-change", + width: 1200, + height: 630, + }; + + try { + const r = await fetch(screenshotEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + api_key: process.env.SCREENSHOT_SERVICE_API_KEY || "", + }, + body: JSON.stringify(payload), + }); + + if (!r.ok) { + const text = await r.text(); + return NextResponse.json({ error: text }, { status: r.status }); + } + + const buf = await r.arrayBuffer(); + return new NextResponse(buf, { + status: 200, + headers: { + "Content-Type": "image/png", + "Cache-Control": + process.env.NODE_ENV === "production" + ? "public, max-age=0, s-maxage=86400, stale-while-revalidate=3600" + : "no-store", + }, + }); + } catch { + return NextResponse.json({ error: "screenshot failed" }, { status: 500 }); + } +} From 3a30c9c684208de26b7e6115494da738cca87404 Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Tue, 5 May 2026 23:31:29 +0200 Subject: [PATCH 02/18] feat: wire midterms hub to real project 32840 data + Labor Hub design 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 --- front_end/bun.lock | 36 +++ front_end/declarations/react-simple-maps.d.ts | 53 ++++ front_end/global.d.ts | 1 + front_end/messages/cs.json | 32 +- front_end/messages/en.json | 32 +- front_end/messages/es.json | 32 +- front_end/messages/pt.json | 32 +- front_end/messages/zh-TW.json | 32 +- front_end/messages/zh.json | 32 +- front_end/package.json | 2 + front_end/public/us-states-10m.json | 1 + .../components/chamber_control_card.tsx | 69 +++-- .../midterms-2026/components/chamber_tabs.tsx | 57 ++-- .../components/congress_outcome_card.tsx | 125 +++----- .../components/consequence_row.tsx | 23 +- .../components/consumer_tile_client.tsx | 16 + .../components/distribution_curve.tsx | 64 ---- .../components/geographic_map.tsx | 283 ++++++++++++------ .../midterms-2026/components/insight_card.tsx | 68 +---- .../components/insights_carousel.tsx | 101 +++++-- .../midterms-2026/components/live_badge.tsx | 14 + .../midterms-2026/components/map_legend.tsx | 48 +++ .../components/responsive_map.tsx | 11 +- .../components/semicircle_gauge.tsx | 50 ---- .../components/state_tooltip.tsx | 47 +-- .../midterms-2026/components/tile_map.tsx | 54 +--- .../components/updated_badge.tsx | 51 ---- .../midterms-2026/components/watch_card.tsx | 95 ++++++ .../src/app/(main)/midterms-2026/constants.ts | 7 +- .../src/app/(main)/midterms-2026/data.ts | 78 +++-- .../helpers/fetch_community_insights.ts | 69 ++--- .../helpers/fetch_dashboard_data.ts | 53 ++-- .../midterms-2026/helpers/post_utils.ts | 57 +++- .../midterms-2026/helpers/state_color.ts | 19 +- .../src/app/(main)/midterms-2026/page.tsx | 18 +- .../sections/community_insights.tsx | 34 +-- .../sections/elections_map_section.tsx | 43 +-- .../sections/electoral_consequences.tsx | 31 +- .../(main)/midterms-2026/sections/footer.tsx | 10 +- .../(main)/midterms-2026/sections/hero.tsx | 42 +-- .../sections/things_to_watch.tsx | 132 ++------ 41 files changed, 1055 insertions(+), 999 deletions(-) create mode 100644 front_end/declarations/react-simple-maps.d.ts create mode 100644 front_end/public/us-states-10m.json create mode 100644 front_end/src/app/(main)/midterms-2026/components/consumer_tile_client.tsx delete mode 100644 front_end/src/app/(main)/midterms-2026/components/distribution_curve.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/live_badge.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/map_legend.tsx delete mode 100644 front_end/src/app/(main)/midterms-2026/components/semicircle_gauge.tsx delete mode 100644 front_end/src/app/(main)/midterms-2026/components/updated_badge.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/watch_card.tsx diff --git a/front_end/bun.lock b/front_end/bun.lock index 39e9674914..fa68fb66f7 100644 --- a/front_end/bun.lock +++ b/front_end/bun.lock @@ -89,6 +89,7 @@ "react-hook-form": "^7.52.1", "react-hot-toast": "^2.4.1", "react-merge-refs": "^2.1.1", + "react-simple-maps": "^3.0.0", "react-tweet": "^3.2.1", "remark": "^15.0.1", "sass": "^1.77.6", @@ -100,6 +101,7 @@ "ts-invariant": "^0.10.3", "ts-node": "^10.9.2", "undici": "^7.24.0", + "us-atlas": "^3.0.1", "victory": "^37.0.2", "wait-on": "^9.0.3", "whatwg-fetch": "^3.6.20", @@ -2998,6 +3000,8 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + "react-simple-maps": ["react-simple-maps@3.0.0", "", { "dependencies": { "d3-geo": "^2.0.2", "d3-selection": "^2.0.0", "d3-zoom": "^2.0.0", "topojson-client": "^3.1.0" }, "peerDependencies": { "prop-types": "^15.7.2", "react": "^16.8.0 || 17.x || 18.x", "react-dom": "^16.8.0 || 17.x || 18.x" } }, "sha512-vKNFrvpPG8Vyfdjnz5Ne1N56rZlDfHXv5THNXOVZMqbX1rWZA48zQuYT03mx6PAKanqarJu/PDLgshIZAfHHqw=="], + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "react-tweet": ["react-tweet@3.2.1", "", { "dependencies": { "@swc/helpers": "^0.5.3", "clsx": "^2.0.0", "swr": "^2.2.4" }, "peerDependencies": { "react": ">= 18.0.0", "react-dom": ">= 18.0.0" } }, "sha512-dktP3RMuwRB4pnSDocKpSsW5Hq1IXRW6fONkHhxT5EBIXsKZzdQuI70qtub1XN2dtZdkJWWxfBm/Q+kN+vRYFA=="], @@ -3300,6 +3304,8 @@ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "topojson-client": ["topojson-client@3.1.0", "", { "dependencies": { "commander": "2" }, "bin": { "topo2geo": "bin/topo2geo", "topomerge": "bin/topomerge", "topoquantize": "bin/topoquantize" } }, "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw=="], + "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], @@ -3392,6 +3398,8 @@ "urlpattern-polyfill": ["urlpattern-polyfill@10.0.0", "", {}, "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="], + "us-atlas": ["us-atlas@3.0.1", "", {}, "sha512-wEIZCq0ImPvGblTd8gZMqNNCPkQshugMUG/8nkSWXb02+XqNFREg9atHOKP9w6prLZTpqcLhSvdBW81MkV3/0Q=="], + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-intl": ["use-intl@4.9.1", "", { "dependencies": { "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", "icu-minify": "^4.9.1", "intl-messageformat": "^11.1.0" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" } }, "sha512-iGVV/xFYlhe3btafRlL8RPLD2Jsuet4yqn9DR6LWWbMhULsJnXgLonDkzDmsAIBIwFtk02oJuX/Ox2vwHKF+UQ=="], @@ -3956,6 +3964,12 @@ "react-docgen/strip-indent": ["strip-indent@4.1.1", "", {}, "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA=="], + "react-simple-maps/d3-geo": ["d3-geo@2.0.2", "", { "dependencies": { "d3-array": "^2.5.0" } }, "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA=="], + + "react-simple-maps/d3-selection": ["d3-selection@2.0.0", "", {}, "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA=="], + + "react-simple-maps/d3-zoom": ["d3-zoom@2.0.0", "", { "dependencies": { "d3-dispatch": "1 - 2", "d3-drag": "2", "d3-interpolate": "1 - 2", "d3-selection": "2", "d3-transition": "2" } }, "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw=="], + "react-tweet/@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], "read-cache/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], @@ -4006,6 +4020,8 @@ "tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "topojson-client/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "ts-node/arg": ["arg@4.1.3", "", {}, "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="], "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], @@ -4250,6 +4266,16 @@ "pm2-sysmonit/pidusage/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "react-simple-maps/d3-geo/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "react-simple-maps/d3-zoom/d3-dispatch": ["d3-dispatch@2.0.0", "", {}, "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA=="], + + "react-simple-maps/d3-zoom/d3-drag": ["d3-drag@2.0.0", "", { "dependencies": { "d3-dispatch": "1 - 2", "d3-selection": "2" } }, "sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w=="], + + "react-simple-maps/d3-zoom/d3-interpolate": ["d3-interpolate@2.0.1", "", { "dependencies": { "d3-color": "1 - 2" } }, "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ=="], + + "react-simple-maps/d3-zoom/d3-transition": ["d3-transition@2.0.0", "", { "dependencies": { "d3-color": "1 - 2", "d3-dispatch": "1 - 2", "d3-ease": "1 - 2", "d3-interpolate": "1 - 2", "d3-timer": "1 - 2" }, "peerDependencies": { "d3-selection": "2" } }, "sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog=="], + "sanitize-html/htmlparser2/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -4382,6 +4408,16 @@ "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "react-simple-maps/d3-geo/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "react-simple-maps/d3-zoom/d3-interpolate/d3-color": ["d3-color@2.0.0", "", {}, "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="], + + "react-simple-maps/d3-zoom/d3-transition/d3-color": ["d3-color@2.0.0", "", {}, "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="], + + "react-simple-maps/d3-zoom/d3-transition/d3-ease": ["d3-ease@2.0.0", "", {}, "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ=="], + + "react-simple-maps/d3-zoom/d3-transition/d3-timer": ["d3-timer@2.0.0", "", {}, "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA=="], + "wrap-ansi-cjs/ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], diff --git a/front_end/declarations/react-simple-maps.d.ts b/front_end/declarations/react-simple-maps.d.ts new file mode 100644 index 0000000000..cce05cf185 --- /dev/null +++ b/front_end/declarations/react-simple-maps.d.ts @@ -0,0 +1,53 @@ +declare module "react-simple-maps" { + import * as React from "react"; + + export interface ComposableMapProps extends React.SVGProps { + projection?: + | string + | ((opts: { width: number; height: number }) => unknown); + projectionConfig?: Record; + width?: number; + height?: number; + children?: React.ReactNode; + } + export const ComposableMap: React.FC; + + export interface GeographyType { + rsmKey: string; + id?: string | number; + type?: string; + properties?: Record; + geometry?: unknown; + } + + export interface GeographiesProps { + geography: string | object; + parseGeographies?: (geos: GeographyType[]) => GeographyType[]; + children: (props: { + geographies: GeographyType[]; + projection: unknown; + path: unknown; + }) => React.ReactNode; + } + export const Geographies: React.FC; + + export interface GeographyProps + extends Omit, "style"> { + geography: GeographyType; + style?: { + default?: React.CSSProperties; + hover?: React.CSSProperties; + pressed?: React.CSSProperties; + }; + onMouseEnter?: (e: React.MouseEvent) => void; + onMouseLeave?: (e: React.MouseEvent) => void; + onClick?: (e: React.MouseEvent) => void; + } + export const Geography: React.FC; + + export interface MarkerProps extends React.SVGProps { + coordinates: [number, number]; + children?: React.ReactNode; + } + export const Marker: React.FC; +} diff --git a/front_end/global.d.ts b/front_end/global.d.ts index ca8480c483..1bf0dbfcb3 100644 --- a/front_end/global.d.ts +++ b/front_end/global.d.ts @@ -15,3 +15,4 @@ declare module "@tanstack/table-core" { className: string; } } + diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index bf8320ff70..c5a6fd2524 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -2186,13 +2186,9 @@ "whyTrustMetaculusLessNoise": "Předpovědi Metaculus, poháněné davem, prořezávají šum tím, že zakládají každou předpověď na transparentních důkazech, odpovědném skóre a desetiletí doložené přesnosti. Metaculus vybavuje politiky, výzkumníky, novináře a korporátní organizace předpověďmi založenými na důkazech, které poskytují jasný, kvantifikovatelný vhled do nejkritičtějších nejistot světa. Prozkoumejte naši nabídku Business řešení a zjistěte, jak může Metaculus zlepšit rozhodování vaší organizace.", "publishTimeLockedDescription": "Čas publikace nelze po vytvoření změnit.", "thousandsOfOpenQuestions": "20 000+ otevřených otázek", - "midtermsHubPageTitle": "2026 US Midterm Elections", "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubUpdatedAgo": "Updated {time} ago", - "midtermsHubLastUpdated": "Last updated: {date}", "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubMapHeading": "2026 Elections Map", "midtermsHubChamberSenate": "Senate", "midtermsHubChamberHouse": "House", "midtermsHubChamberGovernor": "Governor", @@ -2208,12 +2204,7 @@ "midtermsHubVoterTurnout": "Voter turnout forecast", "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubIntegrityFactor1": "Active redistricting litigation", - "midtermsHubIntegrityFactor2": "Voting rule changes pending", - "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", - "midtermsHubChance": "Chance", "midtermsHubElectoralConsequences": "Electoral Consequences", "midtermsHubConsequenceQuestion": "Question", "midtermsHubConsequenceIfRep": "if Rep Congress", @@ -2223,25 +2214,22 @@ "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", - "midtermsHubViewAllQuestions": "View all questions", - "midtermsHubKeyFactor": "KEY FACTOR", - "midtermsHubTopComment": "TOP COMMENT", "midtermsHubScrollLeft": "Scroll insights left", "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubForecastersCount": "{count} forecasters", - "midtermsHubViewQuestion": "View full question", "midtermsHubDemocrat": "Democrat", "midtermsHubRepublican": "Republican", "midtermsHubNotContested": "Not contested", "midtermsHubDemPct": "{pct}% Democrat", "midtermsHubRepPct": "{pct}% Republican", "midtermsHubNoForecast": "No forecast", - "midtermsHubTierLikelyD": "Likely D", - "midtermsHubTierLeanD": "Lean D", - "midtermsHubTierTossUp": "Toss-up", - "midtermsHubTierLeanR": "Lean R", - "midtermsHubTierLikelyR": "Likely R", - "midtermsHubTierNotContested": "Not contested", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", + "midtermsHubHeroTitleLine1": "2026 US", + "midtermsHubHeroTitleLine2": "Midterm Elections", + "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", + "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", + "midtermsHubUpdatedRealtime": "Updated real-time", + "midtermsHubMetaculusUser": "Metaculus User", + "midtermsHubComingSoon": "Coming Soon", + "midtermsHubClickToView": "Click to view full question" } diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 7ab658e7f8..e25242cd68 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2175,13 +2175,9 @@ "whyTrustMetaculusLessNoise": "Metaculus's crowd-powered forecasts cut through the noise by grounding every prediction in transparent evidence, accountable scoring, and a decade of demonstrated accuracy. Metaculus equips policymakers, researchers, journalists, and corporate organizations with evidence-based forecasts that surface clear, quantifiable insight into the world's most critical uncertainties. Explore our suite of Business Solutions to learn more about how Metaculus can improve your organization's decision-making.", "publishTimeLockedDescription": "Publish time cannot be changed after creation.", "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)", - "midtermsHubPageTitle": "2026 US Midterm Elections", "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubUpdatedAgo": "Updated {time} ago", - "midtermsHubLastUpdated": "Last updated: {date}", "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubMapHeading": "2026 Elections Map", "midtermsHubChamberSenate": "Senate", "midtermsHubChamberHouse": "House", "midtermsHubChamberGovernor": "Governor", @@ -2197,12 +2193,7 @@ "midtermsHubVoterTurnout": "Voter turnout forecast", "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubIntegrityFactor1": "Active redistricting litigation", - "midtermsHubIntegrityFactor2": "Voting rule changes pending", - "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", - "midtermsHubChance": "Chance", "midtermsHubElectoralConsequences": "Electoral Consequences", "midtermsHubConsequenceQuestion": "Question", "midtermsHubConsequenceIfRep": "if Rep Congress", @@ -2212,25 +2203,22 @@ "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", - "midtermsHubViewAllQuestions": "View all questions", - "midtermsHubKeyFactor": "KEY FACTOR", - "midtermsHubTopComment": "TOP COMMENT", "midtermsHubScrollLeft": "Scroll insights left", "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubForecastersCount": "{count} forecasters", - "midtermsHubViewQuestion": "View full question", "midtermsHubDemocrat": "Democrat", "midtermsHubRepublican": "Republican", "midtermsHubNotContested": "Not contested", "midtermsHubDemPct": "{pct}% Democrat", "midtermsHubRepPct": "{pct}% Republican", "midtermsHubNoForecast": "No forecast", - "midtermsHubTierLikelyD": "Likely D", - "midtermsHubTierLeanD": "Lean D", - "midtermsHubTierTossUp": "Toss-up", - "midtermsHubTierLeanR": "Lean R", - "midtermsHubTierLikelyR": "Likely R", - "midtermsHubTierNotContested": "Not contested", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", + "midtermsHubHeroTitleLine1": "2026 US", + "midtermsHubHeroTitleLine2": "Midterm Elections", + "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", + "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", + "midtermsHubUpdatedRealtime": "Updated real-time", + "midtermsHubMetaculusUser": "Metaculus User", + "midtermsHubComingSoon": "Coming Soon", + "midtermsHubClickToView": "Click to view full question" } diff --git a/front_end/messages/es.json b/front_end/messages/es.json index 7586a4353d..d52a72ce75 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -2186,13 +2186,9 @@ "whyTrustMetaculusLessNoise": "Los pronósticos impulsados por la multitud de Metaculus cortan el ruido al basar cada predicción en evidencia transparente, puntuaciones responsables y una década de precisión demostrada. Metaculus equipa a los responsables de políticas, investigadores, periodistas y organizaciones corporativas con pronósticos basados en evidencia que ofrecen una visión clara y cuantificable de las incertidumbres más críticas del mundo. Explora nuestra suite de Soluciones Empresariales para aprender cómo Metaculus puede mejorar la toma de decisiones de tu organización.", "publishTimeLockedDescription": "La hora de publicación no puede cambiarse después de la creación.", "thousandsOfOpenQuestions": "20,000+ preguntas abiertas", - "midtermsHubPageTitle": "2026 US Midterm Elections", "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubUpdatedAgo": "Updated {time} ago", - "midtermsHubLastUpdated": "Last updated: {date}", "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubMapHeading": "2026 Elections Map", "midtermsHubChamberSenate": "Senate", "midtermsHubChamberHouse": "House", "midtermsHubChamberGovernor": "Governor", @@ -2208,12 +2204,7 @@ "midtermsHubVoterTurnout": "Voter turnout forecast", "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubIntegrityFactor1": "Active redistricting litigation", - "midtermsHubIntegrityFactor2": "Voting rule changes pending", - "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", - "midtermsHubChance": "Chance", "midtermsHubElectoralConsequences": "Electoral Consequences", "midtermsHubConsequenceQuestion": "Question", "midtermsHubConsequenceIfRep": "if Rep Congress", @@ -2223,25 +2214,22 @@ "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", - "midtermsHubViewAllQuestions": "View all questions", - "midtermsHubKeyFactor": "KEY FACTOR", - "midtermsHubTopComment": "TOP COMMENT", "midtermsHubScrollLeft": "Scroll insights left", "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubForecastersCount": "{count} forecasters", - "midtermsHubViewQuestion": "View full question", "midtermsHubDemocrat": "Democrat", "midtermsHubRepublican": "Republican", "midtermsHubNotContested": "Not contested", "midtermsHubDemPct": "{pct}% Democrat", "midtermsHubRepPct": "{pct}% Republican", "midtermsHubNoForecast": "No forecast", - "midtermsHubTierLikelyD": "Likely D", - "midtermsHubTierLeanD": "Lean D", - "midtermsHubTierTossUp": "Toss-up", - "midtermsHubTierLeanR": "Lean R", - "midtermsHubTierLikelyR": "Likely R", - "midtermsHubTierNotContested": "Not contested", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", + "midtermsHubHeroTitleLine1": "2026 US", + "midtermsHubHeroTitleLine2": "Midterm Elections", + "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", + "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", + "midtermsHubUpdatedRealtime": "Updated real-time", + "midtermsHubMetaculusUser": "Metaculus User", + "midtermsHubComingSoon": "Coming Soon", + "midtermsHubClickToView": "Click to view full question" } diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index 30747d48db..5214bd04f1 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -2184,13 +2184,9 @@ "whyTrustMetaculusLessNoise": "As previsões baseadas na multidão do Metaculus cortam o ruído ao basear cada previsão em evidências transparentes, pontuações responsáveis e uma década de precisão demonstrada. O Metaculus equipa formuladores de políticas, pesquisadores, jornalistas e organizações corporativas com previsões baseadas em evidências que oferecem insights claros e quantificáveis sobre as incertezas mais críticas do mundo. Explore nosso conjunto de Soluções Empresariais para saber mais sobre como o Metaculus pode melhorar a tomada de decisões da sua organização.", "publishTimeLockedDescription": "O horário de publicação não pode ser alterado após a criação.", "thousandsOfOpenQuestions": "20.000+ perguntas abertas", - "midtermsHubPageTitle": "2026 US Midterm Elections", "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubUpdatedAgo": "Updated {time} ago", - "midtermsHubLastUpdated": "Last updated: {date}", "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubMapHeading": "2026 Elections Map", "midtermsHubChamberSenate": "Senate", "midtermsHubChamberHouse": "House", "midtermsHubChamberGovernor": "Governor", @@ -2206,12 +2202,7 @@ "midtermsHubVoterTurnout": "Voter turnout forecast", "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubIntegrityFactor1": "Active redistricting litigation", - "midtermsHubIntegrityFactor2": "Voting rule changes pending", - "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", - "midtermsHubChance": "Chance", "midtermsHubElectoralConsequences": "Electoral Consequences", "midtermsHubConsequenceQuestion": "Question", "midtermsHubConsequenceIfRep": "if Rep Congress", @@ -2221,25 +2212,22 @@ "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", - "midtermsHubViewAllQuestions": "View all questions", - "midtermsHubKeyFactor": "KEY FACTOR", - "midtermsHubTopComment": "TOP COMMENT", "midtermsHubScrollLeft": "Scroll insights left", "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubForecastersCount": "{count} forecasters", - "midtermsHubViewQuestion": "View full question", "midtermsHubDemocrat": "Democrat", "midtermsHubRepublican": "Republican", "midtermsHubNotContested": "Not contested", "midtermsHubDemPct": "{pct}% Democrat", "midtermsHubRepPct": "{pct}% Republican", "midtermsHubNoForecast": "No forecast", - "midtermsHubTierLikelyD": "Likely D", - "midtermsHubTierLeanD": "Lean D", - "midtermsHubTierTossUp": "Toss-up", - "midtermsHubTierLeanR": "Lean R", - "midtermsHubTierLikelyR": "Likely R", - "midtermsHubTierNotContested": "Not contested", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", + "midtermsHubHeroTitleLine1": "2026 US", + "midtermsHubHeroTitleLine2": "Midterm Elections", + "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", + "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", + "midtermsHubUpdatedRealtime": "Updated real-time", + "midtermsHubMetaculusUser": "Metaculus User", + "midtermsHubComingSoon": "Coming Soon", + "midtermsHubClickToView": "Click to view full question" } diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index b8e620e60b..a37200f644 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -2183,13 +2183,9 @@ "whyTrustMetaculusLessNoise": "Metaculus 的群眾驅動預測通過將每一個預測植根於透明的證據、負責的計分和十年的已證準確性,抑制了噪音。Metaculus 為政策制定者、研究人員、記者和企業組織提供基於證據的預測,為全球最重要的不確定性提供清晰、可量化的洞察。探索我們的 企業解決方案,了解 Metaculus 如何改善貴組織的決策。", "publishTimeLockedDescription": "建立後將無法更改發佈時間。", "thousandsOfOpenQuestions": "20,000+ 開放問題", - "midtermsHubPageTitle": "2026 US Midterm Elections", "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubUpdatedAgo": "Updated {time} ago", - "midtermsHubLastUpdated": "Last updated: {date}", "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubMapHeading": "2026 Elections Map", "midtermsHubChamberSenate": "Senate", "midtermsHubChamberHouse": "House", "midtermsHubChamberGovernor": "Governor", @@ -2205,12 +2201,7 @@ "midtermsHubVoterTurnout": "Voter turnout forecast", "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubIntegrityFactor1": "Active redistricting litigation", - "midtermsHubIntegrityFactor2": "Voting rule changes pending", - "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", - "midtermsHubChance": "Chance", "midtermsHubElectoralConsequences": "Electoral Consequences", "midtermsHubConsequenceQuestion": "Question", "midtermsHubConsequenceIfRep": "if Rep Congress", @@ -2220,25 +2211,22 @@ "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", - "midtermsHubViewAllQuestions": "View all questions", - "midtermsHubKeyFactor": "KEY FACTOR", - "midtermsHubTopComment": "TOP COMMENT", "midtermsHubScrollLeft": "Scroll insights left", "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubForecastersCount": "{count} forecasters", - "midtermsHubViewQuestion": "View full question", "midtermsHubDemocrat": "Democrat", "midtermsHubRepublican": "Republican", "midtermsHubNotContested": "Not contested", "midtermsHubDemPct": "{pct}% Democrat", "midtermsHubRepPct": "{pct}% Republican", "midtermsHubNoForecast": "No forecast", - "midtermsHubTierLikelyD": "Likely D", - "midtermsHubTierLeanD": "Lean D", - "midtermsHubTierTossUp": "Toss-up", - "midtermsHubTierLeanR": "Lean R", - "midtermsHubTierLikelyR": "Likely R", - "midtermsHubTierNotContested": "Not contested", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", + "midtermsHubHeroTitleLine1": "2026 US", + "midtermsHubHeroTitleLine2": "Midterm Elections", + "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", + "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", + "midtermsHubUpdatedRealtime": "Updated real-time", + "midtermsHubMetaculusUser": "Metaculus User", + "midtermsHubComingSoon": "Coming Soon", + "midtermsHubClickToView": "Click to view full question" } diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index 39047dd448..577ad4509b 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -2188,13 +2188,9 @@ "whyTrustMetaculusLessNoise": "Metaculus 的众包预测通过以透明证据、可追溯的评分以及十年证明的准确性为基础来削减预测中的噪音。Metaculus 为政策制定者、研究人员、记者和企业组织提供基于证据的预测,为世界上最重要的不确定性提供清晰、可量化的洞察。了解我们的企业解决方案,了解 Metaculus 如何改善贵组织的决策。", "publishTimeLockedDescription": "创建后将无法更改发布时间。", "thousandsOfOpenQuestions": "20,000+ 开放问题", - "midtermsHubPageTitle": "2026 US Midterm Elections", "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubUpdatedAgo": "Updated {time} ago", - "midtermsHubLastUpdated": "Last updated: {date}", "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubMapHeading": "2026 Elections Map", "midtermsHubChamberSenate": "Senate", "midtermsHubChamberHouse": "House", "midtermsHubChamberGovernor": "Governor", @@ -2210,12 +2206,7 @@ "midtermsHubVoterTurnout": "Voter turnout forecast", "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityQuestion": "Will a court change the winner of a 2026 federal election?", "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubIntegrityFactor1": "Active redistricting litigation", - "midtermsHubIntegrityFactor2": "Voting rule changes pending", - "midtermsHubIntegrityFactor3": "Tight margins increase recount probability", - "midtermsHubChance": "Chance", "midtermsHubElectoralConsequences": "Electoral Consequences", "midtermsHubConsequenceQuestion": "Question", "midtermsHubConsequenceIfRep": "if Rep Congress", @@ -2225,25 +2216,22 @@ "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubInsightsSubtitle": "Key factors and top comments from forecasters", - "midtermsHubViewAllQuestions": "View all questions", - "midtermsHubKeyFactor": "KEY FACTOR", - "midtermsHubTopComment": "TOP COMMENT", "midtermsHubScrollLeft": "Scroll insights left", "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubForecastersCount": "{count} forecasters", - "midtermsHubViewQuestion": "View full question", "midtermsHubDemocrat": "Democrat", "midtermsHubRepublican": "Republican", "midtermsHubNotContested": "Not contested", "midtermsHubDemPct": "{pct}% Democrat", "midtermsHubRepPct": "{pct}% Republican", "midtermsHubNoForecast": "No forecast", - "midtermsHubTierLikelyD": "Likely D", - "midtermsHubTierLeanD": "Lean D", - "midtermsHubTierTossUp": "Toss-up", - "midtermsHubTierLeanR": "Lean R", - "midtermsHubTierLikelyR": "Likely R", - "midtermsHubTierNotContested": "Not contested", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party." + "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", + "midtermsHubHeroTitleLine1": "2026 US", + "midtermsHubHeroTitleLine2": "Midterm Elections", + "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", + "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", + "midtermsHubUpdatedRealtime": "Updated real-time", + "midtermsHubMetaculusUser": "Metaculus User", + "midtermsHubComingSoon": "Coming Soon", + "midtermsHubClickToView": "Click to view full question" } diff --git a/front_end/package.json b/front_end/package.json index defd0a9cdf..c4201724a4 100644 --- a/front_end/package.json +++ b/front_end/package.json @@ -107,6 +107,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-error-boundary": "^4.0.13", + "react-simple-maps": "^3.0.0", "react-hook-form": "^7.52.1", "react-hot-toast": "^2.4.1", "react-merge-refs": "^2.1.1", @@ -121,6 +122,7 @@ "ts-invariant": "^0.10.3", "ts-node": "^10.9.2", "undici": "^7.24.0", + "us-atlas": "^3.0.1", "victory": "^37.0.2", "wait-on": "^9.0.3", "whatwg-fetch": "^3.6.20", diff --git a/front_end/public/us-states-10m.json b/front_end/public/us-states-10m.json new file mode 100644 index 0000000000..7a18aa4dac --- /dev/null +++ b/front_end/public/us-states-10m.json @@ -0,0 +1 @@ +{"type":"Topology","bbox":[-179.14733999999999,-14.552548999999999,179.77847,71.352561],"transform":{"scale":[0.003589293992939929,0.0008590596905969058],"translate":[-179.14733999999999,-14.552548999999999]},"objects":{"states":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0]],[[1,2,3,4,5]]],"id":"01","properties":{"name":"Alabama"}},{"type":"MultiPolygon","arcs":[[[6]],[[7]],[[8]],[[9]],[[10]],[[11]],[[12]],[[13]],[[14]],[[15]],[[16]],[[17]],[[18]],[[19]],[[20]],[[21]],[[22]],[[23]],[[24]],[[25]],[[26]],[[27]],[[28]],[[29]],[[30]],[[31]],[[32]],[[33]],[[34]],[[35]],[[36]],[[37]],[[38]],[[39]],[[40]],[[41]],[[42]],[[43]],[[44]],[[45]],[[46]],[[47]],[[48]],[[49]],[[50]],[[51]],[[52]],[[53]],[[54]],[[55]],[[56]],[[57]],[[58]],[[59]],[[60]],[[61]],[[62]],[[63]],[[64]],[[65]],[[66]],[[67]],[[68]],[[69]],[[70]],[[71]],[[72]],[[73]],[[74]],[[75]],[[76]],[[77]],[[78]],[[79]],[[80]],[[81]],[[82]],[[83]],[[84]],[[85]],[[86]],[[87]],[[88]],[[89]],[[90]],[[91]],[[92]],[[93]],[[94]],[[95]],[[96]],[[97]],[[98]],[[99]],[[100]],[[101]],[[102]],[[103]],[[104]],[[105]],[[106]],[[107]],[[108]],[[109]],[[110]],[[111]],[[112]],[[113]],[[114]],[[115]],[[116]],[[117]],[[118]],[[119]],[[120]],[[121]],[[122]],[[123]],[[124]],[[125]],[[126]],[[127]],[[128]],[[129]],[[130]],[[131]],[[132]],[[133]],[[134]],[[135]],[[136]],[[137]],[[138]],[[139]],[[140]],[[141]],[[142]]],"id":"02","properties":{"name":"Alaska"}},{"type":"Polygon","arcs":[[143,144,145,146,147]],"id":"04","properties":{"name":"Arizona"}},{"type":"Polygon","arcs":[[148,149,150,151,152,153]],"id":"08","properties":{"name":"Colorado"}},{"type":"MultiPolygon","arcs":[[[154]],[[155]],[[156]],[[157]],[[158]],[[159]],[[160]],[[161]],[[162]],[[163,164,-4]]],"id":"12","properties":{"name":"Florida"}},{"type":"Polygon","arcs":[[165,166,167,168,-164,-3]],"id":"13","properties":{"name":"Georgia"}},{"type":"Polygon","arcs":[[169,170,171,172,173]],"id":"18","properties":{"name":"Indiana"}},{"type":"Polygon","arcs":[[174,175,176,-151]],"id":"20","properties":{"name":"Kansas"}},{"type":"MultiPolygon","arcs":[[[177]],[[178]],[[179]],[[180]],[[181]],[[182]],[[183]],[[184,185]]],"id":"23","properties":{"name":"Maine"}},{"type":"MultiPolygon","arcs":[[[186]],[[187]],[[188,189,190,191,192,193,194,195]]],"id":"25","properties":{"name":"Massachusetts"}},{"type":"Polygon","arcs":[[196,197,198,199,200]],"id":"27","properties":{"name":"Minnesota"}},{"type":"Polygon","arcs":[[201,202,203,204,205,206,207,208]],"id":"34","properties":{"name":"New Jersey"}},{"type":"MultiPolygon","arcs":[[[209]],[[210]],[[211,212,213,-167,214]]],"id":"37","properties":{"name":"North Carolina"}},{"type":"Polygon","arcs":[[215,-201,216,217]],"id":"38","properties":{"name":"North Dakota"}},{"type":"Polygon","arcs":[[-152,-177,218,219,220,221]],"id":"40","properties":{"name":"Oklahoma"}},{"type":"Polygon","arcs":[[222,223,-204,224,225,226,227]],"id":"42","properties":{"name":"Pennsylvania"}},{"type":"Polygon","arcs":[[228,-217,-200,229,230,231]],"id":"46","properties":{"name":"South Dakota"}},{"type":"Polygon","arcs":[[-221,232,233,234,235,236,237]],"id":"48","properties":{"name":"Texas"}},{"type":"Polygon","arcs":[[-232,238,-149,239,240,241]],"id":"56","properties":{"name":"Wyoming"}},{"type":"Polygon","arcs":[[-195,242,243,244]],"id":"09","properties":{"name":"Connecticut"}},{"type":"Polygon","arcs":[[245,246,247,248,249,250,251,-219,-176,252]],"id":"29","properties":{"name":"Missouri"}},{"type":"Polygon","arcs":[[253,-227,254,255,256]],"id":"54","properties":{"name":"West Virginia"}},{"type":"Polygon","arcs":[[257,258,259,-174,260,-247]],"id":"17","properties":{"name":"Illinois"}},{"type":"Polygon","arcs":[[-153,-222,-238,261,-147]],"id":"35","properties":{"name":"New Mexico"}},{"type":"Polygon","arcs":[[-252,262,263,264,-235,233,-233,-220]],"id":"05","properties":{"name":"Arkansas"}},{"type":"MultiPolygon","arcs":[[[265]],[[266]],[[267]],[[268]],[[269]],[[270]],[[271]],[[272]],[[273,274,275,-144,276]]],"id":"06","properties":{"name":"California"}},{"type":"MultiPolygon","arcs":[[[-209,277]],[[-225,-203,278,279]]],"id":"10","properties":{"name":"Delaware"}},{"type":"Polygon","arcs":[[280,281]],"id":"11","properties":{"name":"District of Columbia"}},{"type":"MultiPolygon","arcs":[[[282]],[[283]],[[284]],[[285]],[[286]],[[287]],[[288]],[[289]]],"id":"15","properties":{"name":"Hawaii"}},{"type":"Polygon","arcs":[[-199,290,-258,-246,291,-230]],"id":"19","properties":{"name":"Iowa"}},{"type":"MultiPolygon","arcs":[[[-261,-173,292,-257,293,294,-248]],[[295,-250]]],"id":"21","properties":{"name":"Kentucky"}},{"type":"MultiPolygon","arcs":[[[296,297]],[[298]],[[299]],[[-226,-280,300,301,302,303,-281,304,-255]]],"id":"24","properties":{"name":"Maryland"}},{"type":"MultiPolygon","arcs":[[[305]],[[306]],[[307]],[[308]],[[309]],[[310]],[[311]],[[312]],[[313]],[[314,315,-171]],[[316]],[[317,318,319,320,321,322]]],"id":"26","properties":{"name":"Michigan"}},{"type":"MultiPolygon","arcs":[[[323]],[[324]],[[325]],[[326]],[[-264,327,-6,328,329]]],"id":"28","properties":{"name":"Mississippi"}},{"type":"Polygon","arcs":[[330,-218,-229,-242,331]],"id":"30","properties":{"name":"Montana"}},{"type":"Polygon","arcs":[[332,-185,333,-190,334]],"id":"33","properties":{"name":"New Hampshire"}},{"type":"MultiPolygon","arcs":[[[335]],[[336]],[[337]],[[338,-207]],[[339]],[[340]],[[341,342,-196,-245,343,-205,-224]]],"id":"36","properties":{"name":"New York"}},{"type":"MultiPolygon","arcs":[[[344]],[[345]],[[-316,346,-228,-254,-293,-172]]],"id":"39","properties":{"name":"Ohio"}},{"type":"Polygon","arcs":[[347,348,349,274,-275,-274,350]],"id":"41","properties":{"name":"Oregon"}},{"type":"Polygon","arcs":[[-251,-296,-249,-295,351,-215,-166,-2,-328,-263]],"id":"47","properties":{"name":"Tennessee"}},{"type":"Polygon","arcs":[[352,-240,-154,-146,353]],"id":"49","properties":{"name":"Utah"}},{"type":"MultiPolygon","arcs":[[[354]],[[-302,355]],[[356,-297]],[[-256,-305,-282,-304,357,-212,-352,-294]]],"id":"51","properties":{"name":"Virginia"}},{"type":"MultiPolygon","arcs":[[[358]],[[359]],[[360]],[[361]],[[362]],[[363]],[[364]],[[365]],[[366]],[[367]],[[368,-348,369]]],"id":"53","properties":{"name":"Washington"}},{"type":"MultiPolygon","arcs":[[[370]],[[371]],[[372]],[[373]],[[374]],[[375]],[[376]],[[377]],[[378]],[[379]],[[380,-323,321,-321,319,-319,381,-259,-291,-198]]],"id":"55","properties":{"name":"Wisconsin"}},{"type":"MultiPolygon","arcs":[[[382]],[[383]],[[384]]],"id":"60","properties":{"name":"American Samoa"}},{"type":"MultiPolygon","arcs":[[[385]]],"id":"66","properties":{"name":"Guam"}},{"type":"MultiPolygon","arcs":[[[386]],[[387]],[[388]],[[389]],[[390]],[[391]],[[392]],[[393]]],"id":"69","properties":{"name":"Commonwealth of the Northern Mariana Islands"}},{"type":"Polygon","arcs":[[-231,-292,-253,-175,-150,-239]],"id":"31","properties":{"name":"Nebraska"}},{"type":"Polygon","arcs":[[-214,394,-168]],"id":"45","properties":{"name":"South Carolina"}},{"type":"MultiPolygon","arcs":[[[395]],[[396]],[[397]],[[398]],[[399]]],"id":"72","properties":{"name":"Puerto Rico"}},{"type":"MultiPolygon","arcs":[[[400]],[[401]],[[402]]],"id":"78","properties":{"name":"United States Virgin Islands"}},{"type":"Polygon","arcs":[[-369,403,-332,-241,-353,404,-349]],"id":"16","properties":{"name":"Idaho"}},{"type":"Polygon","arcs":[[-275,-350,-405,-354,-145,-276]],"id":"32","properties":{"name":"Nevada"}},{"type":"Polygon","arcs":[[405,-335,-189,-343]],"id":"50","properties":{"name":"Vermont"}},{"type":"MultiPolygon","arcs":[[[406]],[[407]],[[408]],[[409]],[[410]],[[411]],[[412]],[[-265,-330,413,-236]]],"id":"22","properties":{"name":"Louisiana"}},{"type":"MultiPolygon","arcs":[[[-192,414]],[[415]],[[416]],[[417]],[[-243,-194,418]]],"id":"44","properties":{"name":"Rhode Island"}}]},"nation":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0]],[[164,4,328,413,236,261,147,276,350,369,403,330,215,196,380,317,381,259,169,314,346,222,341,405,332,185,333,190,414,192,418,243,343,205,338,207,277,201,278,300,355,302,357,212,394,168]],[[6]],[[7]],[[8]],[[9]],[[10]],[[11]],[[12]],[[13]],[[14]],[[15]],[[16]],[[17]],[[18]],[[19]],[[20]],[[21]],[[22]],[[23]],[[24]],[[25]],[[26]],[[27]],[[28]],[[29]],[[30]],[[31]],[[32]],[[33]],[[34]],[[35]],[[36]],[[37]],[[38]],[[39]],[[40]],[[41]],[[42]],[[43]],[[44]],[[45]],[[46]],[[47]],[[48]],[[49]],[[50]],[[51]],[[52]],[[53]],[[54]],[[55]],[[56]],[[57]],[[58]],[[59]],[[60]],[[61]],[[62]],[[63]],[[64]],[[65]],[[66]],[[67]],[[68]],[[69]],[[70]],[[71]],[[72]],[[73]],[[74]],[[75]],[[76]],[[77]],[[78]],[[79]],[[80]],[[81]],[[82]],[[83]],[[84]],[[85]],[[86]],[[87]],[[88]],[[89]],[[90]],[[91]],[[92]],[[93]],[[94]],[[95]],[[96]],[[97]],[[98]],[[99]],[[100]],[[101]],[[102]],[[103]],[[104]],[[105]],[[106]],[[107]],[[108]],[[109]],[[110]],[[111]],[[112]],[[113]],[[114]],[[115]],[[116]],[[117]],[[118]],[[119]],[[120]],[[121]],[[122]],[[123]],[[124]],[[125]],[[126]],[[127]],[[128]],[[129]],[[130]],[[131]],[[132]],[[133]],[[134]],[[135]],[[136]],[[137]],[[138]],[[139]],[[140]],[[141]],[[142]],[[154]],[[155]],[[156]],[[157]],[[158]],[[159]],[[160]],[[161]],[[162]],[[177]],[[178]],[[179]],[[180]],[[181]],[[182]],[[183]],[[186]],[[187]],[[209]],[[210]],[[265]],[[266]],[[267]],[[268]],[[269]],[[270]],[[271]],[[272]],[[282]],[[283]],[[284]],[[285]],[[286]],[[287]],[[288]],[[289]],[[297,356]],[[298]],[[299]],[[305]],[[306]],[[307]],[[308]],[[309]],[[310]],[[311]],[[312]],[[313]],[[316]],[[323]],[[324]],[[325]],[[326]],[[335]],[[336]],[[337]],[[339]],[[340]],[[344]],[[345]],[[354]],[[358]],[[359]],[[360]],[[361]],[[362]],[[363]],[[364]],[[365]],[[366]],[[367]],[[370]],[[371]],[[372]],[[373]],[[374]],[[375]],[[376]],[[377]],[[378]],[[379]],[[382]],[[383]],[[384]],[[385]],[[386]],[[387]],[[388]],[[389]],[[390]],[[391]],[[392]],[[393]],[[395]],[[396]],[[397]],[[398]],[[399]],[[400]],[[401]],[[402]],[[406]],[[407]],[[408]],[[409]],[[410]],[[411]],[[412]],[[415]],[[416]],[[417]]]}]}},"arcs":[[[25302,52136],[56,31],[1,25],[14,-40],[-9,-28],[-10,16],[-18,-2],[-20,-17],[-14,15]],[[25338,57677],[0,15],[150,-6],[63,0],[160,-13],[93,-2],[37,2],[159,-4],[61,-5]],[[26061,57664],[15,-321],[23,-443],[18,-379],[13,-294],[15,-311],[33,-713],[9,-32],[-4,-36],[13,-45],[5,-149],[10,-72],[17,-69],[3,-82],[9,-36],[-10,-117],[18,-28],[12,-40],[-10,-52],[-11,0],[2,-24],[-14,-21],[-13,-52],[3,-56],[-2,-86],[-7,-86],[-14,-72],[-3,-45],[6,-124],[-2,-44],[19,-87],[3,-120],[-7,-57],[2,-43],[-8,-80],[1,-79],[-7,-21],[5,-51],[-3,-54],[20,-91],[7,-64],[2,-61]],[[26229,53027],[-67,0],[-71,-5],[-110,-4],[-174,0],[-135,7],[-166,-2],[3,-38],[-13,-115],[2,-23],[23,-79],[3,-41],[36,-80],[3,-69],[-15,-102],[3,-55],[20,-51],[-18,-35],[-8,-82],[-9,-15],[7,-24],[-15,-26]],[[25528,52188],[-38,-35],[-40,-24],[-56,-4],[18,41],[12,-26],[35,27],[3,34],[-15,48],[-7,43],[-20,47],[-7,90],[9,74],[-3,76],[-7,48],[-20,32],[-15,-46],[2,-38],[-9,-57],[1,-41],[-6,-32],[-1,-144],[-8,-66],[-15,-4],[1,36],[-20,40],[-15,-16],[-5,23],[-18,-22]],[[25284,52292],[-3,222],[-3,357],[-6,423],[-10,773],[9,283],[21,710],[5,212],[33,1120],[6,160],[19,643],[12,362],[-16,35],[-13,85]],[[8053,88108],[6,38],[18,-11],[-20,-43],[-4,16]],[[7905,86007],[29,90],[15,-17],[-1,-46],[-28,-74],[-15,19],[0,28]],[[7695,85792],[26,-14],[-3,-36],[-23,50]],[[7642,85774],[22,-17],[-7,-33],[-14,14],[-1,36]],[[7602,85813],[20,-2],[-5,-34],[-15,36]],[[7594,84711],[8,45],[20,-13],[-8,-73],[-15,-17],[-5,58]],[[7543,85532],[12,28],[21,-40],[-33,-2],[0,14]],[[7540,87187],[29,79],[8,112],[31,-29],[-28,-64],[-9,-49],[5,-24],[-36,-25]],[[7464,85521],[14,55],[18,-28],[23,-4],[-33,-33],[-22,10]],[[7385,86976],[17,6],[5,-88],[-22,82]],[[7168,84530],[29,89],[15,6],[26,-66],[5,20],[-21,65],[6,54],[9,10],[27,-25],[21,19],[-29,54],[16,56],[29,-31],[15,4],[-14,56],[12,17],[20,-20],[14,45],[-18,6],[-13,33],[20,3],[22,67],[34,18],[-12,35],[-3,68],[27,66],[7,-31],[53,51],[7,-7],[-12,-118],[-13,-18],[-26,-103],[5,-84],[36,11],[2,69],[22,-9],[21,-71],[20,48],[12,-33],[-17,-117],[9,-22],[9,72],[27,47],[7,-25],[-3,-115],[-30,-89],[-32,23],[-11,81],[-21,-26],[13,-99],[-22,-20],[-39,13],[-16,-54],[-8,36],[2,74],[-10,2],[-9,-114],[-8,-24],[-31,-17],[2,-37],[-16,-22],[-45,-13],[-86,76],[-21,-13],[-15,29]],[[7129,86075],[41,28],[18,-43],[-5,-48],[-13,-16],[-29,-3],[-16,59],[4,23]],[[6962,82933],[31,45],[8,-48],[-37,-9],[-2,12]],[[6905,82759],[20,62],[19,20],[11,-11],[25,17],[5,-40],[19,-37],[36,16],[-2,-37],[-19,-31],[-46,-6],[-32,-13],[-32,25],[-4,35]],[[6789,83718],[20,17],[4,76],[18,75],[25,34],[5,44],[15,-8],[34,70],[33,36],[39,-15],[29,1],[0,-112],[26,-57],[4,45],[17,53],[-18,48],[6,25],[53,-14],[-3,35],[-52,44],[-19,-4],[-1,127],[31,61],[29,29],[20,-12],[22,-56],[6,-133],[15,13],[9,77],[-5,55],[35,-38],[7,47],[-37,36],[-21,58],[7,44],[17,-6],[51,-87],[9,4],[30,-41],[11,10],[-31,75],[-20,34],[-7,44],[17,0],[30,-58],[29,12],[41,-29],[7,49],[36,15],[-8,-63],[-39,-106],[-7,-83],[19,-36],[0,95],[16,43],[16,-49],[5,46],[17,32],[5,41],[14,10],[10,-31],[22,72],[17,8],[-4,-45],[28,-16],[-11,-38],[-11,20],[-18,-15],[19,-36],[-5,-49],[19,22],[11,-51],[27,1],[-24,-53],[-16,33],[-24,2],[-15,-48],[16,-9],[-8,-53],[26,-8],[-26,-89],[23,17],[13,47],[5,-35],[47,-4],[-3,-41],[-36,-78],[-13,-110],[-38,13],[-2,39],[-10,-41],[-23,42],[-17,-6],[-23,49],[-15,-13],[-24,19],[-8,-46],[27,1],[15,-17],[45,-87],[-8,-70],[-23,-52],[-11,36],[-19,-50],[-19,30],[-6,38],[-21,18],[-20,-12],[-17,-37],[29,2],[20,-49],[-38,-53],[-34,12],[-4,-20],[25,-34],[14,13],[35,1],[22,-43],[-11,-28],[-24,-8],[-34,-33],[-8,11],[-19,-29],[3,-49],[-27,-50],[-13,20],[7,34],[-23,50],[7,47],[27,62],[-8,24],[-15,-21],[-34,-105],[-3,-24],[-22,32],[-22,-10],[-5,-35],[26,-6],[10,-63],[-16,-61],[-25,-20],[1,-70],[-25,-41],[-12,13],[-23,-82],[-19,-30],[-18,21],[-31,-20],[22,116],[11,11],[26,71],[24,31],[-2,37],[-33,-23],[3,52],[22,102],[29,52],[-7,30],[-15,-45],[-34,-61],[-26,-112],[-24,-59],[-14,-11],[-5,-44],[-19,-30],[-4,84],[-26,58],[-33,27],[3,100],[-4,106],[-13,83],[-11,32],[-23,20],[-27,5],[14,30],[-15,37],[5,23]],[[6782,82633],[27,101],[51,97],[21,-4],[16,-54],[-12,-23],[-55,-73],[-29,-79],[-19,35]],[[6519,81925],[26,41],[5,38],[13,22],[8,-32],[-6,-43],[5,-68],[-7,-33],[-36,13],[-8,62]],[[6240,82171],[5,47],[14,25],[13,-59],[-13,-63],[-19,50]],[[6225,82357],[9,35],[3,-82],[-10,-1],[-2,48]],[[6080,82757],[21,48],[48,-3],[25,-54],[-35,23],[-16,-26],[-43,-6],[0,18]],[[5919,82533],[19,25],[11,-57],[-9,-11],[-21,43]],[[5642,81925],[8,64],[19,12],[27,-58],[-8,-18],[-21,37],[-25,-37]],[[5573,81984],[16,-49],[-27,12],[11,37]],[[5524,80844],[-1,36],[13,24],[20,-25],[-9,-59],[-19,-15],[-4,39]],[[5506,81836],[8,67],[10,10],[11,-62],[-29,-15]],[[5485,80904],[12,85],[-11,46],[31,-2],[4,-99],[-27,-38],[-9,8]],[[5445,80747],[10,13],[16,-42],[-1,-34],[-23,-4],[-2,67]],[[5429,81185],[17,-21],[5,49],[17,46],[5,-22],[-8,-58],[9,-13],[1,-44],[-10,-61],[13,-30],[-7,-19],[-17,22],[-2,-21],[-20,8],[-3,164]],[[5385,80755],[21,25],[11,-30],[-20,-26],[-12,31]],[[5377,81028],[9,39],[23,41],[5,-36],[-23,-38],[-9,-58],[-5,52]],[[5303,81352],[14,11],[5,-42],[-19,31]],[[5265,80882],[33,98],[15,27],[-5,18],[-22,0],[-4,76],[22,50],[27,-44],[-1,38],[-13,52],[30,-13],[18,114],[12,-23],[-2,-43],[-9,-2],[-5,-75],[12,-18],[0,33],[13,1],[-4,-60],[-11,-37],[-22,12],[-1,-45],[-28,-71],[-18,-24],[-31,-141],[-6,77]],[[5246,85264],[5,41],[14,-83],[-7,-25],[-12,67]],[[5238,81460],[22,42],[36,-14],[-1,-78],[-50,12],[-7,38]],[[5213,85252],[8,77],[5,-57],[-13,-20]],[[5096,81334],[16,74],[25,25],[18,-23],[-2,-46],[10,5],[12,47],[15,-32],[26,-26],[26,25],[7,-66],[-9,-60],[-14,40],[-22,3],[-14,23],[-3,40],[-10,-12],[0,-69],[11,-20],[11,-87],[-11,-60],[-36,35],[-10,59],[-18,-18],[-18,-90],[4,58],[-10,43],[0,104],[-4,28]],[[5033,85142],[7,131],[39,52],[21,6],[39,76],[5,-2],[-55,-273],[-22,-32],[-32,-4],[-2,46]],[[4937,81216],[27,3],[-4,-71],[-26,47],[3,21]],[[4863,81217],[10,31],[36,8],[-9,-51],[-37,-10],[0,22]],[[4804,81100],[4,51],[17,20],[25,-19],[12,-29],[35,-42],[6,-41],[-30,44],[-23,-57],[-28,74],[-11,-38],[-7,37]],[[4656,80881],[30,66],[26,-27],[0,-83],[-11,-53],[-13,-21],[-27,58],[-5,60]],[[4587,90956],[19,44],[21,16],[24,-17],[22,6],[9,-37],[-1,-50],[-75,-13],[-18,19],[-1,32]],[[4545,80324],[1,51],[68,-54],[19,-39],[26,0],[18,-28],[-20,-23],[-10,-33],[-22,40],[-17,-9],[-31,37],[-18,-29],[-22,57],[8,30]],[[4451,81463],[14,-1],[-5,-38],[-9,39]],[[3979,80040],[12,22],[16,-18],[-21,-37],[-7,33]],[[3880,79918],[22,34],[33,-11],[24,18],[5,-24],[-9,-45],[-26,-13],[-47,27],[-2,14]],[[3807,79887],[60,24],[9,-35],[-15,-32],[-13,37],[-34,-4],[-7,10]],[[3781,79848],[17,29],[7,-36],[-18,-14],[-6,21]],[[3751,80090],[13,56],[14,-15],[18,18],[12,-21],[-22,-33],[13,-48],[30,0],[1,-41],[-23,3],[-18,-79],[-23,24],[-2,78],[16,35],[-9,20],[-16,-21],[-4,24]],[[3635,79967],[5,36],[28,54],[30,-6],[3,-58],[20,6],[14,-15],[8,-44],[13,13],[-2,-41],[-26,-36],[-12,11],[-18,-45],[-6,28],[-25,1],[-17,-20],[-18,91],[3,25]],[[3608,79782],[7,15],[27,-33],[-32,-11],[-2,29]],[[3261,87028],[35,25],[57,0],[22,-20],[23,10],[9,-24],[21,10],[10,30],[-8,38],[34,68],[37,-11],[0,37],[25,46],[33,-40],[24,32],[23,9],[15,54],[19,-132],[27,-10],[29,34],[29,-20],[27,-40],[-12,-65],[11,-44],[-11,-40],[13,-11],[4,-65],[-13,-38],[21,-54],[2,-58],[21,10],[-11,-34],[2,-48],[-71,-21],[-6,-34],[-20,19],[-26,-18],[-28,-58],[8,-64],[-26,-8],[-23,71],[-38,47],[-57,-4],[-14,35],[-28,18],[-27,58],[-35,42],[-40,19],[-32,72],[-25,10],[-2,70],[-28,97]],[[3147,79003],[17,23],[22,53],[25,5],[37,40],[9,27],[28,-21],[14,19],[8,44],[22,-19],[3,72],[24,-11],[-17,106],[20,32],[8,-16],[15,18],[-18,35],[4,39],[17,23],[29,-1],[9,-68],[15,7],[-7,50],[13,31],[-53,47],[-14,-21],[-32,81],[0,46],[31,92],[42,50],[36,32],[6,-15],[23,13],[16,-63],[-16,-42],[7,-35],[17,-18],[14,53],[6,-33],[15,29],[-1,41],[18,63],[15,-58],[11,26],[20,-62],[-8,-56],[-23,-13],[-24,-70],[-40,-70],[2,-39],[34,54],[58,64],[4,22],[22,22],[7,-19],[-5,-72],[-16,-51],[-26,-34],[-6,-23],[-45,-51],[-30,-20],[12,-46],[-16,-1],[-4,-61],[-16,17],[-7,-70],[-20,25],[-4,-76],[-36,-13],[-22,30],[-10,-31],[-12,23],[-21,-37],[-6,11],[-44,-73],[-3,-31],[-23,4],[-16,-23],[-13,-60],[-28,23],[-9,-45],[-49,40],[-15,36]],[[3070,93368],[7,34],[34,50],[123,108],[63,74],[48,74],[45,56],[42,64],[84,105],[55,56],[146,167],[172,154],[133,90],[87,45],[135,15],[52,-11],[34,-31],[-41,-11],[7,-38],[-10,-71],[-31,-77],[1,-70],[12,-56],[-21,-59],[-49,-33],[-4,-20],[49,7],[42,-151],[40,-3],[36,32],[42,-1],[49,-30],[47,21],[69,15],[32,-58],[50,21],[18,-34],[52,32],[18,26],[84,-65],[18,59],[26,44],[37,150],[18,26],[40,-9],[8,-39],[32,-12],[31,27],[26,0],[-32,110],[-71,55],[-45,24],[-48,-1],[-60,-60],[13,112],[-3,83],[-62,114],[-22,91],[-26,35],[-59,14],[-8,56],[-32,90],[7,50],[27,18],[10,43],[16,-37],[24,28],[27,-89],[36,-91],[24,-10],[-19,-101],[3,-55],[28,-50],[57,-123],[53,-68],[39,18],[30,28],[9,49],[-45,2],[-10,45],[-56,70],[-53,112],[10,52],[0,60],[14,23],[1,57],[33,93],[30,-32],[22,3],[1,28],[-38,55],[-25,-8],[-28,57],[-87,-29],[-32,-37],[-34,0],[-39,-19],[-34,17],[-20,43],[-17,-16],[-21,23],[-4,-51],[-36,39],[-91,42],[-54,31],[-48,15],[-22,30],[1,98],[-22,163],[-52,216],[-20,58],[-36,62],[-91,101],[-145,228],[-21,24],[-51,87],[-50,49],[-54,37],[-51,23],[-29,35],[-19,70],[-54,84],[-45,40],[-81,41],[-42,-6],[37,40],[92,59],[20,46],[3,57],[19,76],[11,236],[-8,117],[179,-31],[68,6],[144,42],[79,28],[77,15],[77,63],[41,65],[29,30],[53,87],[81,192],[21,92],[7,79],[-2,186],[39,233],[84,183],[65,179],[37,77],[129,162],[96,-35],[69,-3],[68,40],[64,53],[93,104],[74,108],[44,86],[114,188],[33,31],[99,63],[7,-47],[55,-44],[82,-12],[68,15],[13,24],[32,-3],[61,17],[73,51],[74,85],[71,121],[36,88],[86,185],[68,76],[12,-68],[47,-38],[39,-2],[39,-20],[16,-82],[27,41],[99,-49],[15,-98],[-54,-71],[-35,-63],[-41,-4],[12,-146],[81,-15],[44,59],[-7,76],[16,8],[31,67],[23,12],[19,-48],[4,63],[-27,43],[35,55],[21,-47],[-3,65],[15,22],[71,-74],[45,-66],[13,-42],[-11,-33],[1,-109],[9,-49],[61,10],[35,-37],[16,-40],[48,62],[30,75],[112,0],[69,42],[53,-11],[40,-34],[84,-2],[61,-32],[45,-35],[9,-34],[-26,-11],[-26,-83],[-26,-30],[14,-93],[25,5],[19,-24],[45,-6],[10,-28],[69,-20],[33,9],[-17,-43],[9,-25],[-53,-26],[-8,-24],[20,-25],[89,6],[72,-41],[14,-33],[28,5],[44,86],[57,15],[24,-18],[10,34],[40,7],[29,-27],[15,-59],[42,27],[38,8],[58,83],[13,-22],[45,20],[21,-15],[34,26],[12,-30],[66,-8],[62,-72],[28,-1],[19,-24],[26,31],[24,-8],[37,-73],[0,-45],[35,-19],[41,51],[1,-63],[26,57],[81,-77],[14,-67],[24,-23],[49,1],[43,-15],[29,36],[23,-73],[48,-10],[29,45],[43,-12],[62,12],[66,-10],[49,-38],[50,-3],[17,28],[65,-95],[28,-8],[32,-54],[45,-16],[20,-30],[45,-12],[17,-26],[27,32],[66,-20],[45,77],[48,6],[9,20],[84,49],[18,24],[86,30],[42,-25],[48,32],[9,-18],[79,-73],[53,-37],[70,-80],[26,-67],[37,-11],[87,-97],[68,-40],[52,-62],[27,-47],[61,-13],[58,-45],[0,-9012],[2,-971],[-1,-889],[130,-95],[17,100],[135,-145],[81,180],[170,20],[1,-39],[-33,-272],[47,-112],[66,-76],[26,-22],[11,-116],[29,-80],[266,-580],[30,-299],[-8,-93],[22,4],[51,108],[111,158],[11,23],[68,8],[32,139],[-2,209],[30,-19],[18,19],[19,71],[-1,39],[-32,47],[45,48],[68,28],[131,158],[62,-115],[28,-88],[11,-2],[24,-70],[0,-104],[-12,-24],[1,-37],[16,-47],[-5,-42],[18,-76],[73,-38],[5,-66],[32,-73],[24,0],[29,-108],[-6,-69],[26,-20],[-6,-46],[22,-71],[114,-152],[32,-119],[46,-120],[50,-110],[-23,-49],[33,-134],[47,-140],[28,-176],[57,-182],[32,-161],[30,-111],[25,-123],[53,-180],[32,-155],[-33,-140],[89,-52],[-21,-205],[71,-81],[-8,-61],[11,-57],[7,-119],[71,14],[33,-77],[82,-115],[23,-48],[85,-47],[44,-115],[44,-33],[11,-116],[23,-15],[27,-36],[40,23],[28,-143],[-3,-90],[-20,-107],[-18,-67],[1,-60],[10,-38],[-5,-118],[12,-104],[11,-46],[6,-133],[12,-63],[-32,-100],[-24,-122],[-1,-33],[-20,-90],[-23,-77],[-37,-97],[-27,-55],[-19,-14],[3,-46],[-19,-23],[-12,40],[-1,56],[-13,24],[-1,-44],[-12,-22],[-18,18],[-13,52],[-4,108],[3,57],[-17,33],[8,103],[-9,6],[-16,56],[-6,64],[30,63],[17,63],[15,-8],[-3,75],[-11,82],[14,122],[-9,190],[-10,67],[-43,164],[-22,55],[-12,48],[-7,-34],[23,-62],[25,-85],[7,-72],[22,-90],[10,-131],[-16,-43],[-2,-79],[7,-92],[11,-62],[-9,-28],[-11,96],[-8,12],[3,-70],[-4,-74],[-36,-102],[-14,-5],[-24,49],[19,73],[11,73],[-3,38],[-25,-10],[10,-72],[-8,-42],[-33,-56],[-7,1],[-23,108],[-11,-45],[-25,54],[-17,18],[-12,50],[-28,68],[0,76],[31,31],[22,51],[-20,47],[7,75],[-9,39],[23,44],[3,24],[-18,1],[-3,74],[7,43],[-41,-17],[6,-45],[11,-16],[-1,-37],[-13,-85],[-1,-60],[-19,-73],[-18,13],[7,-93],[-10,-43],[-24,52],[-25,22],[-9,30],[-12,132],[-16,70],[21,-29],[7,75],[27,37],[12,147],[-8,102],[10,57],[6,-21],[12,42],[7,92],[-21,-11],[-4,-58],[-19,-30],[-21,-61],[13,-113],[-8,-42],[-33,3],[-12,-43],[3,-30],[-17,-14],[-7,34],[4,67],[-19,23],[-12,128],[-32,-17],[0,-48],[-28,118],[-3,123],[8,25],[25,1],[7,66],[13,49],[30,12],[16,-72],[7,63],[-15,168],[9,3],[30,-45],[4,-59],[11,-32],[6,23],[-9,77],[-22,35],[-19,48],[-3,53],[-11,13],[-12,-25],[-10,49],[-23,32],[-5,57],[12,23],[-4,64],[-22,28],[-37,66],[-13,69],[-15,33],[-13,79],[29,43],[-11,58],[-18,-47],[-19,18],[-2,-27],[-23,58],[-31,8],[-11,56],[-27,-28],[-54,76],[-8,75],[15,73],[18,-22],[33,4],[9,48],[-22,3],[-32,36],[2,35],[-18,125],[13,81],[-24,-10],[-15,19],[-16,53],[7,103],[20,2],[16,-36],[33,-24],[63,-89],[5,15],[-25,58],[-20,19],[-66,130],[-3,95],[-17,-15],[-2,-65],[-20,3],[-39,166],[-55,147],[-11,140],[9,65],[-2,52],[27,41],[-13,100],[-8,-2],[-9,-85],[-20,-30],[0,-42],[12,-22],[-13,-57],[-10,10],[-22,-20],[-25,11],[-36,34],[-7,-15],[-35,36],[-40,174],[-4,110],[-42,140],[-13,79],[19,1],[-11,174],[-19,-56],[1,-58],[-35,132],[-8,177],[-8,92],[-21,135],[-32,116],[-36,37],[-5,-16],[28,-38],[4,-41],[17,-39],[20,-131],[-15,11],[-34,140],[-22,36],[-22,10],[41,-92],[13,-85],[17,-29],[-7,-97],[9,-88],[13,-23],[-5,-24],[27,-124],[3,-55],[25,-139],[2,-32],[-14,11],[27,-204],[9,-85],[1,-69],[-12,8],[-1,-61],[12,-64],[-29,23],[-19,32],[-14,-4],[-18,43],[-15,66],[-10,84],[-22,-16],[-33,50],[-27,-37],[-50,-19],[3,81],[-25,16],[-4,19],[30,39],[-10,69],[5,53],[-28,109],[0,36],[-22,84],[13,24],[-2,90],[-19,64],[-12,10],[15,-131],[-12,-125],[-27,-5],[-42,70],[-26,30],[-15,150],[-11,-68],[-13,-22],[-44,54],[-14,-20],[-10,50],[-12,-18],[-3,-54],[45,-35],[7,7],[45,-44],[33,-66],[30,-104],[-11,-44],[-24,-44],[6,-11],[33,44],[7,30],[27,1],[7,-105],[26,-78],[16,-157],[-19,-44],[-43,-32],[-11,10],[9,47],[-18,16],[-12,-17],[7,-39],[-10,-37],[-43,22],[-11,-23],[5,-60],[-8,-35],[-29,5],[-8,78],[-36,35],[3,15],[-28,74],[-17,19],[-29,-19],[-35,70],[-16,15],[-56,107],[-51,75],[-2,50],[-30,70],[-37,64],[-5,28],[4,75],[-15,48],[-55,121],[-30,47],[-96,89],[-36,71],[-43,66],[-98,104],[-42,49],[-91,144],[-30,39],[5,29],[31,89],[24,-14],[-9,-27],[24,3],[0,43],[21,70],[-22,7],[2,77],[-14,116],[7,35],[22,41],[-5,34],[16,30],[-13,56],[-19,-51],[-2,-59],[-25,-21],[-21,-72],[-1,-49],[-107,-100],[-12,-28],[-28,-28],[-95,22],[-67,36],[-26,32],[-123,125],[-17,56],[18,-10],[34,24],[11,71],[-33,131],[3,42],[-20,-17],[-31,44],[6,-50],[45,-120],[-10,-21],[-54,-51],[-39,0],[-49,55],[-61,24],[-32,23],[-81,40],[-45,11],[-59,-4],[-63,-33],[-77,-12],[-80,-28],[-54,-48],[-39,39],[-45,-26],[-64,-120],[-46,-115],[6,65],[36,103],[62,117],[46,2],[-16,66],[-49,48],[-8,25],[-10,-82],[-26,84],[-26,30],[-17,-9],[-16,27],[-71,19],[9,28],[38,23],[-42,33],[-19,-6],[-2,25],[37,156],[-15,15],[-13,-36],[-29,-22],[-1,-57],[-20,-66],[-39,12],[-70,97],[1,31],[-26,36],[-39,26],[-41,-35],[-22,27],[13,29],[31,32],[25,74],[-14,10],[-18,-49],[-79,-93],[-29,-23],[-38,5],[1,-54],[60,27],[11,-24],[2,-53],[-17,2],[-28,-37],[-18,6],[-41,-38],[-41,-77],[-12,2],[-12,48],[47,77],[-37,-12],[-17,11],[-1,54],[23,83],[13,27],[18,2],[20,-31],[24,17],[22,41],[38,13],[57,58],[42,20],[-9,25],[-19,-2],[1,71],[-11,-49],[-19,-20],[1,30],[25,65],[1,21],[-33,-58],[-45,-47],[-19,-3],[-4,30],[63,112],[-6,38],[-29,-60],[-38,-14],[-12,26],[-15,-49],[-20,-14],[-54,13],[-10,58],[27,19],[11,-9],[18,25],[40,16],[29,28],[23,65],[-25,2],[-14,-46],[-23,-19],[-45,-2],[-18,68],[-12,3],[-17,-69],[-21,-8],[-4,59],[16,26],[17,-6],[-11,44],[-3,55],[13,34],[26,114],[96,6],[-7,38],[-91,-5],[-21,-63],[-27,-26],[-21,-77],[-31,-48],[-23,12],[20,36],[-10,113],[-12,53],[18,30],[-22,10],[-14,-23],[-1,-51],[12,-63],[-14,-63],[1,-41],[-13,-15],[-26,48],[-2,-67],[-27,-45],[-21,22],[0,52],[-11,19],[-7,-73],[-9,15],[4,129],[9,62],[-15,11],[-17,-130],[9,-111],[-4,-29],[-19,-9],[1,49],[-27,34],[-7,-46],[16,-65],[-13,-8],[-38,16],[-34,-48],[-28,9],[-5,31],[14,95],[42,151],[1,53],[49,123],[16,81],[-6,18],[-73,-211],[-1,-35],[-18,-58],[-8,8],[-8,69],[-12,-1],[-3,-81],[-11,-54],[-18,-42],[-3,-64],[-16,-68],[-21,27],[-7,-44],[24,-28],[-5,-91],[10,-8],[19,85],[37,6],[11,-22],[4,-91],[-14,-45],[-31,-32],[-22,-76],[1,-63],[21,20],[17,75],[30,44],[30,-89],[0,-44],[10,-43],[-23,-192],[-26,0],[-9,52],[-19,9],[1,-36],[-27,-44],[0,-17],[28,14],[23,-27],[61,-124],[26,-85],[-15,-77],[-13,-26],[-34,-35],[-17,16],[-14,-18],[-29,-6],[9,51],[-26,67],[9,60],[-17,63],[-16,-126],[0,-62],[-12,-41],[-19,69],[-9,-8],[-7,-59],[-18,-33],[-34,50],[-29,-40],[-15,52],[-27,-21],[-22,5],[-9,28],[24,74],[-9,14],[-27,-38],[-29,-175],[-30,-42],[-9,18],[14,55],[13,84],[-15,172],[-16,5],[-5,-28],[1,-81],[13,-53],[-10,-4],[-10,-72],[-17,11],[-8,-20],[-5,-68],[-11,-17],[4,-47],[25,-31],[-6,-74],[-27,32],[-12,127],[8,37],[-11,78],[-17,6],[6,-52],[-8,-49],[5,-196],[-5,-64],[-27,74],[-1,43],[-16,28],[-34,31],[3,-40],[24,-36],[-1,-63],[-69,-158],[-32,-91],[0,-36],[-16,-10],[-9,-86],[-12,2],[-3,70],[34,211],[0,29],[-26,-65],[-19,-111],[-12,11],[-9,142],[-13,-52],[-11,2],[16,-62],[-5,-80],[-39,0],[-8,-61],[-18,-24],[-22,-55],[7,-43],[-15,-41],[-16,-10],[0,54],[-16,36],[-13,-45],[-1,-53],[-22,-17],[-24,31],[-9,-23],[-21,38],[-7,43],[-12,-36],[-21,-27],[6,-35],[-20,0],[-5,-39],[-41,-7],[-6,76],[-22,-14],[-21,22],[3,22],[-21,8],[-3,70],[8,36],[16,18],[5,71],[32,31],[9,-11],[29,51],[33,3],[0,45],[33,21],[34,62],[31,-9],[-11,69],[22,26],[3,32],[37,102],[-13,15],[-24,-22],[-60,-109],[-30,-20],[-18,-39],[-39,16],[-43,67],[-15,40],[-5,46],[18,117],[13,45],[15,133],[27,79],[17,33],[35,99],[11,98],[0,71],[23,32],[5,156],[4,27],[-18,44],[-22,163],[39,35],[5,27],[63,27],[89,164],[92,118],[51,-161],[41,-16],[13,-27],[34,112],[30,8],[36,-45],[19,10],[63,-48],[65,-20],[30,-55],[-1,27],[-22,63],[-29,36],[-38,-14],[-51,63],[-24,-1],[-40,42],[-25,68],[-39,66],[-29,28],[18,55],[24,9],[31,124],[23,18],[8,60],[46,27],[30,45],[-32,50],[-20,4],[-43,-64],[-11,-49],[-19,-14],[-12,-70],[0,-70],[-18,-31],[-25,19],[-36,7],[-74,-17],[-26,43],[-28,16],[-14,-52],[-87,-84],[-39,-154],[-36,-12],[-19,-31],[-33,2],[-39,-63],[-48,-109],[-1,-29],[26,-124],[-2,-23],[-38,25],[-19,-18],[-30,-52],[-21,-92],[-27,-29],[-32,-74],[-6,-73],[8,-34],[19,-24],[-40,-56],[-9,-67],[-13,-4],[-28,-70],[-19,-7],[-9,28],[-25,-11],[4,-68],[16,-13],[-3,-30],[25,-42],[8,-35],[-12,-73],[-19,-47],[-7,-62],[-25,-21],[-4,-22],[-39,-2],[-15,12],[-40,-31],[-18,3],[-19,-60],[23,16],[15,-20],[15,30],[19,1],[7,-50],[-16,-116],[-19,-16],[-26,-51],[-28,37],[5,-34],[-13,-18],[-18,18],[7,35],[2,75],[-22,71],[3,-119],[-9,-55],[-18,-15],[-13,25],[-6,-35],[16,-28],[-9,-53],[-49,-10],[17,-93],[-8,-32],[-27,-21],[-10,8],[-24,-45],[-12,13],[-36,-37],[5,-26],[20,-18],[-23,-32],[-7,-43],[2,-61],[-12,-44],[-24,-35],[24,-26],[-6,-59],[10,-60],[27,63],[59,-23],[16,22],[12,-22],[15,25],[22,-78],[19,-27],[19,13],[23,-34],[18,-53],[8,-53],[15,-22],[-28,-14],[-14,-118],[-12,-39],[-30,-25],[-11,-55],[-24,-33],[-48,0],[-15,-17],[-4,-94],[-12,-33],[-27,1],[-5,-20],[10,-79],[11,-32],[-27,-36],[-20,15],[-2,-45],[20,-46],[-11,-81],[-21,-30],[-6,-33],[9,-24],[-23,0],[-14,-52],[-26,66],[-9,-7],[5,-57],[-4,-40],[-21,-3],[-12,-43],[-17,16],[0,27],[-20,-1],[-4,-39],[-22,-24],[-23,31],[-28,-17],[-35,-69],[15,-52],[-9,-51],[-40,-44],[-28,-2],[1,-53],[14,-25],[-6,-40],[-20,-16],[-36,59],[-10,30],[-20,-20],[-5,-63],[1,-69],[-26,-27],[-3,-97],[-65,-7],[-21,24],[-2,-74],[8,-71],[-20,0],[-12,37],[-21,3],[-4,-39],[-32,-27],[-39,-92],[-15,-12],[-5,-44],[13,-11],[47,64],[4,-50],[-6,-53],[-15,-7],[0,-31],[18,-38],[-10,-38],[-18,-26],[-2,-49],[-26,-44],[-8,-32],[2,-42],[-32,14],[-31,-28],[-4,-70],[-12,-12],[-19,75],[-8,-53],[-27,-42],[-11,-53],[-24,-6],[5,-50],[-17,-27],[-25,43],[-24,67],[-22,-16],[0,-51],[11,-5],[1,-36],[-29,-10],[-13,-67],[6,-32],[18,-6],[5,-52],[-36,-4],[-24,-15],[-17,77],[-51,-38],[-18,-51],[-16,-3],[-9,-49],[19,-1],[27,25],[20,-17],[6,-54],[-16,-47],[-43,44],[-23,11],[-5,-70],[-33,6],[-23,21],[-20,-33],[-27,-88],[2,-44],[47,-20],[32,-36],[-3,-29],[-32,-42],[1,-23],[18,-4],[25,31],[16,-7],[-55,-78],[-23,-63],[1,-52],[-13,50],[-10,-17],[16,-66],[-6,-50],[-7,39],[-10,-2],[-2,-53],[-25,80],[0,95],[-19,-60],[8,-73],[-4,-67],[-24,-6],[3,58],[-35,1],[-16,-80],[-25,-9],[-75,-43],[-29,-22],[-7,-22],[-3,-74],[-17,46],[5,80],[-23,-19],[10,-30],[1,-104],[-16,-71],[6,-46],[-24,-81],[-32,-30],[2,52],[24,4],[-1,40],[-10,4],[2,111],[18,72],[-29,39],[-28,12],[-13,-18],[3,-45],[-12,-21],[-13,22],[-25,-13],[-9,-82],[-35,-74],[-26,-20],[-37,28],[-6,-29],[11,-44],[-20,-81],[1,-31],[-21,-37],[-12,105],[-10,13],[-14,-37],[-11,14],[-18,-31],[31,-13],[-2,-68],[-32,-10],[-16,25],[5,39],[-13,24],[-22,-22],[-19,-90],[-61,-85],[-26,2],[-11,29],[-26,-29],[-13,4],[10,156],[29,96],[1,46],[-15,16],[-31,-2],[-24,-29],[-24,-101],[4,-129],[-40,-140],[-7,-12],[-14,-85],[-21,36],[-19,-12],[13,-65],[10,-17],[1,-56],[-25,-38],[-19,33],[0,45],[-16,15],[-13,-60],[10,-49],[-17,-47],[-14,27],[-32,-8],[-16,18],[-14,74],[32,6],[-21,47],[-8,104],[-21,56],[-11,7],[-16,-33],[-10,-65],[7,-27],[14,0],[18,-70],[-11,-46],[14,-95],[0,-52],[-22,30],[-20,-19],[3,-26],[-17,-30],[-18,-7],[-22,28],[-16,59],[4,36],[-13,56],[-21,37],[-31,-25],[-10,-56],[49,-85],[4,-31],[-50,-111],[-24,-21],[-20,-33],[16,-56],[27,2],[9,24],[22,-50],[13,-83],[-26,6],[8,34],[-17,6],[-10,-28],[-16,22],[-17,62],[-25,-39],[2,-69],[-18,-1],[-28,-50],[-23,18],[-37,10],[-45,-5],[-34,-14],[-41,-40],[-29,-71],[-4,-69],[-29,-53],[-51,-33],[-29,3],[-28,28],[-9,30],[-9,74],[-10,30],[-1,55],[8,29],[44,41],[14,25],[23,110],[19,110],[-1,29],[22,54],[14,13],[25,-46],[38,39],[25,49],[25,0],[37,81],[34,20],[36,-14],[31,5],[1,-37],[28,-72],[9,-61],[-5,-50],[27,24],[-8,52],[2,37],[30,-30],[-15,42],[2,77],[-10,109],[32,46],[26,21],[42,-15],[21,14],[10,69],[-16,4],[4,28],[18,10],[7,38],[18,-4],[26,99],[3,-32],[15,-20],[21,48],[-5,83],[-20,-9],[27,68],[64,206],[33,52],[23,62],[19,13],[27,42],[25,68],[27,14],[35,39],[37,20],[89,70],[30,-9],[10,18],[45,5],[10,-10],[-20,-27],[-1,-47],[14,-2],[-3,-49],[-29,-18],[-3,-85],[38,-98],[11,22],[27,-39],[4,19],[-29,53],[-3,85],[-6,32],[26,-28],[57,3],[7,-86],[23,7],[26,-36],[7,22],[-14,40],[27,15],[-56,89],[-32,36],[1,58],[-16,-7],[39,166],[15,123],[11,56],[23,44],[36,96],[18,13],[40,74],[30,81],[82,96],[41,74],[35,39],[68,106],[17,42],[29,-68],[19,-14],[31,19],[8,59],[-13,47],[-2,46],[15,122],[29,108],[41,130],[24,62],[22,37],[40,41],[21,52],[22,87],[14,15],[26,62],[30,25],[8,-74],[18,-15],[4,41],[-10,105],[-23,-2],[-6,34],[2,95],[9,59],[25,409],[14,42],[38,16],[12,53],[-20,-9],[-34,77],[-3,62],[9,97],[20,106],[27,33],[29,101],[49,102],[4,57],[19,93],[-5,45],[13,33],[-3,43],[13,38],[-20,0],[-11,-32],[0,-38],[-21,-53],[-28,-31],[-38,-27],[-21,-31],[-29,-22],[-5,-21],[-41,-28],[-44,-61],[-79,-73],[-26,5],[-28,53],[-5,73],[-20,49],[-40,38],[2,46],[11,17],[9,167],[-12,-3],[-25,-101],[-41,-54],[-6,-58],[5,-58],[-8,-37],[-14,-12],[-3,-32],[9,-80],[16,-91],[19,-76],[-26,-87],[-28,-21],[-42,32],[-39,161],[-47,208],[-26,73],[-23,42],[-31,13],[16,65],[-16,44],[-25,-9],[-6,-87],[-15,7],[1,-65],[-29,-30],[-24,79],[3,26],[-19,19],[-11,-30],[-16,6],[-1,59],[-22,-18],[-25,61],[18,47],[-17,89],[-44,-52],[-46,-72],[-31,-62],[-20,-95],[-13,57],[-36,-33],[-94,-124],[-10,-41],[0,-49],[-41,-38],[-8,-26],[-22,-10],[-15,-44],[-20,-15],[-4,55],[-29,45],[-54,-20],[-29,33],[49,47],[15,-39],[18,12],[5,50],[25,77],[3,60],[-8,85],[1,81],[-16,74],[5,21],[-16,20],[-29,97],[-20,139],[29,128],[21,23],[24,79],[24,26],[-15,77],[-18,44],[-15,64],[-10,107],[-50,162],[-3,74],[-25,67],[-10,63],[-30,85],[-10,44],[-22,9],[-11,-37],[-2,-74],[6,-37],[-8,-56],[-12,-21],[-27,-10],[-18,21],[-19,-54],[-36,-29],[-50,-68],[-73,-48],[-88,-28],[-75,10],[-51,38],[-13,32],[-13,99],[23,18],[-18,69],[-54,62],[-32,108],[-5,34],[-33,51],[-18,62],[-42,8],[-31,42],[-47,110],[23,32],[24,55],[-2,36],[-24,6],[-38,-54],[-48,10],[-16,51],[11,32],[32,1],[57,128],[10,-4],[17,45],[-16,34],[-4,41],[32,24],[-27,7],[5,70],[27,78],[-17,3],[-18,-48],[-40,42],[-6,40],[22,49],[27,-6],[19,23],[0,45],[-17,-21],[-30,32],[-16,54],[2,23],[-20,7],[-13,35],[-10,-14],[-12,-99],[-14,-6],[-38,16],[-10,21],[-10,66],[1,129],[-11,19],[-43,10],[-18,37],[-10,98],[42,44],[6,38],[-17,46],[-30,31],[-45,-27],[0,-46],[-21,23],[-9,88],[8,147],[6,11],[-2,-107],[92,47],[0,19],[-35,20],[-20,28],[-24,82],[2,18],[36,19],[55,-8],[33,25],[-29,134],[-4,46],[3,84],[21,77],[101,278],[4,29],[27,77],[29,61],[5,70],[18,67],[21,34],[19,-4],[9,23],[-11,115],[26,188],[18,70],[38,61],[-18,24],[4,22],[58,133],[60,46],[48,11],[42,-45],[43,-11],[32,-84],[24,-6],[6,-26],[54,-88],[73,24],[61,124],[4,47],[44,29],[63,172],[23,89],[23,39],[-9,58],[14,5],[28,-28],[22,-5],[13,-77],[52,0],[45,20],[26,-20],[77,28],[48,38],[9,51],[56,128],[38,135],[0,71],[-12,81],[-23,95],[-17,121],[-1,118],[-5,52],[-71,157],[-9,32],[-41,28],[-27,1],[10,97],[27,33],[14,-25],[35,-20],[51,7],[-10,45],[31,10],[41,81],[2,114],[-42,122],[-47,68],[-25,48],[-1,-32],[-27,-48],[-20,-86],[-42,-30],[-42,42],[-59,-93],[-81,-34],[-18,-71],[-85,-103],[-21,-70],[-5,-99],[-44,-70],[-4,93],[-15,110],[-23,50],[-28,-3],[4,34],[-32,46],[-9,36],[-38,-60],[16,-45],[21,-7],[16,-39],[24,11],[2,-48],[-22,-81],[-19,-11],[-22,82],[-55,76],[-41,33],[-64,13],[-41,-27],[-27,12],[-52,3],[-45,-22],[-108,-112],[-58,-17],[-111,74],[-94,45],[-47,14],[-88,40],[-49,79],[-20,96],[1,74],[20,36],[-6,64],[-28,63],[-45,56],[-1,59],[-46,64],[-10,56],[39,-33],[33,3],[9,27],[24,15],[16,32],[-3,55],[35,61],[-38,63],[-19,9],[-29,-16],[-39,15],[-64,52],[-104,21],[-49,52],[-38,63],[-55,60],[-46,30],[-15,93]],[[2812,78476],[27,27],[-1,59],[18,0],[10,36],[0,57],[21,33],[5,57],[-10,16],[11,87],[46,106],[16,-26],[27,16],[28,-3],[-9,65],[-12,5],[9,77],[-6,26],[18,76],[40,67],[54,37],[29,-53],[31,1],[2,-23],[-21,-74],[5,-60],[-55,-95],[-65,-72],[-19,-66],[-1,-36],[-28,-81],[-15,-58],[-25,-10],[-28,-71],[-15,-17],[-4,-51],[-15,21],[-27,-49],[-54,-69],[13,45]],[[2752,78371],[29,49],[-1,-45],[-26,-25],[-2,21]],[[2615,78610],[4,50],[19,16],[4,-44],[-9,-45],[-12,-8],[-6,31]],[[2607,82847],[50,-13],[29,5],[10,-14],[-27,-68],[-23,4],[-9,46],[-20,10],[-10,30]],[[2611,78728],[8,-39],[-15,13],[7,26]],[[2547,78457],[17,17],[23,-3],[-2,19],[27,23],[19,-10],[10,-25],[-17,-105],[-30,50],[-25,-29],[-19,14],[-3,49]],[[2517,78521],[8,26],[23,-16],[1,-31],[-14,-31],[-13,16],[-5,36]],[[2497,78313],[4,72],[33,-18],[-7,-57],[-30,3]],[[2431,83489],[9,43],[22,15],[12,-10],[29,22],[-2,-56],[-32,-62],[-5,31],[-33,17]],[[2316,78170],[5,42],[40,72],[31,-27],[1,-27],[-13,-58],[-19,2],[-17,-26],[-15,-48],[-15,21],[2,49]],[[2228,78152],[9,-32],[-16,2],[7,30]],[[2183,78046],[17,42],[16,-37],[-12,-55],[-19,-1],[-2,51]],[[2036,90914],[2,37],[25,87],[-3,75],[8,18],[-5,57],[21,3],[8,-42],[-4,-48],[12,-34],[64,-57],[77,-48],[55,-20],[58,87],[58,58],[58,-14],[29,-69],[23,-15],[15,-104],[-2,-38],[53,-56],[56,-14],[22,-32],[9,-35],[36,-20],[88,-20],[14,6],[102,-55],[-27,-131],[-22,-43],[-51,35],[-43,-1],[-50,-29],[-44,-89],[-10,-61],[1,-53],[-20,-46],[-30,22],[3,24],[-26,119],[-32,62],[-50,60],[-39,-5],[-11,69],[-17,55],[-54,80],[-86,69],[-65,11],[-47,-44],[-18,-58],[-36,-34],[-28,33],[-49,36],[-24,83],[-7,56],[3,73]],[[1818,77819],[15,59],[33,49],[12,-2],[27,-39],[-1,-41],[-28,-51],[-32,-27],[-22,-1],[-4,53]],[[1695,87369],[6,69],[25,52],[11,-3],[-4,-58],[12,-54],[33,-70],[48,-63],[22,-14],[25,13],[57,-94],[-21,-19],[-9,37],[-51,8],[-26,-17],[-33,46],[-58,136],[-37,31]],[[1687,87598],[8,-5],[5,-84],[-20,42],[7,47]],[[1421,77626],[35,0],[33,-31],[9,30],[14,-2],[33,27],[25,-1],[-6,-28],[10,-29],[8,15],[31,-24],[23,16],[10,-11],[32,11],[5,-12],[41,-7],[-25,-23],[-16,6],[-14,-22],[-44,-1],[-22,-35],[-8,16],[-16,-19],[-19,4],[-18,25],[-52,3],[-9,-15],[-29,19],[-10,49],[-11,-1],[-10,40]],[[1064,77503],[55,37],[11,-28],[25,50],[3,-22],[19,38],[5,29],[18,-29],[27,8],[8,34],[6,-24],[35,37],[1,42],[44,10],[-13,41],[43,-5],[13,34],[-1,36],[-32,7],[-23,29],[5,26],[21,-16],[12,36],[-2,39],[38,46],[33,-32],[22,-76],[1,-31],[-21,-85],[-34,8],[-4,-43],[33,-79],[-3,-24],[-15,21],[-17,-10],[-4,-27],[-16,-4],[-20,23],[-19,-95],[-25,31],[-64,-55],[-13,29],[-29,12],[-23,-6],[-13,-35],[-22,-8],[-33,6],[-32,25]],[[1013,77451],[17,17],[7,-29],[-22,-4],[-2,16]],[[1005,77685],[16,-19],[-11,-12],[-5,31]],[[949,77433],[5,9],[40,-3],[1,-26],[-19,3],[-22,-24],[-5,41]],[[890,77452],[11,14],[28,-1],[15,-90],[-16,5],[-17,49],[-21,23]],[[821,77563],[14,45],[26,-9],[23,-80],[-20,-38],[15,-56],[-13,-34],[-18,-2],[-18,22],[3,57],[-7,2],[-8,77],[3,16]],[[811,77269],[5,56],[12,10],[10,-27],[19,10],[-12,22],[34,30],[10,-23],[-2,-55],[-23,0],[13,-52],[-29,29],[2,-40],[-12,12],[-5,-39],[-7,45],[-15,22]],[[601,77040],[11,67],[15,16],[7,36],[-13,66],[4,27],[32,7],[7,59],[-13,69],[10,45],[15,4],[18,-20],[19,59],[9,-14],[3,-74],[-20,-29],[-2,-50],[13,-20],[19,4],[31,26],[30,5],[7,-64],[-7,-88],[-15,-12],[-36,18],[-13,-52],[-18,-12],[-13,-41],[-23,31],[-6,-25],[6,-48],[-11,17],[-13,-26],[-4,55],[-14,29],[-19,-108],[-14,11],[-2,32]],[[469,77371],[13,9],[2,-26],[-14,-17],[-1,34]],[[402,77131],[18,33],[24,-17],[26,34],[50,39],[21,43],[2,128],[12,16],[16,-10],[15,-44],[-26,-98],[5,-86],[-7,-38],[-33,-31],[-36,61],[-27,-33],[-40,-10],[-1,-43],[-10,5],[-9,51]],[[257,77332],[8,29],[29,17],[39,-5],[11,-42],[-2,-30],[19,-32],[15,17],[18,-17],[33,34],[-10,-40],[-41,-32],[-12,-71],[4,-31],[-12,-31],[-8,16],[-9,-44],[8,-57],[-8,-5],[-9,56],[-14,-24],[-16,48],[-13,8],[4,28],[41,25],[-3,64],[-20,1],[-13,34],[-39,66],[0,18]],[[130,77016],[32,-12],[-3,-18],[-27,7],[-2,23]],[[81,76979],[27,-17],[7,-23],[-25,5],[-9,35]],[[77,77233],[14,52],[20,-35],[0,-61],[-19,-10],[-15,54]],[[43,76751],[8,25],[10,-22],[5,-50],[-12,-2],[-10,-32],[-1,81]],[[3,76640],[12,18],[6,-59],[-18,-25],[0,66]],[[99917,77451],[12,-2],[12,35],[26,14],[31,-61],[-9,-68],[-24,-42],[-12,-5],[-25,29],[-11,29],[0,71]],[[99678,77049],[5,24],[26,-29],[19,3],[27,-24],[4,-31],[24,-42],[27,-78],[25,-26],[11,-57],[44,-11],[21,-33],[-18,-19],[-9,14],[-29,-16],[-15,41],[-41,72],[-30,101],[-30,43],[-17,-8],[-22,25],[-22,51]],[[99628,77446],[9,11],[20,-16],[11,-33],[-14,-49],[-10,-4],[-16,91]],[[99570,77271],[19,-7],[21,-67],[-22,15],[-18,59]],[[99530,77510],[7,22],[17,-22],[3,-34],[-17,-20],[-10,54]],[[99282,77351],[24,27],[15,50],[39,26],[6,63],[16,96],[11,18],[20,-52],[-12,-32],[-8,-57],[-20,-53],[21,-18],[0,-35],[-13,-10],[-42,17],[-17,-31],[-4,-70],[-11,2],[-25,40],[0,19]],[[98911,77903],[26,-13],[-18,-27],[-8,40]],[[98408,78327],[22,-8],[3,-26],[-24,15],[-1,19]],[[98376,78346],[14,-8],[0,-29],[-14,37]],[[98352,78374],[20,-27],[-12,-2],[-8,29]],[[98210,77943],[6,31],[19,27],[23,-6],[26,67],[41,5],[-17,-40],[-5,-37],[10,-104],[-21,0],[-17,52],[-35,-19],[-30,24]],[[97960,78551],[47,86],[33,11],[100,-20],[29,-63],[24,-15],[35,-94],[2,-18],[-35,-8],[-21,38],[-17,-71],[-9,-13],[-38,15],[-26,-41],[-27,32],[-12,40],[-3,63],[-32,56],[-35,-24],[-15,26]],[[13347,81071],[9,23],[10,-28],[-5,-52],[-16,34],[2,23]],[[13279,80906],[61,56],[7,-80],[13,-14],[-20,-68],[-24,-1],[-33,63],[-4,44]],[[13241,80901],[26,65],[8,-25],[-26,-58],[-8,18]],[[13234,81005],[16,62],[-2,43],[13,20],[-14,43],[-2,70],[5,39],[11,9],[19,-32],[5,-35],[13,-7],[17,-40],[0,-165],[-9,-34],[-27,-2],[-13,32],[-23,-36],[-9,33]],[[13172,81388],[4,66],[6,1],[52,-135],[-12,-28],[-1,-68],[-15,-111],[-23,74],[-9,113],[-2,88]],[[12999,82714],[4,48],[9,-30],[-13,-18]],[[12979,82569],[2,29],[21,40],[21,-38],[-1,-22],[-24,-37],[-19,28]],[[12954,82690],[20,33],[17,52],[-5,-50],[-18,-75],[-14,40]],[[12888,80934],[18,6],[-4,-53],[-14,47]],[[12850,82395],[7,53],[10,-24],[22,-97],[10,-68],[-13,-15],[-21,61],[-9,4],[-6,86]],[[12838,82543],[18,86],[21,30],[30,-20],[24,22],[28,-42],[4,-36],[-18,-53],[6,-82],[-16,-19],[-44,-22],[-23,65],[-24,29],[-6,42]],[[12768,81460],[21,27],[-2,-46],[-16,-11],[-3,30]],[[12700,80811],[9,0],[10,-128],[-15,29],[-4,99]],[[12674,81692],[9,23],[1,61],[20,-30],[1,-32],[-22,-72],[-9,50]],[[12659,81749],[12,9],[-1,-36],[-11,27]],[[12637,81497],[8,92],[8,25],[32,-9],[10,-16],[-2,-35],[-16,-42],[8,-16],[20,40],[5,43],[13,-19],[19,68],[19,-2],[16,-39],[-5,-60],[-15,-35],[-19,15],[-13,-35],[17,-22],[-2,-37],[-22,-21],[-22,-50],[-9,-97],[-23,78],[17,61],[-1,78],[-29,51],[-14,-16]],[[12629,82086],[11,22],[24,102],[28,10],[11,20],[-22,13],[-1,47],[15,-22],[9,34],[-36,62],[14,29],[-10,86],[23,49],[45,-24],[71,-17],[24,-82],[11,-73],[-5,-69],[14,-6],[13,-68],[17,-48],[17,5],[57,-119],[12,-52],[33,-111],[3,-126],[29,-29],[8,-82],[8,-32],[31,-50],[13,-58],[-10,-6],[-19,46],[-75,98],[-3,-31],[-23,-74],[11,-8],[14,47],[8,-19],[23,10],[5,-37],[20,-13],[10,-29],[-38,-14],[8,-36],[32,21],[14,-52],[16,-14],[11,-87],[8,-9],[-12,-49],[-20,9],[2,-32],[22,-22],[11,8],[3,46],[14,36],[9,-21],[5,-91],[-15,-34],[4,-36],[-22,-94],[-29,-40],[18,-23],[27,59],[15,-10],[0,-203],[7,-72],[-20,-105],[-37,-9],[-26,48],[-14,-19],[-17,38],[-1,37],[-38,-2],[17,53],[27,-10],[1,39],[-19,42],[-19,13],[-35,78],[9,38],[10,115],[-15,9],[-11,-58],[7,101],[8,28],[-11,39],[-5,-55],[-27,-21],[14,-103],[-18,-58],[-47,56],[-4,35],[17,58],[-18,100],[-20,-5],[-10,50],[-21,3],[12,-78],[18,-65],[6,-72],[-7,-36],[15,-17],[20,-145],[24,-26],[-4,58],[24,18],[28,-65],[4,-124],[-14,-16],[-18,74],[-7,-7],[23,-170],[-29,0],[-26,32],[-2,60],[-20,40],[-49,177],[-10,22],[-2,84],[-12,10],[-8,61],[29,8],[-27,33],[3,119],[-33,-28],[-10,26],[-18,-19],[-9,42],[6,84],[33,30],[11,-63],[17,20],[-13,29],[6,39],[14,20],[14,-14],[22,44],[-29,96],[14,12],[-16,32],[4,66],[-24,-21],[-49,87],[-4,28],[16,-1],[-9,50],[3,35],[-27,18],[6,-57],[-10,-13],[-32,39],[-16,58],[12,52],[28,14],[37,-54],[28,35],[-17,67],[-18,16],[-12,-20],[-9,17],[8,30],[-2,42],[7,63],[-5,16],[-13,-47],[-27,-65],[-22,-33],[-23,38],[-5,45]],[[12611,83642],[15,25],[14,-33],[-27,-31],[-2,39]],[[12594,82026],[14,28],[11,-6],[4,-52],[-8,-48],[-10,3],[-11,75]],[[12476,82024],[28,22],[1,-43],[31,33],[8,75],[7,-2],[-9,-101],[-22,-24],[-20,-56],[-18,17],[8,30],[-14,49]],[[12463,83115],[17,52],[16,17],[7,33],[33,-1],[1,26],[28,-40],[11,-83],[18,-54],[31,-2],[9,-21],[10,26],[-29,44],[-13,80],[-2,55],[-35,79],[11,53],[34,26],[51,-37],[25,-9],[47,-44],[31,-20],[7,14],[57,-11],[30,-74],[14,-82],[3,-56],[9,-23],[23,-11],[16,-48],[5,-46],[33,-52],[9,-64],[15,-34],[-31,-38],[-20,-42],[-26,-24],[-16,16],[-31,4],[-43,24],[5,-48],[-23,-51],[-40,25],[-19,29],[-12,-47],[-15,-20],[-25,-2],[-17,15],[-3,87],[-11,32],[-30,-64],[13,-29],[-2,-90],[-23,-3],[-2,-31],[10,-51],[-11,-51],[-2,-62],[-17,-50],[4,-39],[-9,-64],[-10,-11],[-25,15],[-14,-103],[-15,19],[-12,52],[-7,63],[-1,109],[-10,87],[2,75],[14,48],[-3,57],[15,101],[-12,28],[-22,-1],[5,77],[-20,56],[-8,75],[2,43],[-5,91]],[[12311,84926],[43,-96],[7,-47],[14,-9],[8,-130],[19,1],[21,38],[11,-23],[20,-2],[21,-29],[54,13],[-3,-89],[25,-75],[2,-52],[21,-52],[14,-87],[17,-55],[2,-142],[21,-60],[4,-65],[-9,-2],[-5,47],[-38,120],[-10,115],[-19,37],[-9,53],[-21,0],[26,-101],[4,-34],[-13,-23],[35,-121],[23,-49],[-2,-62],[24,-113],[-19,6],[15,-102],[0,-27],[-25,-62],[-20,26],[-17,-1],[4,-34],[-21,-104],[-38,-83],[-24,-25],[-2,-33],[-31,-65],[-29,3],[-9,89],[-4,135],[21,80],[15,23],[-16,31],[-1,69],[14,6],[12,-39],[6,23],[-40,141],[-1,58],[-18,60],[-5,82],[-11,43],[8,125],[-12,81],[-3,95],[-11,91],[4,44],[-20,93],[-17,45],[-9,77],[-6,101],[3,42]],[[12292,85061],[35,-45],[27,-125],[-10,-5],[-22,115],[-23,19],[-7,41]],[[12202,85643],[15,-71],[0,-46],[-10,29],[-5,88]],[[12106,83714],[16,11],[1,26],[28,65],[-4,68],[36,64],[18,-12],[25,-85],[15,10],[18,-58],[23,-24],[38,4],[22,-14],[12,-79],[-2,-49],[-20,35],[-19,-2],[-1,-22],[21,-25],[11,-46],[29,-278],[-1,-71],[12,-56],[0,-58],[18,-142],[5,-107],[-5,-112],[-11,1],[8,-81],[3,-240],[-9,-111],[-11,6],[-28,70],[-10,86],[-21,59],[-40,212],[4,48],[22,23],[-18,30],[-26,-31],[-14,88],[-11,-15],[-17,42],[-12,55],[2,34],[-14,-22],[-10,24],[-30,2],[-19,96],[19,3],[10,-29],[10,13],[-3,105],[19,53],[6,38],[-39,86],[24,63],[-4,22],[14,35],[4,53],[-33,16],[-21,-28],[-14,79],[-26,72]],[[12097,84881],[16,18],[19,-7],[16,-52],[-29,-6],[-22,47]],[[12062,83572],[4,48],[-3,66],[25,9],[34,-62],[15,-74],[12,-19],[17,30],[23,-10],[-16,-82],[-24,-14],[-27,-161],[-12,6],[-40,-30],[-9,8],[3,102],[25,47],[1,50],[-22,4],[2,29],[-13,31],[5,22]],[[11978,85151],[9,14],[7,-59],[-16,45]],[[11978,84786],[30,41],[3,-53],[-16,-19],[-17,31]],[[11861,84371],[5,174],[16,33],[8,-9],[29,60],[3,51],[-12,92],[32,-2],[0,-54],[12,-60],[28,61],[29,-6],[17,-20],[17,49],[36,50],[14,-55],[41,-30],[25,-53],[-12,-78],[-31,-76],[4,-49],[12,-5],[8,60],[32,108],[15,12],[34,-55],[46,-10],[11,-32],[29,-16],[15,-79],[-4,-67],[-22,-44],[-37,49],[-10,-8],[12,-40],[18,-18],[33,-75],[-21,-29],[-25,-2],[4,-28],[21,-13],[23,24],[7,-95],[14,-69],[12,-143],[-14,-42],[-42,-11],[-17,12],[-44,95],[-65,99],[-27,50],[-22,-26],[-9,29],[-4,-32],[30,-55],[22,-8],[-21,-31],[6,-25],[0,-72],[-9,-50],[-31,-87],[-51,47],[-1,38],[-42,85],[-20,90],[-12,-38],[-19,43],[-10,79],[4,25],[-18,86],[2,24],[-16,57],[-26,30],[-7,49],[-25,36]],[[9331,87212],[28,-14],[12,-44],[-27,-9],[-13,67]],[[9232,87245],[16,14],[58,-26],[-3,-31],[-21,-10],[-50,53]],[[8867,87809],[26,35],[27,3],[15,-16],[-18,-53],[-50,31]],[[8817,87545],[4,86],[28,16],[21,-89],[-11,-44],[-37,-10],[-5,41]],[[8816,87079],[2,14],[43,47],[1,-36],[-40,-59],[-6,34]],[[8698,86532],[8,21],[3,71],[23,12],[-3,42],[21,54],[16,7],[-3,41],[30,34],[9,30],[43,75],[22,118],[27,44],[-3,40],[10,71],[27,34],[4,-43],[24,2],[-17,-51],[9,-17],[24,28],[8,-22],[-11,-37],[-62,-118],[-49,-129],[-8,-80],[-33,-60],[30,-57],[-19,-42],[-23,-8],[-22,15],[-17,-57],[-9,6],[-45,-47],[-14,23]],[[8692,87068],[44,270],[11,-41],[12,13],[-4,66],[44,84],[0,-54],[-12,-32],[-3,-82],[-20,-37],[20,-42],[-24,-97],[2,-46],[-17,-104],[-24,45],[-3,25],[-25,7],[-1,25]],[[8672,87628],[15,31],[17,-19],[17,-48],[-24,-45],[-25,81]],[[17950,55027],[5,31],[24,-13],[25,26],[-2,40],[18,63],[1,79],[-15,136],[-16,8],[-7,-13],[-13,25],[-3,-17],[-14,68],[7,67],[2,142],[-15,41],[9,65],[-8,51],[19,9],[28,138],[9,24],[-4,46],[4,110],[8,12],[-2,74],[-7,64],[1,51],[6,6],[-2,55],[-7,20],[28,116],[0,69],[8,28],[23,29],[10,40],[17,21],[1,19],[25,65],[-1,49],[-11,54],[-14,19],[-31,100],[-13,7],[2,58],[-26,239],[-23,63],[-7,69],[-16,57],[1,147]],[[17974,57684],[6,95],[-10,22],[21,43],[0,72],[-9,178],[-6,65],[-11,47],[-4,58],[6,36],[0,103],[-9,38],[3,39],[-7,31],[3,51],[-5,59],[4,56],[9,24],[-10,31],[-12,97],[4,43],[-7,80],[25,31],[9,29],[5,-14],[12,25],[17,-1],[2,-25],[24,19],[13,-3],[18,-71],[-2,-28],[21,-51],[24,11],[24,183],[5,15],[1,230],[-2,272],[0,437]],[[18136,60011],[200,0],[222,0],[313,1],[97,1],[160,2],[6,-7],[235,0],[162,1]],[[19531,60009],[0,-1691],[-1,-237],[1,-30],[0,-925],[-1,-608],[1,-145],[-1,-113],[0,-2847]],[[19530,53413],[-64,2],[-201,-1],[-128,-1],[-172,0],[-359,476],[-212,269],[-102,136],[-368,471],[5,66],[-5,17],[1,60],[8,16],[17,103]],[[19530,64667],[242,0],[154,2],[327,0],[66,-5],[137,-1],[242,1],[7,2],[217,2]],[[20922,64668],[282,1],[135,0],[140,0],[0,-1163]],[[21479,63506],[1,-381],[0,-434],[1,-193],[0,-1270],[1,-137],[0,-1089]],[[21482,60002],[-183,3],[-22,5],[-62,1]],[[21215,60011],[-204,-3],[-169,-5],[-184,0],[-34,2],[-301,-1],[-186,-2],[-2,9],[-152,-1],[-230,-1],[-222,0]],[[19531,60009],[0,583],[1,36],[0,738],[-5,129],[0,517],[2,216],[0,1857],[1,46],[0,536]],[[27414,45876],[14,60],[5,-33],[-14,-44],[-5,17]],[[27386,45813],[9,39],[11,4],[-5,-33],[-15,-10]],[[27310,45701],[52,90],[6,-30],[-45,-80],[-13,20]],[[27118,45540],[19,106],[20,46],[25,43],[3,23],[36,66],[38,-67],[4,-52],[14,-43],[-4,-11],[-40,-48],[-3,27],[-27,-5],[-3,-24],[-46,-73],[-36,-15],[0,27]],[[27062,45510],[39,24],[-10,-43],[-17,-14],[-13,8],[1,25]],[[27020,45533],[6,35],[12,-10],[-5,-41],[-13,16]],[[26992,48019],[5,-18],[19,-211],[8,-28],[17,-26],[7,17],[14,-21],[-14,-31],[-9,-1],[-21,50],[-7,28],[-13,115],[-6,76],[0,50]],[[26806,45614],[15,13],[4,-23],[-20,-10],[1,20]],[[26322,51623],[29,47],[0,-29],[-16,-37],[-11,-6],[-2,25]],[[26229,53027],[6,-79],[11,-52],[2,-83],[6,-75],[14,-48],[136,-25],[261,-56],[206,-51],[135,-34],[-5,-14],[2,-57],[7,-26],[-3,-71],[11,-76],[37,13],[0,75],[6,47],[3,119],[-12,91],[4,60],[-1,49],[8,16],[-4,24],[12,-8],[9,59],[15,-7],[7,-33],[17,-3],[18,-28],[7,-25],[33,-19],[17,-18],[14,15],[13,-19]],[[27221,52688],[4,-13],[-4,-113],[3,-92],[-4,-22],[10,-25],[4,-95],[1,-113],[23,-389],[13,-129],[0,-51],[28,-302],[32,-288],[29,-229],[65,-412],[38,-200],[4,-63],[10,-83],[-18,-57],[-4,-54],[-1,-87],[5,-130],[6,-96],[16,-145],[35,-269],[15,-166],[5,-84],[5,-29],[11,-140],[17,-152],[9,-66],[6,-94],[13,-109],[17,-258],[-1,-235],[-7,-175],[-5,-210],[-3,-37],[-8,-286],[0,-169],[-3,-80],[-7,-81],[0,-44],[-6,23],[3,51],[-5,20],[-16,-25],[-10,-102],[-6,-17],[-1,-64],[-8,-40],[-2,-78],[5,-33],[-2,-46],[6,-16],[-8,-53],[-8,-13],[-1,-49],[21,39],[25,194],[7,41],[0,-41],[-8,-83],[-34,-222],[-9,-82],[-25,-93],[-34,-139],[-24,-66],[-3,12],[34,122],[14,40],[7,53],[3,81],[5,32],[-6,23],[-9,-11],[-7,26],[-6,-18],[-34,-22],[-14,-41],[-9,-6],[-18,45],[-18,-13],[-7,-41],[-27,-18],[-23,-6],[-17,52],[-8,68],[2,79],[6,60],[6,-3],[-2,49],[-12,97],[-11,47],[2,34],[-8,62],[-11,32],[-5,85],[-6,18],[-11,-15],[-9,101],[-24,31],[-1,16],[-21,38],[-21,55],[-11,-13],[-8,-39],[-16,126],[-15,148],[-5,173],[-7,109],[-7,57],[-28,113],[-11,9],[-1,52],[-12,23],[0,-64],[-14,-11],[-1,66],[-8,113],[-12,51],[3,24],[13,-3],[9,-39],[11,159],[-2,86],[-8,14],[0,40],[8,10],[-4,31],[-12,-4],[-5,-33],[-11,-11],[8,-148],[-9,-20],[-20,-11],[-5,-50],[0,75],[-6,50],[-17,94],[-26,177],[-9,94],[-19,140],[-27,149],[-14,56],[-4,58],[-10,61],[10,-19],[1,-30],[15,30],[10,72],[12,28],[13,98],[13,34],[-2,23],[14,26],[11,84],[-5,75],[-22,21],[5,-113],[-22,30],[6,34],[-1,65],[-5,39],[-38,71],[1,-65],[-11,-28],[11,-37],[16,-6],[11,-110],[-10,-42],[0,-62],[-4,-27],[-21,-6],[-3,-32],[7,-38],[-9,-30],[-4,40],[2,83],[-14,85],[-9,26],[-8,58],[6,182],[-5,66],[-4,164],[9,1],[0,-93],[4,-89],[9,-7],[-4,153],[9,37],[1,41],[8,44],[0,38],[7,50],[0,39],[9,47],[1,91],[5,83],[-6,66],[2,56],[-13,29],[4,42],[-9,109],[12,64],[-21,110],[3,31],[-8,55],[-10,3],[5,33],[-1,49],[-7,13],[-47,23],[-9,-51],[-7,-4],[-9,100],[3,45],[-14,32],[-12,9],[-2,63],[-7,58],[-11,45],[-15,6],[-5,44],[-24,48],[0,111],[-4,67],[-12,7],[-10,38],[-13,17],[-13,61],[0,42],[-9,35],[-5,51],[-14,44],[-28,62],[-40,72],[-26,75],[-11,-2],[-26,-33],[-31,29],[-1,-35],[-26,-69],[9,-99],[-3,-28],[-10,-7],[-15,15],[-5,26],[-24,-22],[-31,-73],[-65,-132],[-6,2],[7,44],[-10,12],[-7,-39],[-15,-41],[-36,1],[13,-49],[2,-38],[16,-22],[30,53],[29,40],[27,84],[3,-13],[-24,-82],[-28,-43],[-35,-66],[-12,-14],[-10,33],[-16,22],[-23,51],[-19,7],[-17,-28],[-13,94],[-5,119],[8,42],[0,-105],[6,-91],[7,-22],[10,39],[1,97],[-17,105],[-18,60],[-17,13],[-15,40],[-16,71],[-27,47],[-22,69],[-41,95],[-47,76],[-58,70],[-31,20],[-62,18],[-33,-5],[-47,-25],[-65,-49],[-32,-15],[-7,10],[-63,-51]],[[26061,57664],[39,-2],[136,6],[183,1]],[[26419,57669],[196,-2],[-1,6],[143,10]],[[26757,57683],[-3,-72],[-31,-68],[-26,-105],[1,-36],[-8,-26],[3,-64],[30,-82],[17,-7],[23,-104],[23,-51],[28,16],[8,-12],[8,-64],[3,-64],[12,-30],[3,-50],[11,-52],[0,-51],[7,-68],[12,-24],[8,-67],[14,-48],[11,-101],[9,-1],[19,-72],[29,-52],[29,-98],[4,-61],[9,-41],[1,-40],[13,-38],[12,-2],[17,-37],[18,-93],[15,-26],[4,-24],[-7,-43],[4,-39],[-4,-32],[28,-82],[-4,-30],[13,-42],[11,5],[-2,-41],[8,-45],[27,-55],[9,1],[16,-58],[19,-43],[-5,-67],[12,-64],[2,-49],[9,-15],[0,-85],[4,-32],[-5,-45],[6,-8],[4,-48],[-5,-41],[12,-48],[25,-26],[0,-23],[23,-81],[-4,-34],[15,-105],[4,4],[6,-76],[-10,-44],[11,-67],[-4,-26],[5,-62],[21,-34],[11,17],[22,-71],[9,-4]],[[27376,54230],[12,-12],[-5,-64],[-14,-29],[-10,15],[-6,-48],[10,-8],[-16,-60],[-21,23],[-3,-56],[11,-11],[-9,-59],[-17,-54],[-17,13],[-3,-16],[12,-33],[8,5],[-1,-84],[-7,-62],[-28,-26],[0,-22],[17,10],[6,-24],[-23,-131],[-6,-91],[7,-13],[-5,-30],[-5,-97],[-13,-22],[-8,-59],[-9,-3],[0,-71],[-7,-69],[-7,2],[-12,-44],[13,-24],[11,24],[1,-81],[-16,-161],[5,-70]],[[25527,65491],[15,-41],[12,20],[7,-15],[-6,-40],[27,-23],[29,1],[28,25],[52,75],[31,59]],[[25722,65552],[90,-1],[198,-1],[187,1],[87,1],[0,-75]],[[26284,65477],[1,-242],[0,-1380],[-3,-437],[0,-441],[-2,-278],[0,-238]],[[26280,62461],[-19,-45],[-2,-20],[16,-71],[2,-31],[-11,-38],[3,-37],[17,-7],[4,-25],[-13,-46],[6,-44],[-22,3],[-16,-23],[-9,5],[-35,-63],[-14,-39],[-19,10],[-10,43],[-26,-7],[-16,7],[-12,-33],[4,-125],[6,-71],[-16,-40],[-7,-44],[-30,-34],[-19,-161],[-19,-39],[-14,24],[-10,-13],[-6,-63],[-16,-72],[1,-87],[-5,-70],[-8,-24],[-23,-18],[-4,-36],[-13,58],[-22,1],[-26,55],[-1,94],[-21,67],[-8,-2],[1,-34],[13,-12],[-1,-25],[-13,-2],[-8,-29],[-8,23],[-9,-29],[9,-17],[-6,-42],[-18,-9],[-2,-94],[5,-38],[-23,-9],[-2,-63],[-11,-29],[-7,24],[5,51],[-10,10],[-10,-25],[-10,22],[-12,89],[-17,-2],[-14,-52],[-29,-27],[-10,-30],[-5,-97],[-7,-27],[-11,-3],[-8,62],[-18,13],[-22,57],[-22,44],[-20,6],[-14,-37],[-14,19],[-6,49],[-8,6],[-7,-66],[11,-64],[-13,-40],[-11,3],[3,56],[-5,30],[-42,-31],[-11,52],[-9,4],[-10,-40],[10,-91],[-12,-47],[-23,32]],[[25386,60941],[-11,2],[-4,34],[12,-9],[-16,92],[24,-8],[-15,29],[15,67],[1,60],[-8,26],[19,19],[2,38],[-12,-19],[22,73],[-14,60],[-4,59],[9,-10],[9,68],[5,-35],[7,41],[8,-33],[13,102],[8,9],[13,84],[-2,38],[24,42],[0,66],[9,83],[7,32],[14,12],[14,76],[-1,42],[-15,94],[11,111],[-18,40],[1,79],[-16,55],[-3,73],[19,60],[-9,50],[2,60],[6,32],[13,9],[0,347],[-1,0],[0,397],[1,12],[0,294],[1,247],[0,1196],[1,254]],[[21479,63506],[257,-1],[104,0],[263,-1],[273,1],[163,0],[279,-1],[263,-1],[144,0],[133,0]],[[23358,63503],[16,-60],[13,-11],[1,-43],[13,6],[5,-36],[26,-14],[8,41],[24,-13],[-4,-28],[7,-33],[11,-11],[-4,-42],[-9,-1],[3,-36],[12,16],[-1,-49],[-10,-6],[-13,24],[-5,-75],[-15,-19],[-7,-41],[2,-41],[-17,-24],[1,-48],[15,-41],[1,-29],[16,-31],[12,-61],[14,0],[5,-21],[-9,-25],[6,-79],[18,-52],[-3,-28],[15,-13],[5,-31],[14,-11],[9,16],[5,-31],[20,-3],[-5,-41],[0,-444],[-1,-120],[0,-696],[-1,-52],[0,-779],[-1,-97],[0,-281]],[[23550,60009],[-249,1],[-196,-1],[-169,0],[-218,0],[-208,-1],[-258,2],[-103,0],[-154,3],[-149,-4],[-209,-4],[-155,-3]],[[30863,68384],[14,7],[-7,-35],[-7,28]],[[30827,68345],[7,83],[6,-34],[19,-55],[-15,-45],[-5,34],[-12,17]],[[30819,68560],[3,42],[11,-71],[-14,29]],[[30732,68434],[14,2],[3,-41],[-17,39]],[[30712,67988],[8,37],[1,-75],[-9,38]],[[30703,68290],[8,41],[25,45],[11,-50],[-9,-8],[14,-78],[-11,-39],[-18,-13],[-12,24],[5,25],[-3,50],[-10,3]],[[30697,68452],[13,68],[2,64],[9,25],[6,-26],[-9,-35],[1,-36],[-8,-70],[-10,-29],[-4,39]],[[30213,67064],[-35,81],[0,69],[6,42],[-41,128],[-9,53],[8,101],[-4,10],[6,88],[-6,23],[-5,313],[0,89],[-11,861],[-15,757]],[[30107,69679],[21,15],[0,33],[25,-41],[-1,-38],[7,-43],[11,-15],[13,85],[2,86],[-6,28],[12,36],[8,-3],[12,-44],[21,-9],[1,39],[-24,83],[-1,32],[22,107],[14,27],[11,43],[8,-1],[15,41],[24,38],[-9,71],[21,66],[23,45],[5,56],[-7,28],[-14,0],[0,66],[10,38],[-8,14],[15,51],[5,46],[-15,44],[16,115],[8,18],[-1,37],[17,32],[14,59],[11,8],[17,324],[215,890],[13,-3],[27,-38],[11,4],[-3,-58],[1,-141],[4,-18],[37,-73],[25,43],[26,30],[28,2],[11,52],[30,11],[25,-12],[0,62],[16,23],[25,-6],[23,-36],[4,-32],[33,-68],[36,-132],[0,-15],[28,-51],[0,-545],[3,-693],[-1,-71],[9,-30],[-15,-39],[14,-70],[-15,-34],[1,-136],[23,-18],[3,20],[10,-57],[19,-28],[30,-23],[14,19],[8,-62],[1,-56],[-13,8],[-11,-22],[7,-68],[16,-63],[-9,-73],[-10,-41],[23,-116],[0,-23],[18,-40],[13,28],[1,46],[18,-30],[19,-3],[13,-56],[6,-51],[-8,-14],[10,-32],[12,-98],[15,-38],[1,-118],[-12,-50],[-10,2],[-4,-32],[-32,-113],[-24,-25],[4,-22],[-15,-8],[6,42],[-4,85],[-27,-30],[12,-58],[-12,-43],[-11,13],[-13,-58],[-13,18],[-10,-12],[2,-35],[16,-40],[-22,-57],[-13,46],[-6,57],[-13,-10],[-3,-38],[-8,3],[-7,59],[-7,-62],[-9,-14],[-8,-74],[-7,19],[-6,-48],[-4,42],[-20,-51],[-5,27],[-12,-92],[-16,39],[0,36],[-10,-21],[-11,9],[2,-66],[-16,-71],[11,-33],[-35,-16],[-23,32],[-16,107],[6,29],[22,27],[-9,45],[-13,-35],[2,44],[-10,-3],[1,-68],[-5,0],[0,64],[-24,-56],[5,-35],[-6,-43],[14,-67],[-3,-45],[-23,-77],[9,-11],[-12,-31],[11,-56],[-5,-69],[-16,-10],[-3,85],[6,4],[-3,55],[-17,45],[-4,64],[15,51],[-19,40],[6,26],[-10,10],[-18,-21],[1,113],[11,44],[1,50],[-7,14],[-7,-46],[-14,-40],[-11,33],[-7,-32],[-15,-4],[14,-81],[-3,-48],[-12,-23],[-10,-71],[-4,-72],[-13,-78],[19,-30],[-13,-28],[10,-57],[-10,-38],[-27,4],[-13,-72],[-15,-1],[-2,-58],[-11,-9],[0,52],[7,37],[-9,15],[-3,-30],[-12,-13],[5,65],[-18,-9],[1,-52],[-16,-37],[-5,-50],[-12,51],[-2,-47],[-15,-32],[-12,26],[-3,-66],[-13,51],[-13,-92],[-23,-27],[-5,-19],[-5,83],[-16,5],[-15,-42],[-5,-39],[-20,4],[-7,-49],[-20,4],[-10,-49],[-3,-50],[5,-29],[-8,-32],[-13,29],[-22,-31],[-9,-46],[6,-67],[-8,-43],[-10,-15],[3,-33],[-14,-24],[-14,4],[-10,-25],[-11,-98],[5,-19],[-14,-80],[1,-22],[-14,-74],[-9,-13]],[[30332,65028],[13,-24],[29,5],[18,17],[12,70],[16,-105],[-1,-30],[-10,-16],[-33,4],[-41,60],[-3,19]],[[30177,65078],[18,0],[19,90],[27,60],[11,-15],[7,-70],[15,-27],[10,40],[-1,-84],[-41,1],[-31,-9],[-18,-47],[-16,61]],[[29500,66699],[224,-22]],[[29724,66677],[199,-23],[126,-12],[10,46],[21,1],[-1,62],[15,36],[19,-18],[9,62],[36,32],[15,-30],[8,13]],[[30181,66846],[2,-72],[9,-136],[25,-47],[12,42],[14,-34],[-16,-90],[-23,-7],[-38,-35],[12,-50],[-18,-50],[-7,9],[-5,-59],[-4,41],[-11,-53],[11,-74],[-13,10],[-5,-28],[8,-26],[-5,-27],[14,-14],[6,36],[14,8],[9,-47],[24,-28],[12,-42],[2,-46],[21,-93],[-1,-50],[-7,-39],[-12,-5],[3,-24],[21,-51],[12,8],[8,-24],[8,-83],[-5,-50],[13,-48],[23,-35],[34,-12],[9,-23],[19,44],[19,8],[27,33],[6,25],[-1,79],[-6,61],[-13,-31],[0,97],[-5,55],[-15,34],[-9,-7],[-5,-39],[-11,59],[13,11],[21,-18],[23,-58],[18,-123],[9,-119],[2,-137],[-15,-129],[-2,-44],[-7,9],[12,112],[-7,23],[-26,-5],[-43,-40],[-30,8],[-7,-28],[-17,-7],[-15,-62],[-33,-10],[-34,-66],[-16,-47],[-21,-28],[-23,-15],[6,26],[35,34],[16,47],[24,49],[5,40],[-2,117],[6,41],[-26,26],[0,-59],[-7,14],[-4,-50],[-14,3],[2,-32],[-31,-11],[-7,-49],[2,-44],[-6,-29],[-25,-23],[-12,17],[-10,-14]],[[30097,65246],[-3,189],[-18,18]],[[30076,65453],[-8,41]],[[30068,65494],[-10,48],[-19,33],[-5,56],[2,81],[-12,-6],[0,147],[-116,-13]],[[29908,65840],[-1,18],[-202,13],[-22,-4],[-42,6],[-3,-39],[-14,-6],[1,45],[-54,3],[-118,14],[-15,-1]],[[29438,65889],[-6,42],[34,395],[34,373]],[[22823,73980],[83,-1],[266,0],[229,-1],[0,449],[27,-37],[28,20],[29,-53],[8,-35],[14,-202],[7,-25],[18,-251],[-6,-69],[4,-54],[14,-43],[29,-45],[24,-12],[8,19],[44,-11],[11,-58],[61,-7],[48,-21],[8,-66],[-3,-49],[6,-12],[42,2],[5,15],[22,-2],[23,20],[-1,48],[33,46],[39,18],[9,-22],[53,0],[72,-98],[26,4],[1,-49],[-19,-7],[-4,-37],[16,-31],[41,13],[15,-54],[-6,-34],[6,-49],[24,-127],[25,27],[-8,84],[13,45],[15,-11],[42,16],[16,-44],[-2,-65],[17,-41],[14,9],[9,-36],[41,-9],[5,-67],[-4,-31],[20,-21],[23,13],[-3,-75],[22,28],[14,-22],[19,24],[34,17],[46,113],[17,13],[13,32],[25,30],[14,-13],[0,-67],[17,-17],[-6,-26],[10,-48],[16,-3],[35,29],[6,-29],[25,15],[26,-18],[19,13],[48,8],[29,-28],[9,-66],[34,-53],[15,35],[20,9],[13,-20],[23,6],[11,-18],[25,22],[-27,-56],[-10,20],[-4,-34],[-17,-15],[-26,-58],[-37,-34],[-14,-37],[-52,-44],[-48,-54],[-15,-6],[-42,-45],[-4,-14],[-51,-77],[-37,-79],[-72,-183],[-17,-69],[-55,-178],[-26,-72],[-26,-41],[-26,-88],[-11,-11],[-28,-83],[-10,-10],[-61,-134],[-9,-25],[8,-50],[14,-46]],[[24276,71309],[-21,50],[-14,-11],[-3,-29],[-16,-14],[9,-19],[-11,-43],[-22,16],[0,-685],[-10,-10],[-6,-59],[-24,7],[-10,-56],[-14,13],[-8,-38],[-25,-23],[-20,-47],[-20,-148],[-22,-49],[-7,-114],[1,-53],[21,-21],[10,9],[14,-32],[-1,-31],[23,-85],[-1,-50],[-15,-47],[1,-29],[-17,-49],[2,-91],[-4,-32],[6,-91],[-16,-58],[11,-34],[-2,-65],[6,-41],[-7,-42],[3,-75],[-12,-80],[5,-36],[16,-27],[31,-87],[-1,-24],[14,-16],[9,-44],[50,-9],[12,-17],[10,-78],[14,-38],[50,-51],[22,-39],[14,-40],[0,-53],[8,-15],[5,-84],[17,-45],[27,-41],[3,-29],[36,-90],[39,-29],[15,-55],[30,-136],[9,-75],[-8,-113],[2,-71],[10,-37],[-4,-44],[8,-53]],[[24498,67577],[-204,0],[-224,0],[-116,-1],[-223,0],[-180,1],[-313,0],[-199,0]],[[23039,67577],[0,1215],[1,101],[-1,302],[0,479],[-10,65],[-36,59],[-18,4],[-14,55],[-9,73],[-26,98],[5,46],[25,62],[22,39],[12,63],[14,46],[4,128]],[[23008,70412],[-4,106],[6,37],[-11,188],[-1,129],[-13,27],[-20,98],[-5,49],[-4,119],[-11,57],[-2,39],[5,68],[-6,112],[5,17],[6,117],[-6,-11],[-11,51],[1,171],[-5,92],[-2,114],[3,50],[-7,103],[1,217],[-5,3],[-7,89],[-11,92],[-8,17],[-11,101],[-3,58],[-11,34],[-5,121],[-16,101],[-3,110],[7,53],[-8,93],[3,52],[-1,107],[-7,59],[7,76],[9,24],[3,62],[-15,80],[-8,124],[-6,24],[-11,98],[3,40]],[[28860,63072],[14,65]],[[28874,63137],[9,32],[5,61],[12,42]],[[28900,63272],[17,44],[38,25],[25,45],[0,69],[16,25],[7,33],[34,72],[18,15],[12,51],[10,-8],[17,39],[-14,71],[-20,41],[-7,52],[-21,54],[-7,68],[-25,19],[-4,45],[2,95],[-10,37],[-17,-5],[-9,14],[-2,83],[7,30],[-8,22],[9,100],[11,-5],[23,113],[-24,120],[13,48],[18,29],[16,56],[-4,24],[17,35],[11,49],[14,124],[22,69],[16,13]],[[29101,65083],[88,-173],[132,-246],[3,-19],[-20,-187],[-12,-65],[-4,-65],[-6,-22]],[[29282,64306],[-7,-34]],[[29275,64272],[-37,-35],[-4,-84],[-9,-16],[-3,-50]],[[29222,64087],[0,-43],[19,-28],[16,18],[24,-44],[14,-9],[4,44],[4,-90],[-3,-107],[-14,-182],[-13,-247],[-5,-173],[-40,-241],[-11,-48],[-9,-9],[3,-32],[-10,-55],[-20,-63],[-1,-19],[-31,-55],[-32,-108],[-21,-118],[-22,-149],[-20,-60],[-19,-14],[-11,14],[6,88],[15,97],[3,69],[-21,37],[-18,4],[-6,25],[-18,-3],[-7,-36],[-9,24],[-2,47],[-18,37],[-3,30],[-10,-12],[-15,68],[-7,-8],[-18,58],[-10,56],[-19,25],[6,140],[-12,29]],[[28861,63044],[-1,28]],[[28814,58772],[18,-19],[12,-74],[1,-47],[-15,9],[2,45],[-18,86]],[[28734,57762],[8,60],[8,0],[47,86],[15,12],[14,33],[12,2],[16,35],[13,10],[5,74],[11,252],[0,54],[-8,95],[-6,105],[3,-2],[11,-106],[5,-95],[-3,-136],[-15,-286],[-24,-8],[-50,-64],[-43,-78],[-19,-43]],[[27156,59531],[84,-16],[55,-3],[95,-15],[37,4],[114,-21],[48,-2],[170,-2],[279,0],[74,4],[264,0],[106,-1],[0,10],[31,-2],[120,0],[142,0]],[[28775,59487],[2,-58],[17,-245],[16,-158],[23,-156],[35,-271],[-9,14],[-18,146],[-14,80],[-13,11],[-9,174],[-16,155],[-8,23],[3,39],[-5,89],[-10,30],[-4,48],[-25,13],[-1,40],[-13,-20],[6,-61],[16,-48],[11,10],[0,-67],[15,-135],[9,-61],[-1,-26],[11,-75],[0,-47],[-19,64],[1,37],[-13,61],[-3,38],[-12,12],[6,-38],[-3,-28],[14,-39],[-31,25],[-32,118],[-15,12],[19,-97],[10,-17],[6,-52],[-9,-23],[-24,-14],[-21,71],[7,-63],[10,-27],[-7,-14],[-31,45],[-13,5],[-5,29],[-18,24],[23,-73],[21,-23],[-7,-19],[-24,-8],[-13,-62],[-16,-22],[-17,1],[-8,31],[-20,12],[-12,121],[0,61],[12,78],[-5,13],[-14,-76],[-2,-76],[8,-94],[11,-77],[-5,-42],[8,-34],[40,10],[37,47],[9,-49],[13,6],[13,30],[26,24],[32,-1],[14,-41],[0,-43],[-14,-78],[3,-55],[2,-164],[9,5],[7,115],[3,150],[14,40],[-6,33],[14,20],[25,-21],[16,-94],[7,-65],[-4,-52],[7,-98],[-8,-25],[4,-54],[-13,-54],[-17,-10],[-6,18],[-10,-16],[-6,-40],[-10,-9],[-3,-43],[-7,-11],[-7,-71],[-13,-15],[-3,-46],[-17,-25],[-3,-24],[-26,25],[-29,-4],[-26,18],[-5,25],[-10,-14],[-15,45],[-13,115],[31,3],[5,45],[-28,-25],[-12,11],[-9,-33],[9,-57],[6,-86],[-7,0],[-29,47],[-14,-10],[-20,34],[-31,30],[-23,48],[-1,-29],[17,-61],[21,-5],[63,-102],[46,-28],[8,-31],[1,-40],[-7,-56],[-13,-54],[1,-28],[-9,-53],[-15,-42],[-50,-112],[-50,112],[1,-65],[24,-55],[36,-43],[36,80],[13,2],[27,30],[4,62],[11,-14],[3,-66],[8,-31],[18,-5],[-9,75],[20,-29],[2,-85],[-17,-75],[-18,-13],[0,-34],[-11,-20],[-14,-84],[-7,-71],[-17,20],[1,80],[-10,19],[1,-82],[-16,-7],[42,-64],[23,123],[33,120],[24,75],[47,176],[8,-21],[-28,-83],[-26,-96],[-22,-62],[-21,-78],[-18,-82],[-21,-115],[-3,-32],[-4,67],[-35,55],[-39,1],[-60,-38],[-29,-33],[-20,-32],[-32,-81],[-39,-75],[-26,-63],[-37,-113],[-14,-55],[-25,-128],[-14,-111],[-10,-112],[-9,-69],[-4,-69],[-13,7],[-3,34],[-21,21],[-23,9],[-28,-2],[-30,-12],[-35,-42],[-8,-17]],[[28030,56345],[-93,330],[-104,362],[-31,116],[-88,302],[-61,1],[-74,7],[-178,10],[4,135],[-42,199],[-30,-73],[-5,21],[7,48],[-2,53],[-55,13],[-105,16],[-188,29],[-23,-20],[-5,39],[-12,-52],[-7,9],[-20,-27],[-6,5],[-29,-47],[-28,-60],[-7,21],[-91,-99]],[[26419,57669],[9,275],[22,48],[3,-26],[28,4],[22,63],[-5,57],[9,25],[-4,44],[17,62],[13,15],[8,48],[16,8],[15,45],[80,7],[11,50],[22,32],[5,26],[10,-7],[16,48],[4,35],[16,4],[8,40],[21,29],[24,-18],[26,115],[-3,63],[10,30],[15,-36],[20,98],[33,70],[10,-40],[-5,-70],[15,-20],[28,62],[15,93],[15,35],[17,21],[12,-5],[9,33],[19,-12],[4,-50],[15,0],[14,28],[33,200],[21,53],[11,12],[8,-23],[17,0],[-10,74],[7,65],[6,12],[-4,80],[9,60]],[[20923,73979],[188,-1],[322,0],[212,0],[92,0],[193,1],[160,-1],[135,0],[141,1],[256,1],[201,0]],[[23008,70412],[-210,-1],[-186,1],[-119,1],[-252,4],[-192,2],[-179,2],[-253,1],[-123,0],[-292,1],[-173,0],[-105,0]],[[20924,70423],[0,2828],[-1,17],[0,711]],[[23550,60009],[0,-581]],[[23550,59428],[6,-130],[7,-206],[6,-127],[14,-355],[4,-82],[15,-387],[-2,-319],[-3,-261],[-1,-235],[-2,-170],[-2,-417],[-5,-642]],[[23587,56097],[-9,-25],[-19,73],[-16,-13],[-1,23],[-17,-1],[-18,79],[-15,-16],[-12,5],[-21,78],[-7,58],[-22,2],[-23,85],[-27,28],[-7,-68],[-11,-32],[-13,-7],[-34,4],[0,15],[-21,-7],[-7,61],[-10,3],[-24,-52],[-20,3],[1,-45],[-13,9],[-9,-31],[-29,61],[-1,-30],[-30,-29],[-15,13],[1,-20],[-14,8],[-8,-90],[-14,-14],[-19,19],[-7,-81],[-12,-3],[-1,22],[-17,79],[-21,-6],[-8,57],[-11,-4],[-16,30],[12,58],[-25,16],[-8,-86],[-18,-9],[-4,42],[-13,14],[-6,-33],[-9,25],[-6,95],[-5,11],[-14,-27],[-6,15],[3,-73],[-16,-57],[-13,19],[11,-42],[-13,-22],[3,-64],[-11,-31],[-11,14],[-12,94],[10,30],[-2,66],[-9,28],[-10,-19],[-2,-42],[-13,19],[-9,-7],[-11,-64],[-20,5],[-5,30],[1,63],[-12,19],[-16,-27],[-11,25],[3,41],[-19,42],[-9,-3],[-12,-58],[-29,-92],[-12,-9],[-16,34],[-9,3],[4,65],[-5,-1],[7,60],[-7,20],[-13,-14],[-19,11],[-10,91],[8,35],[-4,50],[-17,-47],[-20,22],[-15,0],[-19,28],[-11,-40],[-3,-44],[-20,-26],[-24,96],[-21,23],[-12,-37],[-21,4],[-13,26],[-13,3],[-17,24],[-19,46],[-16,-27],[-9,15],[-12,-11],[-2,20],[-18,-3],[-5,140],[-18,56],[4,22],[-16,5],[-18,57],[-4,-92],[-12,-4],[-22,47],[-17,2],[-6,-49],[-30,15],[-24,78],[-37,143],[-20,-19],[0,2257],[-123,0],[-179,0],[-150,0],[-175,1],[-210,0]],[[21214,59429],[1,119],[0,463]],[[27478,65805],[48,61],[45,74],[9,24],[10,60],[13,3],[3,-28],[36,72],[47,74]],[[27689,66145],[0,-314],[1,-2],[247,-1],[77,3],[167,-2],[243,1],[105,3],[111,-4],[169,-1],[107,2],[14,-59],[5,4],[8,-100],[15,4],[27,-34],[10,-49],[-8,-32],[14,-21],[2,-159],[-8,-12],[25,-113],[1,-34],[11,5],[15,-33],[-2,-18],[25,3],[3,-22],[16,8],[-1,-30],[13,-55]],[[28900,63272],[-23,37],[-22,6],[-24,-20],[-15,-34],[-20,-82]],[[28796,63179],[-217,-1],[-118,-1],[-151,0],[-236,3],[-305,-2]],[[27769,63178],[-167,0],[-124,0],[0,1068]],[[27478,64246],[0,1559]],[[20920,69320],[5,4],[0,872],[-1,227]],[[23039,67577],[-40,0],[5,-22],[-7,-37],[22,-64],[-1,-110],[-14,-5],[8,-52],[-3,-31],[24,-3],[2,-83],[9,-35],[-7,-66],[-14,-16],[5,-56],[-8,-28],[6,-21],[-12,-46],[0,-80],[-11,-16],[-3,-52],[-11,-26],[1,-76],[15,-27],[17,-60],[10,-87],[-4,-45],[13,-31]],[[23041,66402],[-17,-7],[-5,30],[-24,-5],[-7,53],[-21,66],[7,52],[-11,16],[-21,3],[-1,40],[-28,35],[-12,-17],[-8,48],[-43,13],[-5,28],[-18,20],[-1,38],[-25,25],[-15,-14],[-15,13],[-7,-23],[-18,14],[-16,-13],[-16,11],[-18,-17],[-25,9],[-20,21],[-8,-11],[-9,-74],[-12,-29],[-19,-9],[-31,69],[-36,62],[-58,85],[-9,59],[-112,0],[-186,-1],[-153,1],[-214,-1],[-206,-2],[-62,4],[-178,0],[-143,1],[-293,1]],[[20922,66996],[0,345],[-1,8],[0,1828],[-1,143]],[[23587,56097],[8,8],[0,-54],[11,-25],[9,6],[-2,-36],[10,-9],[5,27],[8,-18],[3,29],[21,-27],[11,46],[12,-17],[5,-30],[6,21],[16,-22]],[[23710,55996],[0,0]],[[23710,55996],[1,-11],[0,-608]],[[23711,55377],[0,-1196],[8,-12],[11,-72],[18,-31],[9,-83],[15,-55],[-2,-34],[8,-56],[-7,-42],[3,-45],[-7,-28],[6,-51],[9,-32],[13,-6],[3,-35],[-6,-27],[14,-36],[-1,-32],[8,-15],[3,-49],[-6,-53],[12,-47],[6,2],[6,-113],[19,12],[-6,-102],[8,-31],[2,-54],[-13,-21],[-1,-33],[12,-55],[-9,-22],[-3,-38],[4,-63],[-9,-33],[-9,-63],[-3,-73],[-12,-25],[-2,-68],[-14,-30],[-3,-40],[7,-24],[5,-91],[-13,-51],[-6,-74],[16,-52],[1,-53],[-5,-39],[9,-79],[-12,-70],[9,-19],[-11,-51],[-14,-39],[-7,-78],[-13,-74],[-17,-45],[-2,-25],[9,-30],[17,-94]],[[23768,51502],[0,-14],[-46,3],[-37,-40],[-128,-208],[-28,-61],[-10,-54],[-15,7],[22,71],[9,47],[13,-1],[9,19],[8,46],[9,-16],[11,8],[-15,55],[-54,-54],[-8,26],[17,90],[4,80],[0,74],[-12,5],[-5,23],[-17,-29],[-10,-41],[-4,-50],[-15,-24],[-6,46],[-18,-45],[-2,-35],[9,-32],[-10,-50],[10,-55],[20,-17],[-6,-54],[11,-20],[2,-78],[-2,-67],[-8,-31],[-9,10],[-19,-71],[-22,-65],[-3,26],[-13,-8],[-2,-88],[6,-26],[13,44],[26,65],[3,19],[32,99],[15,23],[0,45],[26,-15],[-20,-61],[-62,-153],[-24,-69],[-4,-25],[-32,-91],[-40,-143],[-15,-8],[-69,-146],[-35,-80],[-53,-89],[-61,-112],[-34,-87],[-13,-42],[-15,-74],[-53,-111],[-24,-68],[-47,-155],[-32,-143],[-11,-83],[-14,-59],[-33,-221],[-24,-197],[-11,-127],[-10,-192],[-2,-108],[1,-103],[8,-205],[14,-207],[12,-134],[16,-229],[11,-260],[-7,47],[-5,147],[-12,189],[-11,152],[-24,16],[18,27],[-9,83],[-6,87],[-9,88],[-3,106],[1,138],[-1,122],[11,276],[16,180],[19,148],[14,70],[10,87],[8,35],[-2,47],[13,86],[18,38],[9,45],[18,127],[28,58],[8,53],[16,2],[35,105],[23,49],[24,23],[-4,43],[9,29],[-9,16],[-16,-41],[-20,-35],[-11,-40],[-17,-5],[-6,40],[0,60],[-18,12],[-6,-31],[0,-84],[-5,-25],[6,-41],[-4,-36],[-20,-56],[-17,-61],[-8,-1],[-18,30],[-3,43],[-32,-61],[-17,-67],[21,-22],[24,57],[8,-74],[-7,-23],[-38,-223],[-12,-2],[-6,55],[-21,-10],[-4,18],[-32,-11],[-11,12],[-6,-38],[12,-32],[15,-1],[11,17],[-4,-64],[7,-48],[15,-34],[17,-18],[-19,-146],[-13,-185],[-12,-106],[-20,-27],[-7,-27],[-11,18],[14,21],[3,59],[-8,3],[-6,-31],[-29,-75],[4,-32],[24,-16],[25,38],[8,2],[-5,-116],[3,-4],[-4,-202],[-6,-207],[-4,-3],[10,-230],[8,-65],[1,-84],[9,-1],[23,-215],[5,-29],[-9,-47],[4,-34],[-1,-62],[8,-72],[20,-11],[1,-35],[12,18],[2,-127],[-23,7],[-13,-8],[0,-19],[-21,-12],[-6,-99],[-20,11],[-7,40],[-15,3],[-6,55],[-11,4],[-18,98],[-31,12],[-11,33],[-17,-9],[-7,16],[-23,-17],[-4,18],[-13,-1],[-3,-29],[-10,31],[-20,-14],[-8,22],[-6,-22],[-14,20],[-8,30],[3,25],[-16,1],[-3,44],[-15,-1],[-21,75],[-9,-11],[-25,48],[-19,-22],[-13,35],[-15,69],[-10,8],[-2,33],[-8,12],[-18,-15],[-21,47],[-12,-3],[-8,20],[-12,-19],[-9,44],[6,50],[-10,56],[-11,13],[-3,98],[-6,42],[-3,79],[-8,25],[-1,49],[-7,64],[-17,43],[2,31],[-13,26],[-7,37],[5,20],[-11,46],[-9,7],[-1,52],[6,31],[-3,66],[4,29],[-4,86],[-16,25],[1,37],[-12,11],[9,30],[5,86],[-3,31],[4,60],[-13,10],[5,76],[-12,58],[-7,-14],[-7,46],[-9,-18],[-9,39],[-10,-6],[-18,90],[-9,11],[-3,38],[-8,-9],[-10,38],[0,50],[-7,26],[3,41],[-13,48],[2,47],[-17,16],[-7,81],[-10,23],[-8,70],[-35,53],[-4,53],[-7,-2],[-13,59],[2,38],[-17,100],[3,47],[-9,39],[9,29],[-12,11],[-9,47],[4,39],[-14,28],[1,33],[-15,28],[-4,51],[2,41],[-8,26],[-4,73],[-6,1],[-9,88],[-8,1],[-6,47],[1,76],[-7,106],[-18,46],[-10,40],[3,14],[-9,71],[-23,41],[-2,30],[-17,46],[-18,25],[-13,84],[0,21],[-26,21],[-6,44],[-23,7],[3,57],[-2,65],[-5,5],[-6,-67],[-6,27],[2,53],[-17,27],[-13,108],[-8,-4],[-9,32],[-13,-25],[-7,44],[-8,-27],[-37,-13],[-19,31],[-6,-8],[-15,22],[-22,-14],[-12,24],[-17,-7],[-4,-22],[-26,21],[-11,50],[-13,-3],[-20,40],[-14,-18],[-10,-111],[-34,19],[-9,-37],[-7,11],[-22,-32],[-7,10],[-8,-54],[2,-23],[-13,-40],[0,-50],[-7,0],[-2,-59],[-11,-30],[1,-33],[-7,-58],[2,-52],[-5,-48],[-10,-5],[-3,-72],[-5,-37],[10,-22],[-5,-38],[-18,-38],[-10,8],[-7,-67],[-17,-43],[-7,-36],[-4,-84],[-14,-8],[-17,15],[-15,-11],[-14,46],[-27,24],[-27,111],[-24,33],[-10,-8],[-22,38],[-18,77],[-13,24],[-39,21],[-17,28],[-19,61],[-11,12],[-19,81],[0,24],[-14,47],[-21,7],[-12,27],[-5,34],[-30,78],[-9,43],[-7,110],[-12,56],[-7,60],[-13,63],[0,59],[-7,57],[6,72],[-3,54],[2,55],[-7,68],[-11,30],[-3,44],[-13,39],[-1,30],[-13,34],[2,33],[-13,177],[-7,40],[-12,4],[-11,91],[-15,0],[-14,66],[-12,10],[-3,24],[-15,34],[-12,-4],[-7,28],[-31,39],[0,39],[-44,118],[-13,107],[-12,37],[-18,26],[-9,33],[-9,2],[-2,36],[-24,106],[-20,35],[-4,54],[-14,32],[-21,7],[-35,78],[-11,88],[-10,24],[-6,68],[-13,89],[-9,41],[-19,38],[-11,-19],[-11,41]],[[20232,53938],[-5,28],[-16,20],[1,23],[-10,25],[-3,38],[7,18],[1,101],[331,0],[250,-1],[226,0],[183,1],[0,1212],[2,254],[0,149],[2,417],[2,232],[0,1518],[1,434],[0,809],[-1,213],[11,0]],[[20922,66996],[0,-2328]],[[19530,64667],[-136,-2],[-143,-1],[-52,-3],[-133,1],[-93,2],[0,442],[1,219],[-1,508]],[[18973,65833],[0,676],[1,163],[0,1090],[-1,26],[0,516],[-1,267],[1,140]],[[18973,68711],[-2,175],[-1,282],[1,156],[75,2],[22,-12],[85,2],[11,8],[45,-5],[25,8],[87,-2],[194,5],[11,-7],[221,0],[316,0],[14,-4],[144,-2],[178,-1],[23,7],[132,0],[251,-2],[115,-1]],[[29908,65840],[0,-107],[3,-222],[0,-150],[-3,-209],[-12,-8],[3,-76],[-8,-29]],[[29891,65039],[-7,19],[-17,2],[-21,-24],[-18,3],[-13,-24],[-16,28],[-6,-37],[-49,-35],[-3,20],[-15,0],[-27,-33],[-6,21],[-26,1],[-7,-26],[-18,23],[-40,-27],[-3,60],[-9,-17],[-19,-82],[-12,0],[-22,-73],[-13,23],[-24,-58],[-7,13],[-23,-28],[5,-22],[-10,-32],[-29,-11],[-22,-54],[-9,15],[-15,-35]],[[29390,64649],[1,33],[-20,102],[68,130],[-19,97],[4,161],[5,295],[9,422]],[[23231,64184],[167,-9],[159,-8],[156,3],[130,8],[62,0],[197,12],[152,13],[101,14],[12,-38],[1,-33],[18,-14],[-1,-36],[16,-57],[10,-2],[1,-56],[11,-31],[19,-7]],[[24442,63943],[-8,-18],[-17,-144],[-2,-71],[6,-165],[14,-99],[7,-28],[-8,-66],[5,-38],[15,-34],[4,-28],[-3,-61],[26,-77],[15,-57],[11,-14],[10,-69],[13,-8],[11,-76],[16,-54],[59,-137],[20,-90],[3,-90],[8,-78],[-9,-36],[11,-85],[3,-65],[22,-69],[11,7],[11,32],[9,66],[19,4],[27,-45],[16,-5],[38,-82],[-2,-60],[-12,-29],[-13,-59],[10,-92],[-2,-37],[-22,-96],[-6,-99],[-17,-68],[-7,-52],[-1,-78],[5,-70],[18,-50],[20,-89],[25,-37],[11,-54],[8,0],[17,-60],[16,7],[3,-27],[-12,-24],[6,-50],[20,-5],[10,31],[13,-28],[1,-29],[15,-12],[23,-70],[-2,-40],[14,-3],[9,-42],[19,-24],[0,-57],[11,-68],[-13,-8],[2,-50],[26,-161],[-4,-62],[-15,-12],[-8,-56],[17,-53],[-1,-47],[13,-87],[11,-46],[-1,-53],[24,-56],[10,27],[-15,45],[14,12],[17,-56],[8,-53],[11,14]],[[25079,59990],[9,-21],[-5,-90],[-6,-46],[-10,-9],[-1,-38],[17,-37],[-3,-28],[-16,3],[-4,-44],[11,-58],[-8,-29],[-7,-71],[-13,-18],[-18,78],[-13,-13],[-13,-142]],[[24999,59427],[-8,-40],[-13,7],[2,31]],[[24980,59425],[6,38],[-5,49],[-21,2],[-4,-30],[9,-58]],[[24965,59426],[6,-22],[-7,-64],[10,-59],[-5,-37],[-24,-1],[0,-37],[21,-39],[1,-27],[-15,-15],[-30,16],[-3,-21],[28,-74],[1,-61],[-22,-40],[-3,-75],[-12,-23]],[[24911,58847],[-179,-6],[16,110],[23,58],[0,23],[15,53],[16,28],[3,43],[9,7],[5,133],[-22,44],[3,15],[-5,71],[-349,-1],[-323,1],[-141,0],[-222,1],[-210,1]],[[23358,63503],[-31,56],[8,79],[-24,76],[-2,77],[-20,19],[1,32],[-18,26],[-12,110],[1,39],[-11,34],[-1,40],[13,42],[-11,18],[-1,-33],[-20,3],[1,63]],[[26900,61666],[18,-20],[35,40],[23,11],[5,49],[3,98],[12,28],[16,-7],[6,30],[-3,101],[-10,105],[21,52],[0,67],[14,88],[12,27],[6,37],[13,-40],[11,-1],[12,-79],[-8,-40],[11,-20],[9,22],[3,42],[9,20],[9,-25],[4,111],[-10,29],[-3,43],[19,19],[-3,96],[8,41],[10,12],[3,55],[31,-7],[3,83],[25,76],[17,-19],[6,-52],[11,3],[21,47],[17,8],[11,54],[11,8],[8,48],[16,57],[30,89],[16,9],[4,83],[10,26],[-11,50],[12,48],[0,57],[10,23],[-4,60],[13,-11],[-2,52],[7,20],[0,122],[9,30],[0,58],[10,48],[5,61],[10,23],[5,65],[-3,95],[4,71],[-9,84],[-11,44],[9,50],[14,-1],[18,27]],[[27769,63178],[-3,-600],[10,7],[33,103],[12,-1],[10,67],[31,60],[13,75],[11,-2],[28,-36],[11,54],[38,158],[13,-10],[-13,-14],[13,-18],[2,-26],[20,-34],[17,1],[7,-18],[28,-4],[14,38],[-7,49],[13,-1],[-11,32],[14,-11],[9,37],[19,-23],[28,87],[24,-20],[17,-49],[8,-40],[18,20],[17,-23],[14,4],[0,-37],[-16,-15],[12,-69],[19,-41],[-2,-33],[9,-12],[5,-43],[-6,-63],[10,-14]],[[28258,62713],[-9,-45],[-21,-175],[-78,209],[-66,179],[-4,-62],[5,-28],[-6,-36],[7,-11],[-23,-106],[6,-15],[-11,-54],[10,-36],[-16,-62],[-7,-7],[-24,-88],[6,-16],[-14,-63],[-7,21],[-15,-66],[-10,-24],[0,36],[-23,-71],[-20,-130],[-35,101],[-9,-69],[-17,-82],[-1,-71],[-8,1],[-10,-57],[-15,-138],[-28,-94],[-46,53],[-17,109],[-31,47],[-14,-121],[3,-65],[-12,-60],[-22,-87],[6,-44],[-12,-21],[-25,-81],[-7,-56],[6,-29],[-8,-30],[-6,-60],[-10,-51],[-18,-51],[-30,-104],[-21,-112],[2,-36],[-12,-39],[4,-41],[17,-34],[-12,-40],[-18,-34],[-1,-32],[13,-4],[-4,-29],[-49,-99],[-10,68],[-12,-9],[-19,-47],[-41,-71],[-4,26],[-21,38],[-7,-51],[9,-43],[-19,-47],[-17,-5],[-37,-27],[-31,-51],[-27,75],[-11,45],[-8,-21],[-8,-55],[-23,-17],[-2,-30],[-13,-29],[-34,-7],[-19,48],[0,24],[-12,25],[-18,3],[-7,52],[-14,31],[-2,90],[-15,22],[-1,33],[18,34],[-11,29]],[[27075,60636],[-14,-4],[-28,30],[-3,37],[-8,1],[-7,40],[-9,-2],[-7,42],[-15,11],[-13,134],[-10,19],[-13,61],[1,34],[-15,19],[-8,39],[11,55],[-15,22],[-9,80],[-10,45],[-15,37],[0,38],[8,0],[3,55],[-4,20],[11,33],[1,58],[-8,36],[1,90]],[[24442,63943],[12,16],[-2,50],[4,90],[-11,35],[13,69],[31,43],[18,-1],[18,41],[1,61],[6,49],[1,67],[10,27],[14,66],[11,19],[6,106],[-1,98],[-13,75],[-13,6],[-20,85],[11,77],[2,74],[6,55],[19,21],[13,-15],[23,40],[31,-6],[24,17],[16,56],[10,13],[26,-1],[18,57],[16,21],[-1,66],[9,59],[0,52],[9,29],[28,49],[-1,41],[8,72],[-3,58],[7,61],[-7,37],[1,89],[-13,41],[-47,73],[-15,74],[3,60],[-16,63],[-22,37],[-2,26],[-22,35],[0,47]],[[24658,66423],[45,-2],[227,-2],[51,-5],[194,-11],[41,4],[152,1],[82,-5]],[[25450,66403],[-2,-124],[-8,-97],[10,-109],[21,-117],[12,-37],[3,-54],[13,-139],[4,-75],[14,-93],[8,-20],[2,-47]],[[25386,60941],[-8,-66],[-17,-35],[-11,-56],[7,-105],[20,-68],[-6,-50],[-48,-19],[-12,-11],[-18,-52],[-14,26],[-18,-44],[-2,-55],[-9,-65],[2,-25],[17,-65],[7,-66],[-10,-89],[-15,-10],[-13,13],[-19,52],[-35,40],[-14,39],[-31,42],[-19,5],[-13,-21],[-13,-42],[-9,-65],[-16,-52],[-4,-55],[14,-52]],[[20232,53938],[-214,0],[-254,0],[0,-524],[-234,-1]],[[24911,58847],[4,-38],[18,-49],[0,-45],[-24,23],[-9,-49],[20,-36],[-11,-33],[-11,-1],[-9,-52],[-17,-22],[-9,21],[-15,-41],[6,-68],[9,-17],[12,23],[3,-38],[-26,-42],[1,-53],[9,-10],[-3,-40],[-22,56],[-9,-9],[-5,-58],[9,-42],[-11,-75],[-8,88],[-23,-67],[-3,-46],[12,-6],[2,43],[15,-35],[-9,-47],[0,-45],[-16,-10],[4,-47],[15,-7],[6,-29],[-12,-43],[15,-58],[-10,-25],[-12,21],[-9,-26],[-9,-100],[-25,13],[-3,-49]],[[24751,57677],[17,-55],[0,-45],[-18,-44],[-31,-44],[-4,59],[-11,2],[-2,-29],[8,-44],[-5,-25],[6,-72],[-14,-19],[-5,34],[2,57],[-10,-27],[2,-35],[-7,-14],[6,-40],[20,-2],[2,-31],[-14,-50],[-11,18],[1,49],[-10,-21],[0,-64],[12,-61],[1,-27],[-13,-51],[6,-83],[-25,-57],[0,-63],[-10,1],[3,54],[-19,-1],[-5,-27],[8,-45],[-7,-30],[-19,-15],[-4,-71],[-16,43],[-9,-29],[14,-43],[20,1],[2,-25],[-13,-28],[-16,33],[-12,-32],[9,-51],[11,3],[3,-24],[-8,-61],[-24,-6],[7,-45],[-11,-16],[-5,43],[-16,-23],[-3,-29],[22,-34],[-18,-78],[7,-55],[16,-26],[-10,-34],[-8,18],[-24,-2],[-1,-59],[10,-26],[13,14],[8,-50],[-12,-18],[-16,22],[-7,35],[-17,-16],[-2,-28],[25,-51],[2,-33],[-28,-41],[13,-63],[-14,-80],[18,16],[-1,51],[14,-27],[-4,-54],[-17,-10],[-3,-19],[10,-24],[17,14],[10,73],[5,-32],[-23,-90],[0,-64],[12,-68],[3,42],[10,13],[2,-23],[-13,-52],[2,-75],[-3,-23],[-14,-8],[-11,14],[-5,-39],[23,-62],[-13,-59]],[[24512,55359],[-219,4],[-154,5],[-157,7],[-86,0],[-185,2]],[[16868,55911],[16,-5],[28,-53],[21,-22],[1,-25],[16,-61],[-5,-43],[-13,25],[-26,7],[-6,35],[0,74],[-20,16],[-12,52]],[[16867,55390],[9,4],[22,-117],[40,-130],[-10,4],[-11,-22],[-17,44],[-13,72],[-14,119],[-6,26]],[[16741,55926],[8,7],[1,-41],[-11,7],[2,27]],[[16637,56552],[8,-17],[11,14],[0,-30],[-17,-4],[-9,21],[7,16]],[[16596,55679],[14,7],[20,-35],[8,-31],[-13,-15],[-20,21],[-9,53]],[[16501,56608],[18,-7],[13,-22],[15,5],[19,-43],[19,-4],[7,39],[20,-20],[-11,-43],[-28,-11],[-17,-31],[-42,24],[-1,61],[-12,52]],[[16410,56520],[22,8],[9,21],[13,-8],[13,19],[0,-48],[18,-13],[1,-48],[-41,-54],[-16,37],[-19,86]],[[16354,56562],[16,17],[6,28],[6,-35],[13,-26],[-21,-6],[-20,22]],[[15305,65829],[155,-4],[9,6],[77,-1],[56,12],[28,-8],[115,2],[37,5],[95,-1],[45,-5],[154,-8],[114,-4],[197,0],[92,2]],[[16479,65825],[0,-1]],[[16479,65824],[0,-1448],[1,-34],[0,-756],[-1,-393],[-1,-13],[0,-457],[-1,-122],[2,-263],[26,-77],[115,-330],[114,-336],[63,-182],[99,-298],[125,-377],[62,-186],[127,-394],[313,-989],[234,-759],[70,-234],[147,-492]],[[17950,55027],[-416,-126],[-254,-89],[-3,98],[-9,62],[-8,20],[-14,-23],[-3,66],[1,71],[-8,41],[9,61],[-9,161],[-13,127],[-9,54],[-40,193],[-25,61],[-10,51],[-15,37],[-9,-2],[-19,95],[-26,59],[-14,15],[-20,57],[-25,88],[-24,39],[-2,-47],[-21,-23],[-18,14],[-10,25],[-15,6],[-4,38],[9,34],[1,43],[-14,116],[-11,64],[-17,50],[-35,1],[-22,-8],[-16,-36],[-14,38],[-28,16],[-37,59],[-12,2],[-24,53],[-17,141],[-10,10],[-17,53],[-4,-3],[-20,65],[-21,25],[-6,20],[-32,4],[-16,-24],[-18,25],[-22,-10],[-37,60],[-23,0],[-15,15],[-44,-7],[-48,-22],[-1,31],[-13,66],[-16,30],[-12,-4],[-6,32],[13,143],[-11,60],[8,119],[-17,54],[9,115],[1,140],[-18,56],[-14,7],[-3,-21],[-27,55],[-11,48],[10,131],[-2,50],[-11,53],[-27,14],[-27,102],[-18,101],[-24,25],[-17,66],[-5,81],[-16,47],[-7,37],[-14,35],[-6,99],[-5,35],[-20,29],[-15,104],[-25,94],[-33,64],[-11,45],[-8,80],[1,42],[-11,101],[2,92],[-11,16],[10,74],[18,-38],[9,30],[7,61],[8,141],[-21,149],[-12,43],[-10,11],[-9,-28],[-37,2],[-28,68],[-22,102],[-14,18],[-2,31],[-17,60],[-4,62],[5,129],[-12,89],[-2,52],[-12,14],[-6,33],[-1,64],[5,27],[2,77],[-6,134],[14,28],[16,8],[6,-24],[7,-88],[-9,-9],[11,-107],[-2,-27],[27,-23],[27,-80],[8,5],[14,-63],[12,3],[-6,40],[-12,19],[-10,87],[-7,109],[-21,54],[-2,46],[-22,26],[9,74],[-7,68],[-15,-1],[-14,68],[8,-9],[9,26],[0,40],[19,-2],[10,39],[-10,71],[-26,44],[-13,-30],[-14,-6],[2,-47],[-5,-46],[15,-51],[-12,-25],[-4,-44],[19,-56],[-7,-14],[-11,29],[7,-71],[-15,-16],[-9,38],[-26,61],[-13,-12],[-23,67],[-10,53],[-17,33],[-16,8],[-20,-33],[14,127],[3,48],[-12,92],[7,20],[-16,70],[-11,-10],[-1,67],[-17,106],[-20,51],[-14,19],[-22,64],[-37,176],[-14,29],[-8,41],[-31,90],[-24,116],[14,78],[-1,42],[-14,126],[-16,123],[-7,105],[3,99],[13,132],[-5,52],[0,65],[-14,92],[-4,108],[-16,36],[-6,55],[-35,133],[-7,6],[0,43],[-8,43],[-22,32],[-49,151],[4,63],[-4,70],[-13,74],[9,98],[14,109],[42,265],[11,95],[7,119],[-12,37],[-1,97],[4,2],[11,99],[12,244],[-5,126],[-15,128],[-8,96],[-9,-4],[-17,54],[10,74],[5,110],[-3,67]],[[28861,63044],[-1,28]],[[28874,63137],[-6,7],[-22,-81],[2,-39],[11,-31],[1,-49],[-8,-28],[0,-41],[14,-52],[7,-58],[21,-62],[10,-68],[3,-58],[-5,-35],[2,-126],[16,-48],[7,-43],[2,-74],[34,-137],[16,-29],[10,24],[4,-36],[5,-129],[4,-244]],[[29002,61700],[-105,1],[-74,9],[-2,72],[-25,1311],[0,86]],[[28426,62262],[21,70],[37,-118],[-36,-118]],[[28448,62096],[0,93],[-22,73]],[[6433,39922],[2,44],[19,76],[12,7],[13,93],[16,52],[3,58],[-17,94],[-4,45],[-1,105],[8,37],[18,-9],[18,-32],[3,-32],[22,-44],[15,-47],[12,6],[25,-32],[39,-77],[13,-19],[22,-61],[16,-57],[17,-76],[-3,-47],[1,-90],[8,-10],[16,12],[7,-57],[-1,-57],[22,-82],[22,-39],[5,-21],[-4,-44],[-13,-51],[-17,-47],[-14,-56],[-23,-38],[-20,-47],[-20,-18],[-16,15],[-9,-9],[-18,-66],[-16,-25],[-13,-48],[-14,-16],[-12,-59],[1,-33],[-8,-37],[-5,-59],[-20,-63],[-15,61],[-22,52],[-22,29],[-10,95],[5,113],[4,151],[-10,106],[1,46],[-9,12],[-5,114],[-7,66],[-9,8],[-8,108]],[[6254,41293],[5,69],[11,56],[14,6],[13,-33],[10,-78],[10,-50],[24,29],[18,35],[26,-21],[0,-16],[17,-51],[10,-15],[5,-40],[31,-37],[5,-32],[0,-52],[-8,-43],[-13,-40],[-6,3],[-17,-36],[-14,10],[-30,-53],[-21,-9],[-17,26],[-7,210],[-7,20],[-14,-24],[-26,50],[-13,58],[-6,58]],[[6253,40841],[7,29],[28,57],[10,-30],[-4,-44],[5,-17],[-13,-19],[-5,15],[-18,-23],[-10,32]],[[6153,41274],[7,27],[13,5],[26,-19],[17,-60],[8,-51],[-9,-65],[-19,-29],[-17,-5],[-7,60],[-3,73],[-14,32],[-2,32]],[[6084,41504],[6,52],[9,29],[-2,58],[17,-2],[3,-14],[54,-29],[10,34],[4,-42],[22,-13],[27,18],[17,-25],[-8,-55],[-18,-51],[-20,-21],[-35,32],[-26,31],[-15,-14],[-29,-5],[-16,17]],[[5814,42059],[43,9],[13,49],[4,35],[13,48],[14,15],[6,-27],[6,-70],[20,-84],[4,-52],[-4,-15],[3,-47],[18,-55],[3,57],[12,-2],[-4,-64],[8,-24],[-1,-29],[17,-70],[-12,-37],[-11,18],[-21,-29],[-6,26],[-17,31],[-25,11],[-30,-20],[-7,4],[-7,84],[-11,34],[-1,31],[-14,67],[0,61],[-13,45]],[[5394,42585],[1,40],[11,38],[4,49],[33,72],[8,26],[23,-18],[3,25],[16,-11],[8,14],[15,-20],[10,-37],[5,-45],[0,-46],[-11,-63],[1,-106],[-4,-28],[-28,-78],[-8,15],[-36,12],[-18,71],[-24,29],[-9,61]],[[5266,42374],[4,43],[12,44],[19,45],[2,38],[11,9],[2,-31],[-5,-57],[2,-36],[-20,-28],[-16,-97],[-12,30],[1,40]],[[24498,67577],[-5,-52],[10,-50],[-1,-74],[27,-41],[13,-76],[-18,-60],[-6,-52],[-9,-24],[1,-108],[8,-88],[0,-68],[12,-25],[13,-169],[33,-64],[47,-37],[17,-20],[18,-109],[0,-37]],[[23231,64184],[4,28],[-10,56],[-13,16],[-15,58],[2,37],[13,35],[-4,83],[10,51],[-7,27],[2,66],[-9,20],[1,42],[-7,30],[5,27],[-5,93],[12,17],[-24,24],[4,37],[-5,63],[13,41],[-20,27],[7,29],[-1,116],[-20,10],[2,77],[-9,1],[-2,-37],[-15,28],[3,56],[-11,39],[7,39],[-8,41],[11,18],[-6,54],[8,20],[3,46],[-12,23],[-1,38],[-14,61],[6,11],[3,66],[-16,6],[1,30],[-15,11],[5,16],[-15,25],[1,83],[-22,56],[-3,48],[10,25],[-14,103],[-11,29],[0,76],[9,44],[-4,44],[-14,8]],[[26280,62461],[18,48],[7,-2],[15,-54],[21,-28],[14,29],[14,-3],[8,29],[8,-8],[4,-69],[29,-32],[10,-84],[16,-83],[0,-66],[5,-26],[40,-41],[33,19],[29,-46],[3,-34],[15,-26],[5,-50],[28,-29],[14,66],[26,22],[15,-32],[28,-20],[11,-24],[5,-43],[13,24],[30,-1],[10,53],[18,34],[9,39],[15,0],[21,32],[7,-33],[-3,-44],[9,-111],[13,-37],[21,-6],[21,-79],[13,-35],[2,-44]],[[27075,60636],[-108,-317],[-37,-39],[-22,-41],[-19,-49],[-25,-45],[0,-86],[-16,-39],[-12,-2],[-12,-33],[3,-59],[-6,-45],[-25,-36],[-29,-4],[-16,-80],[-2,-50],[-16,-4],[-33,-35],[-39,-52],[-21,2],[-33,-49],[-8,-27]],[[26599,59546],[-4,-21],[-83,8],[-79,2],[-76,5],[-83,11],[-51,16],[-74,9],[-55,-14],[-96,8],[-56,10],[-83,22],[-49,4],[-16,-22],[-7,22],[-146,-12],[-92,-2],[-114,-8],[1,35],[-61,17],[10,-147],[-6,-64],[-137,6],[-126,0],[-84,6],[-33,-10]],[[24980,59425],[-15,1]],[[28739,61120],[-14,0]],[[28725,61120],[-1,72],[8,28],[11,-40],[-4,-60]],[[28711,61320],[2,79],[11,13],[7,-30],[5,-118],[-15,21],[4,27],[-14,8]],[[28627,62072],[5,20],[5,-46],[-10,26]],[[29002,61700],[-1,-43],[-9,-105],[-5,-15],[-20,-211],[-18,-120]],[[28949,61206],[-107,-38],[-6,-55],[-6,4]],[[28830,61117],[-13,30],[-19,-4],[-22,-64],[-9,-1],[-1,67],[7,70],[17,28],[-17,2],[2,45],[11,40],[-21,9],[-12,-33],[-4,28],[5,58],[26,27],[-6,39],[-9,-13],[-4,36],[15,112],[-15,-24],[-7,-47],[0,-60],[-8,-1],[-11,77],[11,9],[-1,53],[-8,10],[-4,-49],[-9,-33],[6,-27],[-9,-62],[-4,29],[-17,-23],[-9,12],[-16,83],[-9,13],[2,43],[-24,151],[20,14],[5,39],[-8,-7],[-9,51],[8,40],[12,22],[6,-25],[12,26],[1,-36],[22,-9],[13,-39],[4,10],[-16,69],[-17,14],[-8,42],[-7,-3],[-16,77],[-5,-28],[-12,20],[-1,-59],[-7,8],[3,101],[18,92],[14,-45],[8,10],[-3,124],[-8,17],[-17,-47],[-12,4],[1,-48],[-12,-17],[4,104],[16,101],[7,-51],[21,-11],[11,31],[-6,54],[11,55],[-16,-9],[-3,-52],[4,-31],[-12,17],[3,63],[-6,59],[-7,14],[16,136],[12,42],[-2,24],[14,25],[7,37],[30,-6],[-11,24],[18,70],[-10,6],[12,122],[-35,-24],[-6,-48],[12,-24],[4,-32],[-12,-14],[-34,-97],[-7,31],[7,54],[-8,28],[0,-44],[-9,-51],[8,-34],[-7,-46],[-6,96],[-15,14],[6,-103],[-8,8],[-6,-42],[12,-22],[-21,-44],[-7,-28],[-13,8],[-2,26],[-22,46],[3,-46],[9,-13],[2,-41],[27,-54],[2,-67],[-5,-25],[13,-48],[-23,-45],[7,-37],[-3,-40],[-9,4],[-7,-55],[6,-53],[-17,-58],[9,-50],[-2,-54],[6,-73],[-2,-89],[7,-65],[22,-80],[8,-62],[-9,-45],[-14,7],[9,-42],[9,22],[8,-14],[-7,-47],[4,-48],[18,-92],[-5,-22],[5,-97],[-9,19],[-11,58],[-8,4],[-5,64],[-8,-9],[-2,-57],[-7,38],[-8,-3],[-5,48],[-12,45],[-23,23],[-35,3],[-8,129],[-8,13],[-4,-31],[10,-68],[-2,-22],[-22,43],[-2,34],[-13,31],[-12,114],[-20,-44],[-33,-56],[-12,27],[-7,115],[11,82],[15,57],[20,30],[-5,10]],[[28423,61914],[-1,45],[7,27],[18,25],[1,85]],[[28426,62262],[-9,37],[-20,6],[-8,17],[1,48],[-26,42],[-34,15],[-6,39],[-10,13],[1,58],[9,18],[7,49],[-23,43],[-6,45],[-15,-1],[-16,25],[-13,-3]],[[26327,70327],[8,17],[9,-60],[65,-63],[-17,-58],[-23,18],[-14,61],[-28,85]],[[26120,70229],[5,40],[7,-20],[-12,-20]],[[26084,70289],[21,-39],[-4,-36],[-19,38],[2,37]],[[26055,70019],[7,48],[3,69],[6,15],[1,57],[18,-7],[-1,-85],[4,-83],[-19,-44],[-19,30]],[[26035,70180],[13,8],[-5,-54],[-7,1],[-1,45]],[[25984,69839],[13,-17],[1,-59],[-14,76]],[[25933,69486],[6,23],[19,-25],[-6,-96],[-17,52],[-2,46]],[[25908,69335],[11,44],[6,-47],[-17,3]],[[25759,70041],[20,-64],[-8,-22],[-4,36],[-14,43],[6,7]],[[25722,65552],[30,68],[27,87],[37,261],[34,146],[19,134],[10,99],[10,176],[-1,58],[6,87],[-1,163],[-6,182],[-11,120],[-35,230],[-11,94],[-12,138],[-17,119],[0,48],[22,102],[7,50],[1,77],[-9,134],[-14,90],[4,32],[20,51],[24,151],[21,112],[4,124],[9,134],[-11,112],[1,33],[27,43],[20,15],[6,93],[-2,74],[10,36],[13,-18],[16,80],[18,-35],[17,13],[8,32],[9,86],[10,16],[15,94],[11,47],[9,-7],[12,35],[3,-39],[-19,-32],[-3,-42],[12,-81],[-23,-81],[13,19],[0,-74],[-6,-7],[-8,-84],[7,-95],[8,-2],[19,145],[3,98],[13,20],[-8,-158],[-15,-44],[-6,-67],[14,-14],[8,65],[7,13],[22,135],[5,78],[-1,56],[4,82],[-6,85],[5,94],[17,49],[10,3],[18,48],[18,16],[25,-7],[26,14],[12,20],[-1,33],[-33,17],[-14,47],[-8,114],[16,75],[13,14],[20,69],[-2,31],[-18,27],[60,-15],[7,49],[61,-102],[26,-58],[13,20],[11,-16],[13,10],[32,-34],[24,-85],[3,-57],[17,-27],[33,4],[8,-9],[29,-77],[23,-7],[34,-71],[29,6],[21,-78],[11,-25],[-9,-27],[15,-96],[11,-30],[14,-133],[-20,21],[-16,34],[-15,-41],[4,-110],[12,-44],[21,-26],[5,-59],[1,-98],[7,-37],[-4,-71],[-8,-67],[2,-65],[-5,-111],[-1,-121],[-12,-15],[-14,-64],[-24,2],[-8,-30],[-8,-108],[-4,-115],[-19,-5],[-8,-19],[0,-49],[-42,-6],[-13,-35],[-9,-76],[-5,-124],[-8,-42],[13,-91],[16,-32],[9,33],[5,-38],[20,-21],[13,-38],[10,16],[28,120],[14,0],[7,25],[-7,51],[9,44],[9,87],[4,-50],[19,31],[5,29],[-21,31],[24,-3],[11,24],[5,41],[23,10],[34,28],[9,45],[31,30],[11,-30],[23,-25],[15,-39],[17,-123],[10,-37],[7,-97],[5,-208],[17,-178],[4,-247],[10,-143],[18,-110],[2,-48],[-11,-46],[-7,-145],[4,-47],[-12,-113],[-4,-67],[-18,-66],[-15,4],[-12,-42],[1,66],[-9,27],[10,16],[15,62],[-14,26],[-15,-5],[-19,-34],[-7,-32],[9,-27],[2,-45],[-15,4],[-11,-52],[1,-84],[-7,-77],[-17,-53],[-30,-25],[-18,-97],[2,-127],[-3,-44],[-15,-25],[-1,-70],[-16,-39],[-6,-39],[-13,-4],[-7,-65],[-11,-11],[-17,-72],[5,-79],[-8,-9]],[[26661,65520],[-86,-11],[-167,-20],[-124,-12]],[[25045,72671],[21,69],[95,141],[7,-2],[43,98],[31,42],[6,-11],[28,29],[-1,-28],[-12,-16],[-30,-80],[0,-30],[-25,-55],[-51,-53],[-42,-66],[2,-20],[23,0],[-59,-73],[-18,3],[-18,52]],[[24721,71146],[19,40],[12,3],[39,49],[38,34],[31,77],[19,64],[16,27],[33,17],[15,-17],[29,27],[22,-3],[52,75],[30,94],[22,5],[29,23],[10,39],[5,51],[25,65],[14,16],[30,82],[23,23],[21,56],[11,54],[20,52],[58,85],[56,25],[50,-7],[23,-39],[1,-45],[-11,5],[-17,-24],[-19,13],[-21,-10],[5,-47],[-32,-57],[-30,-95],[-19,-20],[-5,-74],[-7,9],[-7,-52],[-14,-26],[-5,-67],[-12,-48],[-9,-6],[-12,-164],[6,-52],[-9,-35],[11,3],[20,87],[3,44],[7,-13],[29,80],[28,43],[-12,-55],[-12,-24],[-15,-77],[30,84],[26,18],[14,-13],[37,1],[15,-30],[8,8],[17,-35],[8,-39],[15,11],[22,-63],[3,-61],[17,-49],[6,-47],[14,-49],[19,-15],[-2,-79],[11,-28],[26,-15],[42,10],[25,43],[11,-8],[14,-72],[12,-27],[19,-4],[8,46],[16,-7],[9,-38],[4,69],[-7,53],[16,20],[7,-32],[-6,-56],[17,-26],[26,72],[40,62],[59,110],[11,-21],[61,42],[46,-16],[35,-3],[30,7],[62,85],[24,12],[30,-7],[32,14],[-22,-84],[-2,-135],[2,-45],[-8,-19],[9,-51],[16,-8],[9,16],[6,-26],[15,3],[17,-28],[34,49],[13,-4],[11,-46],[-1,-35],[25,28],[7,-7],[10,65],[19,24],[23,-19],[11,9],[8,39],[27,-5],[5,-30],[-10,-100],[7,-120],[1,-95],[-29,-2],[-8,-65],[15,-12],[5,21],[15,-4],[8,-38],[19,-19],[-13,-40],[24,-75],[12,-1],[20,-45],[8,38],[14,-35],[8,26],[-16,100],[14,-15],[41,13],[12,-15],[14,-91],[16,-31],[-10,-64],[-14,-20],[-27,39],[-31,-16],[-31,42],[-11,-12],[-27,1],[-27,23],[-60,-26],[-13,-28],[-37,69],[-15,48],[-13,-7],[-13,30],[-10,-41],[2,-51],[-15,-32],[1,-45],[9,-63],[-12,-20],[-24,42],[-3,31],[-21,37],[-5,30],[-22,64],[-38,46],[-13,-7],[-33,49],[-18,-5],[-13,23],[-5,-20],[-19,11],[-25,-75],[-17,-73],[-9,-8],[-32,23],[-23,-15],[-8,-22],[2,-34],[-23,37],[-21,17],[-24,-14],[-10,11],[-23,-24],[-13,-42],[-2,-63],[-9,-72],[-14,4],[-7,-39],[-22,-15],[-6,-46],[-13,-3],[0,-45],[-12,-9],[4,-61],[-20,32],[-9,40],[12,27],[1,34],[11,31],[1,40],[13,-4],[15,87],[-3,43],[-12,10],[-18,-75],[-38,24],[3,-51],[-13,-47],[-5,-56],[-30,-31],[-5,-27],[-6,39],[3,55],[-4,67],[-8,32],[-10,-19],[-5,-120],[3,-12],[-31,-55],[-26,-129],[-17,-146],[-19,-65],[-20,-112],[-41,-174],[6,-33]],[[25508,69434],[-19,14],[-8,47],[-13,28],[-2,29],[15,118],[11,48],[-2,34],[-11,25],[-16,-47],[-27,-10]],[[25436,69720],[0,0]],[[25436,69720],[-7,47],[7,24],[-3,38],[13,34],[6,40],[-3,47],[8,69],[-14,75],[12,13],[-8,44],[-13,18],[-6,33],[-24,4],[-9,43],[-21,-18],[-18,45],[17,52],[-7,21],[0,47],[-40,48],[-14,-14],[-24,47],[-12,-15],[-21,21],[-2,25],[-27,-2],[-3,-32],[-14,0],[-4,29],[-17,16],[-12,-13],[-86,142],[-287,231],[1,27],[-28,167],[-27,16],[-10,24]],[[24739,71113],[0,0]],[[24739,71113],[-12,-6],[-6,39]],[[25252,52115],[30,-13],[-14,-20],[-12,9],[-4,24]],[[25179,52148],[16,13],[30,-29],[6,-15],[-24,5],[-28,26]],[[25119,52114],[15,7],[15,31],[-4,-29],[-21,-27],[-5,18]],[[25071,52140],[21,-1],[8,13],[-5,-43],[-10,-7],[-17,26],[3,12]],[[24751,57677],[229,-2],[156,1],[202,1]],[[25284,52292],[-4,-32],[-7,15],[-10,-40],[-18,27],[-13,-15],[-5,30],[-18,7],[-18,-23],[-23,49],[-2,-28],[-21,38],[-22,-3],[-31,-27],[-28,-42],[-30,-33],[4,54],[-10,30],[-14,-27],[9,-17],[1,-40],[-25,-57],[-8,-80],[-9,10],[-13,-16]],[[24969,52072],[-12,0],[-13,50],[-4,66],[0,69],[-15,77],[0,54],[-10,43],[-11,19],[-4,39],[-10,34],[-4,95],[-8,14],[10,88],[-4,36],[7,25],[9,125],[7,19],[-1,45],[7,22],[-7,36],[-165,-2],[-134,-1],[-226,0],[13,21],[7,58],[-16,50],[-1,28],[10,46],[-3,41],[-12,30],[1,38],[18,-7],[17,20],[2,43],[-12,37],[-1,45],[-7,16],[9,39],[4,-50],[8,-29],[9,7],[0,29],[-13,75],[-1,74],[20,18],[12,35],[-4,32],[-18,-13],[-9,30],[5,38],[10,-28],[18,0],[1,106],[11,35],[23,5],[4,18],[-29,11],[7,94],[12,11],[2,-43],[7,-17],[5,28],[-6,21],[24,78],[-2,50],[7,25],[19,14],[0,50],[-19,26],[5,25],[13,-39],[5,36],[12,22],[9,69],[-15,24],[2,-62],[-32,11],[2,79],[27,20],[7,27],[14,-31],[5,42],[-2,53],[12,-8],[16,79],[-4,18],[-6,-40],[-23,17],[0,58],[8,20],[-8,35],[-10,-20],[-23,49],[3,58],[5,1],[17,-48],[10,7],[-5,26],[-21,50],[14,31],[8,37],[-7,37],[-6,-46],[-20,-26],[-6,14],[-3,52],[9,38],[16,33],[2,27],[-31,30],[1,71],[5,37],[21,52],[2,39],[-9,72],[-11,-4],[1,-67],[-14,-26],[-9,22],[3,48],[10,50]],[[17580,73980],[234,-2],[232,3],[190,-5],[190,2],[348,-2],[211,0],[232,2],[328,0],[141,0],[292,0],[227,1],[143,-1],[294,0],[114,0],[167,1]],[[18973,68711],[-21,23],[-6,49],[-24,59],[2,43],[-28,117],[-17,36],[-8,-52],[-20,-3],[5,-33],[-16,-98],[15,-64],[-32,26],[-9,-16],[-24,13],[-3,-19],[-30,-40],[-13,63],[-22,-8],[-13,-25],[-10,13],[-21,-30],[-8,22],[-14,-8],[-12,35],[-16,7],[-9,-35],[-9,0],[-2,-61],[-8,-45],[-24,38],[-8,-20],[-11,24],[-33,2],[-17,22],[-17,-23],[-15,-72],[6,-53],[-9,-22],[-11,44],[-18,24],[-15,40],[-6,53],[5,34],[-21,81],[9,48],[-5,50],[-18,109],[-32,58],[-26,-44],[-4,40],[-28,53],[-11,97],[14,13],[2,55],[-4,61],[-19,39],[2,26],[-17,15],[-7,73],[-24,73],[-1,35],[-14,54],[2,70],[-8,19],[-6,57],[6,30],[-1,46],[-19,0],[7,95],[-15,25],[-12,-2],[2,26],[-10,44],[-15,26],[-9,-13],[2,-41],[-15,-35],[-19,-82],[-19,-26],[-13,12],[-3,-58],[-24,-42],[-5,36],[-14,22],[-13,60],[-25,0],[4,53],[-8,36],[18,37],[1,48],[-19,74],[18,89],[25,1],[7,44],[-13,54],[6,49],[-19,27],[-4,54],[9,58],[-17,33],[2,49],[19,7],[-1,79],[-6,30],[13,24],[1,116],[13,65],[-7,69],[12,3],[10,123],[-3,58],[-8,10],[-26,-23],[-4,-21],[-22,15],[-19,-5],[-8,39],[6,39],[-7,30],[-14,9],[-4,-30],[-15,-21],[-6,17],[6,55],[-34,58],[-16,59],[4,71],[-20,60],[-13,-1],[-6,60],[-19,82],[-17,54],[-12,13],[-5,37],[-11,7],[2,38],[-9,41],[-24,10],[-32,45],[-7,54],[-47,87],[-11,-2],[12,36],[22,8],[3,22],[-16,7],[-5,57],[-11,5],[2,34],[13,32],[-12,55],[2,63],[-13,24],[-7,47],[-8,-6],[-8,88],[-15,21],[-39,152],[-1,279],[1,109],[0,804]],[[29991,69339],[3,68],[18,64],[-3,15],[11,74],[-13,36],[18,11],[4,26],[22,42],[15,-61],[28,-5],[13,70]],[[30213,67064],[-30,-174],[-2,-44]],[[29724,66677],[-9,53],[-6,-10],[-12,102],[9,73],[1,57],[13,10],[8,45],[-6,52],[9,33],[-5,65],[4,138],[12,64],[-6,60],[10,135],[-5,35],[5,74],[15,33],[6,109],[9,44],[19,43],[6,44],[-1,64],[19,67],[0,85],[12,44],[12,69],[0,47],[-10,68],[6,57],[-6,34],[10,61],[35,32],[10,-13],[16,21],[5,52],[24,14],[14,39],[7,45],[16,22],[-6,59],[11,13],[2,73],[-10,38],[-11,102],[15,52],[4,64],[18,75],[-13,89],[11,30]],[[29842,64964],[3,18],[27,22],[-21,-43],[-9,3]],[[29812,64781],[5,20],[11,-16],[0,-50],[-6,35],[-10,11]],[[29792,64880],[16,16],[-12,-43],[-4,27]],[[29275,64272],[5,-67],[-9,-39],[-38,-78],[-11,-1]],[[28633,68013],[16,50],[-5,-45],[-11,-5]],[[28611,68032],[12,40],[9,-5],[-21,-35]],[[27689,66145],[33,53],[53,111],[29,91],[30,50],[26,25],[11,70],[13,36],[4,52],[36,56],[19,54],[-4,80],[-13,40],[2,43],[-7,37],[-23,35],[1,92],[-18,15],[9,77],[-5,125],[-3,13],[28,22],[38,42],[56,47],[40,20],[33,2],[38,-9],[36,8],[86,-42],[23,-19],[15,-47],[23,-46],[12,-10],[40,49],[35,1],[43,12],[21,-19],[23,-1],[14,25],[30,20],[18,40],[9,1],[19,80],[30,64],[10,8],[19,54],[14,5],[20,-15],[17,19],[9,53],[2,87],[-9,180],[-20,40],[11,50],[14,11],[2,-33],[21,41],[-3,43],[-19,47],[-22,-15],[5,29],[-15,51],[-15,1],[1,80],[16,115],[19,5],[23,42],[0,48],[18,22],[27,55],[9,-5],[16,29],[25,75],[4,46],[12,51],[41,121],[31,100],[56,141],[-1,12],[68,109],[26,62],[18,0],[22,37],[27,-29],[19,10],[91,-8],[53,-1],[107,14],[24,-1],[93,10]],[[29478,69336],[1,-109],[-7,-30],[-4,-63],[13,-57],[-10,-55],[-2,-92],[-4,-31],[4,-70],[19,-88],[4,-69],[-12,-80],[7,-114],[-4,-36],[-19,-63],[-5,-89],[-8,-79],[9,-27],[-1,-103],[10,-67],[-5,-64],[12,-57],[-16,-97],[-7,-114],[10,-26],[7,66],[19,0],[2,-53],[13,-29],[-2,-343],[-6,-422],[0,-74],[-4,-37],[8,-65]],[[29390,64649],[-7,-42],[-21,-42],[-2,-79],[-13,2],[9,-60],[18,87],[11,-15],[10,42],[21,27],[18,2],[4,33],[22,-30],[2,41],[9,-28],[20,-8],[18,-23],[23,28],[2,43],[17,8],[10,-12],[75,3],[39,20],[14,18],[30,63],[9,40],[12,13],[13,50],[21,21],[12,-2],[-18,-52],[-9,-6],[15,-31],[5,-44],[17,-12],[11,25],[16,-72],[15,17],[23,48],[2,28],[17,11],[12,-12],[-5,-21],[-67,-94],[-78,-123],[-50,-62],[-81,-93],[-53,-78],[-43,-41],[-27,-12],[-6,15],[-64,-60],[-61,10],[-46,-55],[3,38],[-23,-2],[3,25],[-11,39],[7,35],[-8,36]],[[26861,65369],[16,17],[-2,-36],[-14,19]],[[26831,65420],[6,90],[11,-35],[-3,-35],[-14,-42],[0,22]],[[26661,65520],[1,-26],[21,-28],[13,17],[37,-82],[25,-20],[11,-21],[18,-66],[19,-29],[18,23],[2,50],[7,13],[14,-55],[19,2],[-1,-37],[29,-96],[16,-33],[22,-21],[41,57],[22,4],[17,35],[51,60],[37,-38],[19,15],[20,-9],[29,61],[29,86],[8,10],[16,58],[44,102],[10,1],[67,108],[30,17],[24,34],[65,69],[17,24]],[[15490,70789],[21,10],[13,-45],[-1,-55],[17,-42],[25,-1],[32,51],[14,-4],[59,-119],[7,-61],[18,-81],[1,-57],[7,-52],[-3,-67],[10,-88],[-4,-41],[3,-50],[33,-54],[46,-35],[11,-19],[17,14],[13,-32],[18,-5],[23,39],[23,7],[54,83],[12,45],[15,16],[21,-15],[29,12],[28,26],[19,-30],[17,-9],[18,14],[34,-40],[9,-75],[14,4],[19,50],[34,4],[13,-16],[11,34],[47,51],[15,35],[21,-9],[21,-51],[22,6],[34,26],[20,5],[11,42],[28,27],[29,46],[46,27],[37,10],[13,57],[14,23],[24,-22],[64,39],[17,-14],[19,6],[28,42],[11,36],[163,1],[250,-3],[164,-3]],[[17338,70482],[16,-107],[11,-27],[16,-75],[18,5],[10,-47],[19,-2],[16,-48],[0,-53],[13,-49],[7,-54],[-24,-108],[-1,-55],[-10,-23],[-24,-150],[1,-36],[-9,-51],[-7,-116],[-13,-40],[-6,-53],[-14,-44],[-3,-51],[7,-58],[-9,-68],[-18,-76],[0,-21],[-32,-49],[-14,-93],[-8,-97],[-10,-66],[-18,-48],[3,-66],[-8,-36],[15,-69],[-8,-56],[13,-35],[19,25],[16,-59],[20,12],[2,-53],[18,-17],[1,-31],[-10,-62],[-12,-17],[1,-42],[11,-32],[-1,-43],[-12,-81],[0,-54],[-11,-11],[-2,-59],[0,-2105]],[[17307,65831],[-166,-2],[-273,-5],[-26,-3],[-143,2],[-220,2]],[[15305,65829],[-21,55],[-15,55],[-7,69],[2,33],[-14,81],[2,32],[-7,63],[-2,126],[13,147],[-7,107],[-17,87],[-11,2],[-11,123],[15,90],[12,96],[6,83],[0,52],[9,80],[6,99],[-6,42],[17,53],[8,43],[16,132],[15,193],[15,283],[7,225],[4,188],[-2,8],[10,197],[-1,69],[7,184],[0,92],[-5,39],[8,91],[12,227],[8,117],[4,157],[-3,66],[3,109],[-4,69],[5,24],[5,176],[-1,76],[-11,41],[6,87],[-2,83],[-7,45],[16,36],[1,109],[-19,193],[27,-79],[14,-14],[-3,38],[23,10],[7,17],[11,-28],[12,31],[15,0],[20,51]],[[26599,59546],[33,-4],[78,1],[5,-5],[157,-1],[61,3],[151,-2],[3,26],[77,-5],[-8,-28]],[[18139,65823],[61,-6],[262,13],[192,2],[5,-5],[196,3],[118,3]],[[18136,60011],[0,880],[1,11],[-1,272],[1,173],[-1,298],[1,318],[0,1661],[1,149],[0,628],[1,266],[0,1156]],[[28738,60998],[7,-15],[-7,-27],[0,42]],[[28949,61206],[-16,-79],[-15,-98],[-8,-27],[-5,21],[-15,-7],[-19,-78],[-12,-70],[-11,-126],[1,-64],[-23,-128],[7,-14],[-18,-91],[-12,-79],[-9,-11],[1,-57],[-11,-86],[-18,-64],[-16,2],[4,-35],[-9,-5],[-1,84],[-10,55],[-4,61],[2,70],[9,59],[3,89],[10,105],[4,76],[16,79],[18,110],[-4,34],[21,17],[15,65],[1,28],[-20,21],[25,54]],[[28739,61120],[-11,-44],[-3,44]],[[28423,61914],[-20,-21],[-13,21],[0,-52],[-18,-113],[-4,-52],[2,-76],[15,-59],[29,15],[6,26],[15,0],[12,38],[9,-30],[-6,-74],[19,-64],[0,-49],[35,-59],[25,-2],[7,-28],[6,21],[24,-9],[4,-44],[16,-39],[8,-59],[13,-15],[15,-54],[20,-23],[30,-68],[-5,-65],[-15,-24],[-2,-107],[4,-29],[-11,-46],[13,-23],[-19,-30],[-31,64],[-11,-27],[-7,25],[0,41],[-13,81],[-19,32],[-8,35],[-12,12],[-21,103],[-11,11],[-4,45],[-13,30],[-2,-30],[28,-89],[20,-114],[15,-24],[1,-25],[16,-14],[10,-96],[11,-53],[30,-4],[7,-36],[31,-24],[-14,-29],[-1,-37],[13,-8],[4,22],[8,-41],[5,-110],[-8,-90],[-18,63],[-21,45],[-9,-42],[11,-39],[7,-55],[9,-14],[-22,-24],[-18,-4],[6,-39],[22,11],[-2,-76],[14,12],[4,-31],[12,-20],[6,-49],[-9,-97],[-4,15],[-16,-18],[-15,-34],[-10,68],[-29,61],[-14,45],[4,48],[-13,71],[-22,-9],[-8,-25],[-10,46],[-18,11],[-4,23],[-21,-35],[11,-33],[12,8],[18,-13],[15,-55],[9,-2],[8,58],[8,-119],[1,-67],[22,-19],[25,-82],[1,-65],[27,4],[9,27],[1,-33],[9,-11],[-3,86],[17,6],[13,-29],[35,-36],[17,27],[11,-11],[8,-128],[12,-140],[9,-71],[7,-94]],[[15775,72069],[4,105],[10,72],[12,-52],[-2,-58],[19,-32],[-18,-27],[-5,-26],[-18,-12],[-2,30]],[[15740,73454],[0,40],[20,-41],[-17,-26],[-3,27]],[[15720,73667],[6,14],[26,-115],[-18,42],[-14,59]],[[15715,73495],[8,29],[12,-47],[-15,-33],[-5,51]],[[15707,73080],[13,89],[14,59],[2,55],[16,11],[7,-19],[0,-49],[19,-38],[3,-26],[-15,-19],[-17,15],[-1,-29],[-13,-34],[-14,-7],[3,-24],[23,10],[11,-42],[8,-84],[-4,-14],[10,-100],[8,27],[-5,67],[10,-3],[16,-57],[16,-13],[7,-94],[-8,-58],[-14,13],[-12,86],[-21,-25],[2,30],[-19,44],[3,93],[-5,56],[-18,-4],[-3,30],[-22,54]],[[15649,73739],[11,-4],[34,-53],[-8,-8],[-22,20],[-15,45]],[[15623,73630],[17,26],[2,-33],[-19,7]],[[15618,73982],[15,0],[4,-29],[-17,-2],[-2,31]],[[15586,73509],[7,30],[20,1],[2,-29],[15,-33],[17,-9],[-18,69],[37,108],[12,-2],[37,-58],[-19,-50],[11,-66],[-2,-62],[-11,-25],[4,-69],[-20,-12],[-15,50],[-9,-12],[-22,11],[-31,62],[-6,76],[-9,20]],[[15577,73617],[18,-10],[18,-54],[-30,41],[-6,23]],[[17306,73978],[-1,-290],[0,-441],[-2,-286],[0,-702],[1,-52],[0,-1247],[-7,-48],[20,-76],[8,-54],[0,-63],[12,-40],[-17,-93],[7,-11],[11,-93]],[[15490,70789],[-34,9],[-3,34],[-12,6],[-10,-31],[-13,10],[-19,-51],[-9,7],[-21,74],[-10,8],[-7,-47],[-10,-10],[6,139],[1,124],[-4,165],[13,-61],[-2,-100],[4,-137],[17,0],[1,37],[-10,40],[0,61],[12,-37],[14,83],[-19,116],[11,42],[25,53],[-17,37],[-6,-27],[-18,-2],[4,-23],[-16,4],[-20,44],[-1,55],[-11,130],[8,8],[6,-52],[11,31],[52,63],[-10,27],[-34,23],[-2,45],[-27,14],[-8,-24],[8,-90],[-16,-21],[3,38],[-4,206],[-14,176],[-24,80],[-9,207],[-5,77],[-15,162],[-14,36],[-4,55],[-14,23],[-5,31],[-19,29],[-12,110],[-8,101],[1,53],[-11,56],[12,58],[9,138],[-19,46],[3,22],[17,1],[40,-55],[32,-64],[10,-1],[25,-39],[6,12],[41,-55],[-1,-20],[35,-41],[56,-11],[21,13],[31,-37],[9,19],[10,-18],[24,7],[12,-32],[41,2],[32,71],[-3,-24],[22,-42],[7,-45],[17,17],[17,-5],[-3,-32],[14,-20],[1,-59],[10,6],[3,53],[-14,34],[0,40],[12,28],[21,10],[3,-31],[-15,-34],[10,-50],[7,6],[2,48],[10,14],[8,-100],[-15,-10],[11,-42],[8,-78],[13,-21],[-8,-25],[-16,2],[2,-42],[-17,-36],[-9,-113],[-14,-9],[11,113],[-6,17],[-19,-101],[-4,-53],[-21,-70],[-38,-183],[-13,-119],[12,8],[23,-13],[12,27],[21,17],[3,38],[-44,-62],[-17,27],[42,226],[31,74],[29,28],[3,73],[16,73],[31,68],[-9,97],[22,-40],[14,-187],[-22,0],[3,-40],[11,-14],[-4,-56],[7,-19],[0,-53],[-14,-38],[-1,-38],[15,-16],[-10,-48],[-6,-87],[4,-21],[-10,-57],[7,-49],[-15,-79],[-27,78],[8,96],[-16,-42],[-6,-63],[30,-83],[-10,-16],[1,-48],[-11,-26],[-16,46],[-20,105],[12,37],[1,61],[-7,25],[0,-51],[-12,-57],[7,-72],[-6,-48],[13,14],[11,-66],[27,-22],[11,37],[0,33],[13,16],[8,79],[10,53],[-2,30],[25,-58],[10,25],[-4,36],[27,34],[0,50],[-8,58],[-8,11],[6,35],[-10,40],[-7,71],[22,38],[-24,58],[16,82],[-6,91],[15,46],[9,119],[22,25],[-1,69],[-14,23],[-23,86],[1,74],[-10,49],[-15,4],[3,-24],[-11,-28],[10,-67],[18,-50],[1,-35],[-26,85],[-12,4],[-7,58],[2,77],[18,24],[16,-22],[10,42],[-10,45],[-28,45],[-13,48],[1,39],[-29,-33],[-7,33],[4,45],[-13,-17],[3,39],[24,28],[13,-15],[9,-49],[19,7],[-10,108],[17,6],[5,34],[-21,66],[-5,66],[8,44],[-13,30],[-17,-6],[-11,-41],[9,-44],[-19,40],[8,57],[-10,25],[-9,-14],[-1,65],[-21,54],[11,21],[-5,36],[-13,16],[16,54],[184,0],[97,-6],[145,5],[143,-2],[190,0],[169,0],[165,1],[178,0],[325,-2]],[[25685,69733],[6,80],[31,-17],[-23,-128],[-14,65]],[[25568,69555],[12,14],[2,-63],[-14,23],[0,26]],[[24707,71654],[8,83],[12,4],[-5,-75],[-15,-12]],[[24691,71507],[21,33],[-14,-47],[-7,14]],[[24655,71564],[5,21],[30,29],[5,-13],[-11,-48],[-24,-11],[-5,22]],[[24625,71464],[3,27],[20,41],[1,-19],[-24,-49]],[[24631,71609],[14,-54],[-13,-3],[-8,37],[7,20]],[[24621,71679],[35,36],[11,-55],[14,34],[2,-44],[-16,-9],[-17,-49],[-11,42],[-16,20],[-2,25]],[[24616,71401],[17,18],[32,86],[14,-32],[-30,-33],[5,-35],[-17,-4],[-20,-37],[-1,37]],[[24563,71635],[15,17],[-1,-44],[-14,27]],[[24276,71309],[20,-30],[34,11],[49,52],[19,27],[18,0],[48,70],[25,27],[6,26],[17,-22],[7,33],[25,10],[22,70],[14,-12],[14,35],[10,-4],[24,-81],[-14,-76],[-24,-78],[9,-73],[-16,-35],[-11,-77],[13,-16],[42,73],[5,54],[42,-108],[13,-22],[9,10],[25,-27]],[[25508,69434],[-10,-58],[-1,-80],[-37,-13],[-21,-40],[3,-47],[-11,-59],[-9,-15],[-11,-83],[-12,-42],[-7,-104],[3,-25],[-13,-50],[20,-42],[12,7],[7,53],[26,64],[10,3],[15,63],[0,36],[31,133],[26,36],[11,-7],[11,34],[15,-32],[-6,46],[12,97],[27,109],[7,100],[12,-5],[21,32],[1,59],[17,60],[22,-3],[-2,-87],[-16,-5],[-2,-142],[-12,-40],[-8,4],[-5,-53],[-14,-52],[5,-43],[-13,-39],[4,-26],[-18,-33],[-13,-62],[-8,-87],[-17,-107],[-9,-26],[-9,-62],[-21,-268],[9,-94],[-2,-71],[-12,-41],[-23,-46],[-3,-62],[-8,-37],[-14,-162],[2,-82],[8,-50],[-2,-102],[-21,-118],[-7,-136],[-20,-107],[-9,-156],[8,-76],[-5,-38],[8,-85],[-7,-57],[14,-62],[0,-85],[6,-63],[16,-59],[-4,-89],[-11,-107],[6,-145]],[[2682,386],[26,11],[1,-51],[-9,8],[-8,-28],[-10,60]],[[2636,452],[21,8],[-4,-21],[-13,-12],[-4,25]],[[2313,271],[13,35],[16,2],[19,68],[4,-20],[27,2],[4,-44],[-7,13],[-29,-30],[-23,-89],[-11,47],[-13,16]],[[90204,32599],[5,15],[34,16],[21,137],[6,65],[7,-11],[7,-43],[14,-11],[-10,-88],[-32,-109],[-9,-44],[-2,-111],[-12,-56],[-18,30],[-7,101],[8,50],[-12,59]],[[90597,35570],[10,43],[4,-16],[-8,-45],[-6,18]],[[90537,37432],[5,20],[1,-49],[-6,29]],[[90507,37973],[14,53],[0,59],[11,0],[0,-66],[-9,-13],[-10,-55],[-6,22]],[[90501,34555],[4,28],[2,101],[9,-12],[20,67],[5,-22],[-14,-80],[4,-63],[-13,7],[0,-73],[-12,18],[-5,29]],[[90487,38800],[6,39],[11,-28],[-1,-51],[-8,-23],[-7,25],[-1,38]],[[90486,35993],[20,2],[3,-36],[-19,-8],[-4,42]],[[90472,34420],[4,45],[13,54],[5,-54],[-4,-24],[7,-40],[-2,-61],[-9,-29],[-14,109]],[[90343,33382],[11,51],[21,35],[15,-9],[-2,-35],[-12,-13],[-8,-39],[-11,-6],[-5,23],[-9,-7]],[[28030,56345],[-13,-8],[-36,-51],[-27,-66],[-35,-121],[-18,-79],[-7,-45],[-16,-58],[-14,-93],[-13,-174],[3,-55],[-7,-48],[-14,-47],[-23,-41],[-9,-97],[-17,10],[-17,-16],[-11,39],[-16,-33],[-10,-63],[3,-31],[8,10],[0,-33],[-15,-20],[-18,-45],[-9,-52],[-34,-59],[-10,38],[-11,-7],[-1,-32],[17,-22],[-5,-59],[-25,-52],[-7,-40],[-10,5],[-24,-22],[-17,-59],[-6,18],[-36,-90],[-8,21],[-15,-28],[-2,30],[-13,-1],[-4,-42],[15,-83],[-7,-74],[-32,-62],[-19,-20],[-2,41],[-19,40],[-11,-22],[-2,-31],[12,-15],[14,-59],[-15,-65],[-25,-59],[-9,0],[-17,-67],[5,-21]],[[31708,38299],[11,-21],[12,13],[9,-36],[-17,-36],[-12,19],[-3,61]],[[31642,38028],[49,54],[35,-32],[-18,-26],[-21,-7],[-11,-24],[-8,12],[-17,-17],[-9,40]],[[31637,38350],[7,-14],[0,-44],[-7,58]],[[31170,38320],[15,18],[17,49],[-4,63],[7,32],[14,12],[20,-6],[17,-25],[50,2],[20,-22],[24,26],[18,-5],[25,-25],[8,20],[28,0],[27,-20],[16,1],[7,-19],[6,24],[37,-35],[4,19],[18,-18],[6,7],[31,-33],[14,-49],[7,13],[5,-27],[10,-6],[14,22],[-5,-40],[3,-84],[13,-30],[-7,-43],[-11,19],[3,-28],[-10,2],[-7,-32],[-13,1],[-11,-59],[-7,-83],[-8,-6],[-1,-44],[-25,-46],[-15,-9],[-12,12],[-7,-27],[-10,13],[-21,-43],[-10,9],[-9,-29],[-8,9],[-18,66],[-12,-10],[-7,-34],[-21,60],[-8,0],[-25,-33],[-14,24],[-12,-19],[-25,47],[-7,-39],[-14,-27],[-21,-3],[-12,-18],[-8,41],[-21,8],[-11,-32],[-19,19],[-12,-12],[1,53],[10,31],[-11,12],[3,65],[5,15],[1,75],[7,31],[-10,101],[-13,21],[-9,78]],[[30985,38023],[20,12],[5,-43],[-15,-38],[-12,32],[2,37]],[[31858,38286],[13,39],[11,-4],[15,-28],[-13,-4],[1,-49],[-9,20],[-13,-5],[-5,31]],[[31828,37522],[7,36],[-4,40],[7,30],[10,-13],[8,22],[17,3],[6,-29],[39,5],[2,-18],[-32,-48],[-41,-30],[-19,2]],[[31779,38288],[6,50],[10,-23],[12,26],[19,-21],[33,6],[-2,-23],[-14,-4],[9,-42],[-16,-11],[-15,23],[-7,-20],[-12,49],[-23,-10]],[[17306,73978],[171,1],[103,1]],[[18139,65823],[-119,3],[-36,-2],[-58,9],[-63,-7],[-79,0],[-293,0],[-184,5]],[[29478,69336],[71,5],[115,0],[40,-9],[62,-5],[57,2],[168,10]],[[25149,51924],[13,-63],[5,-76],[-3,-101],[-10,-105],[-4,3],[8,90],[4,100],[-1,46],[-12,106]],[[25020,51931],[11,38],[32,81],[2,-14],[-13,-63],[2,-28],[13,-33],[-15,-50],[-3,47],[-10,-10],[-17,10],[-2,22]],[[24941,51321],[17,10],[-9,-42],[-8,32]],[[24732,50824],[7,27],[13,-65],[-8,-14],[-12,52]],[[24681,50807],[39,-33],[-19,-8],[-20,41]],[[24628,50763],[25,19],[2,-17],[-26,-20],[-1,18]],[[24275,51386],[10,27],[12,-5],[3,26],[22,-6],[23,-61],[9,7],[6,-29],[-15,-36],[-1,-40],[-14,-19],[-59,115],[4,21]],[[24969,52072],[-17,-35],[-8,7],[-17,-56],[-2,-38],[-12,-15],[3,-40],[-28,17],[-11,-45],[5,-68],[18,-10],[13,25],[-4,-59],[8,-34],[15,-19],[13,11],[6,29],[5,101],[-2,13],[24,54],[3,44],[18,-34],[13,6],[0,-21],[-17,-34],[0,-34],[15,-17],[3,-61],[15,14],[11,82],[16,-25],[-4,-55],[-13,0],[-12,-44],[22,2],[-3,-29],[-31,-22],[13,-66],[11,22],[-7,-59],[-9,26],[-14,11],[-12,-55],[-1,-99],[-9,-5],[-6,86],[-11,1],[-2,-66],[13,-58],[-13,33],[-14,5],[-8,-17],[-15,6],[18,-39],[0,-32],[-23,49],[0,-25],[12,-32],[-12,-14],[1,-34],[11,-36],[20,-11],[-1,-22],[12,-24],[-1,-34],[7,-46],[9,30],[5,-21],[22,-2],[16,-34],[3,30],[20,-91],[11,44],[24,-111],[0,-59],[6,-13],[18,28],[7,-41],[-24,-15],[-2,-33],[16,-3],[-8,-52],[-11,24],[-12,-96],[2,-44],[-21,36],[-2,55],[-8,16],[-31,-138],[-15,-41],[7,67],[9,29],[5,48],[9,21],[0,54],[9,23],[-1,51],[-10,24],[-4,-62],[-16,-29],[-12,29],[-14,77],[-34,43],[-10,45],[-56,33],[-12,-14],[-48,-143],[-46,-115],[-7,10],[4,41],[-12,16],[-2,41],[6,3],[-12,100],[-11,27],[-9,-1],[1,-28],[-8,-36],[2,76],[-10,56],[-13,-64],[-14,14],[-7,-22],[-7,31],[-6,-27],[10,-63],[-12,4],[-7,-27],[-4,-54],[-8,7],[-5,-57],[-8,12],[-19,-33],[-6,-76],[-16,16],[5,21],[-13,67],[-18,57],[-11,-13],[-26,21],[-10,33],[-25,12],[-16,25],[-16,60],[12,14],[7,58],[8,-12],[12,-52],[7,22],[-1,-68],[15,-17],[-4,104],[-23,64],[0,42],[-11,7],[-9,-41],[-13,-13],[2,41],[-10,-7],[3,39],[9,28],[-10,40],[-29,-50],[-10,80],[-10,-8],[-6,118],[-26,1],[6,33],[1,85],[-32,16],[-13,-10],[-20,-38],[-7,57],[8,41],[8,-3],[-2,46],[-16,8],[-14,-22],[-9,20],[-2,-40],[-24,-36],[-14,-34],[-6,32],[-18,-16],[0,-32],[9,-30],[18,-8],[-9,-30],[8,-63],[20,21],[5,-18],[-13,-14],[6,-20],[-32,-6],[-26,-49],[-20,-9],[-82,63],[-35,42],[-57,101],[-23,32],[-41,40],[-33,5],[-14,-14],[-36,7],[-58,-25],[-17,-13],[-26,-53]],[[30097,65246],[-20,-46],[-6,102],[2,64],[-10,22],[2,-159],[-17,2],[-2,-35],[-10,-7],[-1,55],[16,95],[8,68],[17,46]],[[30029,65436],[5,7],[11,-52],[-5,-7],[-11,52]],[[30019,65203],[6,51],[-4,23],[5,57],[4,-20],[0,-85],[-11,-26]],[[29960,64853],[11,80],[5,-14],[1,-76],[-17,10]],[[30068,65494],[-3,-45],[-14,-41],[-6,42],[4,35],[-19,54],[-8,40],[5,-129],[-22,17],[12,-39],[1,-75],[-12,-11],[8,-53],[0,-72],[-10,-45],[-8,-73],[-12,6],[-27,-18],[-27,-34],[-38,-30],[-1,16]]]} diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx index 7383757647..85155f02c2 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx @@ -2,7 +2,7 @@ import { getTranslations } from "next-intl/server"; import { MIDTERMS_COLORS } from "../constants"; import { ChamberData } from "../helpers/fetch_dashboard_data"; -import { getBinaryProbability } from "../helpers/post_utils"; +import { getMultipleChoiceOptionProbability } from "../helpers/post_utils"; const CURRENT_SENATE = { dem: 47, rep: 53 }; const CURRENT_HOUSE = { dem: 215, rep: 220 }; @@ -16,32 +16,46 @@ type Props = { export default async function ChamberControlCard({ data }: Props) { const t = await getTranslations(); - const senateDemProb = getBinaryProbability(data.senateControl); - const houseDemProb = getBinaryProbability(data.houseControl); + const senateDemProb = getMultipleChoiceOptionProbability( + data.senateControl, + "Democrats" + ); + const senateRepProb = getMultipleChoiceOptionProbability( + data.senateControl, + "Republicans" + ); + const houseDemProb = getMultipleChoiceOptionProbability( + data.houseControl, + "Democrats" + ); + const houseRepProb = getMultipleChoiceOptionProbability( + data.houseControl, + "Republicans" + ); const senateDemNeeded = Math.floor(SENATE_TOTAL / 2) + 1 - CURRENT_SENATE.dem; const houseDemNeeded = Math.floor(HOUSE_TOTAL / 2) + 1 - CURRENT_HOUSE.dem; return ( -
-

+
+

{t("midtermsHubChamberControl")}

-
+
@@ -52,57 +66,72 @@ export default async function ChamberControlCard({ data }: Props) { type RowProps = { chamberLabel: string; demProb: number | null; + repProb: number | null; currentDem: number; currentRep: number; - demNeeded: number; demNeededLabel: string; }; function ChamberRow({ chamberLabel, demProb, + repProb, currentDem, currentRep, demNeededLabel, }: RowProps) { - const demPct = demProb == null ? null : Math.round(demProb * 1000) / 10; - const repPct = demPct == null ? null : Math.round((100 - demPct) * 10) / 10; + const total = demProb != null && repProb != null ? demProb + repProb : null; + const demShare = + demProb != null && total && total > 0 ? (demProb / total) * 100 : null; + const repShare = demShare != null ? 100 - demShare : null; + + const demPct = demProb != null ? Math.round(demProb * 1000) / 10 : null; + const repPct = repProb != null ? Math.round(repProb * 1000) / 10 : null; return (
-
- +
+ {chamberLabel} {demPct != null && repPct != null && ( - + {demPct}% - {" / "} + + {" / "} + {repPct}% )}
-
- {demPct != null && ( +
+ {demShare != null && repShare != null && ( <>
+
)}
-
+
R {currentRep} diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx index 8047b55b40..65e803642c 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx @@ -2,7 +2,11 @@ import { getTranslations } from "next-intl/server"; import cn from "@/utils/core/cn"; -export default async function ChamberTabs() { +type Props = { + className?: string; +}; + +export default async function ChamberTabs({ className }: Props) { const t = await getTranslations(); const tabs: { key: string; label: string; active: boolean }[] = [ { key: "senate", label: t("midtermsHubChamberSenate"), active: true }, @@ -11,23 +15,40 @@ export default async function ChamberTabs() { ]; return ( -
- {tabs.map((tab) => ( - - ))} +
+ {tabs.map((tab) => + tab.active ? ( + + ) : ( + + + + {t("midtermsHubComingSoon")} + + + ) + )}
); } diff --git a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx index 32024968b8..676c135c22 100644 --- a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx @@ -1,27 +1,26 @@ -import { faUsers } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { getTranslations } from "next-intl/server"; import { PostWithForecasts } from "@/types/post"; -import { QuestionWithNumericForecasts } from "@/types/question"; import { MIDTERMS_COLORS } from "../constants"; -import { getForecastersCount } from "../helpers/post_utils"; +import { getMultipleChoiceOptionProbability } from "../helpers/post_utils"; + +type OutcomeKey = "RR" | "RD" | "DR" | "DD"; + +const OUTCOME_OPTION_LABEL: Record = { + RR: "Rep Senate / Rep House", + RD: "Rep Senate / Dem House", + DR: "Dem Senate / Rep House", + DD: "Dem Senate / Dem House", +}; type Outcome = { - key: "RR" | "RD" | "DR" | "DD"; + key: OutcomeKey; pct: number | null; borderColor: string; bgColor: string; }; -const PLACEHOLDER_OUTCOMES: Record = { - RR: 6.2, - RD: 61.5, - DR: 0.1, - DD: 32.3, -}; - type Props = { post: PostWithForecasts | null; }; @@ -29,36 +28,31 @@ type Props = { export default async function CongressOutcomeCard({ post }: Props) { const t = await getTranslations(); + const buildOutcome = ( + key: OutcomeKey, + borderColor: string, + bgColor: string + ): Outcome => { + const prob = getMultipleChoiceOptionProbability( + post, + OUTCOME_OPTION_LABEL[key] + ); + return { + key, + pct: prob != null ? Math.round(prob * 1000) / 10 : null, + borderColor, + bgColor, + }; + }; + const outcomes: Outcome[] = [ - { - key: "RR", - pct: extractPct(post, "RR") ?? PLACEHOLDER_OUTCOMES.RR, - borderColor: MIDTERMS_COLORS.repPrimary, - bgColor: MIDTERMS_COLORS.repLight, - }, - { - key: "RD", - pct: extractPct(post, "RD") ?? PLACEHOLDER_OUTCOMES.RD, - borderColor: MIDTERMS_COLORS.repPrimary, - bgColor: MIDTERMS_COLORS.repLight, - }, - { - key: "DR", - pct: extractPct(post, "DR") ?? PLACEHOLDER_OUTCOMES.DR, - borderColor: MIDTERMS_COLORS.demPrimary, - bgColor: MIDTERMS_COLORS.demLight, - }, - { - key: "DD", - pct: extractPct(post, "DD") ?? PLACEHOLDER_OUTCOMES.DD, - borderColor: MIDTERMS_COLORS.demPrimary, - bgColor: MIDTERMS_COLORS.demLight, - }, + buildOutcome("RR", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repLight), + buildOutcome("RD", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repLight), + buildOutcome("DR", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demLight), + buildOutcome("DD", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demLight), ]; - const forecasters = getForecastersCount(post); - - const labels: Record = { + const labels: Record = { RR: t("midtermsHubOutcomeRepRep"), RD: t("midtermsHubOutcomeRepDem"), DR: t("midtermsHubOutcomeDemRep"), @@ -66,25 +60,20 @@ export default async function CongressOutcomeCard({ post }: Props) { }; return ( -
-
-

- {t("midtermsHubCongressForecast")} -

- {forecasters > 0 && ( - - - {forecasters} - - )} -
-
+
+

+ {t("midtermsHubCongressForecast")} +

+
{outcomes.map((o) => ( -
- +
+ {labels[o.key]} -
+
+ + {o.pct != null ? `${o.pct.toFixed(1)}%` : "—"} +
- - {o.pct?.toFixed(1)}% -
))}
-

+

{t("midtermsHubCongressSummary")}

); } - -function extractPct( - post: PostWithForecasts | null, - outcomeKey: Outcome["key"] -): number | null { - if (!post?.group_of_questions) return null; - const questions = post.group_of_questions.questions as - | QuestionWithNumericForecasts[] - | undefined; - if (!questions) return null; - const question = questions.find((q) => q.label === outcomeKey); - if (!question) return null; - const center = - question.aggregations[question.default_aggregation_method]?.latest - ?.centers?.[0]; - if (center == null) return null; - return Math.round(center * 1000) / 10; -} diff --git a/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx b/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx index 78f73f0f60..ac97e58b42 100644 --- a/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx @@ -23,19 +23,21 @@ export default async function ConsequenceRow({ row }: Props) { })(); return ( -
-

+

+

{question}

); @@ -45,26 +47,31 @@ function ConsequenceBar({ pct, color, bg, + mobileLabel, }: { pct: number; color: string; bg: string; + mobileLabel: string; }) { return ( -
-
+
+ + {mobileLabel} + +
+ + {pct}% +
- - {pct}% -
); } diff --git a/front_end/src/app/(main)/midterms-2026/components/consumer_tile_client.tsx b/front_end/src/app/(main)/midterms-2026/components/consumer_tile_client.tsx new file mode 100644 index 0000000000..cfcc78615f --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/consumer_tile_client.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { FC } from "react"; + +import ConsumerQuestionTile from "@/components/consumer_post_card/consumer_question_tile"; +import { QuestionWithForecasts } from "@/types/question"; + +type Props = { + question: QuestionWithForecasts; +}; + +const ConsumerTileClient: FC = ({ question }) => { + return ; +}; + +export default ConsumerTileClient; diff --git a/front_end/src/app/(main)/midterms-2026/components/distribution_curve.tsx b/front_end/src/app/(main)/midterms-2026/components/distribution_curve.tsx deleted file mode 100644 index 985a5de463..0000000000 --- a/front_end/src/app/(main)/midterms-2026/components/distribution_curve.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { FC } from "react"; - -import { MIDTERMS_COLORS } from "../constants"; - -type Props = { - value: number; - color?: string; -}; - -const DistributionCurve: FC = ({ - value, - color = MIDTERMS_COLORS.demPrimary, -}) => { - return ( -
- -
- - {value}% - -
-
- ); -}; - -export default DistributionCurve; diff --git a/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx b/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx index ed6309b872..58dfc8b5c9 100644 --- a/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx @@ -1,108 +1,223 @@ "use client"; -import { useTranslations } from "next-intl"; -import { FC, useMemo } from "react"; - -import ExperimentMap from "@/app/(main)/experiments/components/experiment_map"; -import { BaseMapArea, MapType } from "@/types/experiments"; +import { geoAlbersUsa } from "d3-geo"; +import { FC, MouseEvent, ReactNode, useMemo, useState } from "react"; +import { + ComposableMap, + Geographies, + Geography, + GeographyType, +} from "react-simple-maps"; -import { MIDTERMS_COLORS, STATE_NAMES } from "../constants"; +import { MIDTERMS_COLORS } from "../constants"; +import MapLegend from "./map_legend"; import StateTooltipContent from "./state_tooltip"; -import { - getCommentsCount, - getDemWinPct, - getForecastersCount, - SenateRaceWithPost, -} from "../helpers/post_utils"; +import { getDemWinPct, SenateRaceWithQuestion } from "../helpers/post_utils"; import { getStateColor } from "../helpers/state_color"; -type Props = { - races: SenateRaceWithPost[]; +const GEO_URL = "/us-states-10m.json"; + +const FIPS_TO_ABBR: Record = { + "01": "AL", + "02": "AK", + "04": "AZ", + "05": "AR", + "06": "CA", + "08": "CO", + "09": "CT", + "10": "DE", + "11": "DC", + "12": "FL", + "13": "GA", + "15": "HI", + "16": "ID", + "17": "IL", + "18": "IN", + "19": "IA", + "20": "KS", + "21": "KY", + "22": "LA", + "23": "ME", + "24": "MD", + "25": "MA", + "26": "MI", + "27": "MN", + "28": "MS", + "29": "MO", + "30": "MT", + "31": "NE", + "32": "NV", + "33": "NH", + "34": "NJ", + "35": "NM", + "36": "NY", + "37": "NC", + "38": "ND", + "39": "OH", + "40": "OK", + "41": "OR", + "42": "PA", + "44": "RI", + "45": "SC", + "46": "SD", + "47": "TN", + "48": "TX", + "49": "UT", + "50": "VT", + "51": "VA", + "53": "WA", + "54": "WV", + "55": "WI", + "56": "WY", }; -type StateMapArea = BaseMapArea & { - race: SenateRaceWithPost | null; +type Props = { + races: SenateRaceWithQuestion[]; + /** Tabs slot (rendered absolute top-left on md+) */ + tabsSlot?: ReactNode; }; -const GeographicMap: FC = ({ races }) => { +type HoverState = { + abbr: string; + x: number; + y: number; +} | null; + +// Tuned by hand via the dev slider widget. +const MAP_VIEWBOX_WIDTH = 760; +const MAP_VIEWBOX_HEIGHT = 540; +const MAP_SCALE = 970; +const MAP_TRANSLATE: [number, number] = [385, 290]; + +const GeographicMap: FC = ({ races, tabsSlot }) => { const racesByState = useMemo( () => new Map(races.map((r) => [r.state, r])), [races] ); - const mapAreas: StateMapArea[] = useMemo( - () => - Object.keys(STATE_NAMES).map((abbr) => ({ - abbreviation: abbr, - name: STATE_NAMES[abbr] ?? abbr, - x_adjust: 0, - y_adjust: 0, - race: racesByState.get(abbr) ?? null, - })), - [racesByState] + const [hovered, setHovered] = useState(null); + + // react-simple-maps' MapProvider hardcodes translate to viewbox center, so + // we provide a fully-configured projection function instead. + const projection = useMemo( + () => geoAlbersUsa().scale(MAP_SCALE).translate(MAP_TRANSLATE), + [] ); - const getMapAreaColor = (area: StateMapArea) => { - if (!area.race) return MIDTERMS_COLORS.notContested; - return getStateColor(getDemWinPct(area.race.post)); + const handleEnter = (abbr: string, e: MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const parent = e.currentTarget.closest(".geo-map-container"); + if (!parent) return; + const parentRect = parent.getBoundingClientRect(); + setHovered({ + abbr, + x: rect.left - parentRect.left + rect.width / 2, + y: rect.top - parentRect.top, + }); }; - return ( -
- { - if (!mapArea?.race) return null; - return ( -
- -
- ); - }} - /> - -
- ); -}; + const handleClick = (race: SenateRaceWithQuestion | undefined) => { + if (!race?.parentPost || !race.question) return; + const url = `/questions/${race.parentPost.id}/?sub-question=${race.question.id}`; + window.open(url, "_blank", "noopener,noreferrer"); + }; + + const hoveredRace = hovered ? racesByState.get(hovered.abbr) : null; -const Legend: FC = () => { - const t = useTranslations(); return ( -
- - - {t("midtermsHubDemocrat")} - - - - {t("midtermsHubRepublican")} - - - - {t("midtermsHubNotContested")} - +
+ {tabsSlot && ( +
+ {tabsSlot} +
+ )} +
+ +
+
+ + + {({ geographies }: { geographies: GeographyType[] }) => + geographies.map((geo) => { + const abbr = FIPS_TO_ABBR[String(geo.id ?? "")]; + const race = abbr ? racesByState.get(abbr) : undefined; + const demWinPct = getDemWinPct(race?.question ?? null); + const fillColor = race + ? getStateColor(demWinPct) + : MIDTERMS_COLORS.notContested; + const isContested = race !== undefined; + const isHovered = hovered?.abbr === abbr; + + return ( + + isContested && abbr && handleEnter(abbr, e) + } + onMouseLeave={() => setHovered(null)} + onClick={() => isContested && handleClick(race)} + style={{ + default: { + fill: fillColor, + stroke: MIDTERMS_COLORS.stateStroke, + strokeWidth: 1.5, + outline: "none", + cursor: isContested ? "pointer" : "default", + opacity: isContested ? 1 : 0.5, + transition: + "fill 150ms ease-out, opacity 150ms ease-out, filter 150ms ease-out", + filter: + isContested && isHovered + ? "brightness(0.9)" + : undefined, + }, + hover: { + fill: fillColor, + stroke: MIDTERMS_COLORS.stateStroke, + strokeWidth: isContested ? 2 : 1.5, + outline: "none", + cursor: isContested ? "pointer" : "default", + opacity: isContested ? 1 : 0.75, + filter: isContested ? "brightness(0.9)" : undefined, + }, + pressed: { + fill: fillColor, + stroke: MIDTERMS_COLORS.stateStroke, + strokeWidth: isContested ? 2 : 1.5, + outline: "none", + opacity: isContested ? 1 : 0.75, + }, + }} + /> + ); + }) + } + + +
+ + {hoveredRace && hovered && ( +
+ +
+ )}
); }; diff --git a/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx b/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx index 5cdd0e5505..5152538ddb 100644 --- a/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx @@ -1,11 +1,10 @@ "use client"; -import Link from "next/link"; import { useTranslations } from "next-intl"; import { FC } from "react"; -import { CommentType, KeyFactor } from "@/types/comment"; -import cn from "@/utils/core/cn"; +import { ActivityCard } from "@/app/(main)/labor-hub/components/activity_card"; +import { CommentType } from "@/types/comment"; import { CommunityInsight } from "../helpers/fetch_community_insights"; @@ -15,65 +14,24 @@ type Props = { const InsightCard: FC = ({ insight }) => { const t = useTranslations(); - - const isKeyFactor = insight.type === "key-factor"; - const text = isKeyFactor - ? extractKeyFactorText(insight.keyFactor) - : extractCommentText(insight.comment); - const forecasters = insight.race.post?.forecasts_count ?? 0; - const sourceTitle = insight.race.post?.title ?? insight.race.name; - const link = insight.race.post ? `/questions/${insight.race.post.id}` : "#"; + const { comment, sourcePost } = insight; return ( - - + - {isKeyFactor ? t("midtermsHubKeyFactor") : t("midtermsHubTopComment")} - -

- {text} -

-
- {sourceTitle} - - {" · "} - {t("midtermsHubForecastersCount", { count: forecasters })} - -
- +

{extractCommentText(comment)}

+ +
); }; -function extractKeyFactorText(kf: KeyFactor): string { - if (kf.driver?.text) return kf.driver.text; - if (kf.news?.title) return kf.news.title; - if (kf.base_rate?.reference_class) { - const projected = kf.base_rate.projected_value; - const unit = kf.base_rate.unit; - if (projected != null && unit) { - return `${kf.base_rate.reference_class}: ${projected}${unit}`; - } - return kf.base_rate.reference_class; - } - return ""; -} - function extractCommentText(comment: CommentType): string { - return stripMarkdown(comment.text).slice(0, 280); + return stripMarkdown(comment.text).slice(0, 320); } function stripMarkdown(text: string): string { diff --git a/front_end/src/app/(main)/midterms-2026/components/insights_carousel.tsx b/front_end/src/app/(main)/midterms-2026/components/insights_carousel.tsx index 1861e34dd9..2f4f940f88 100644 --- a/front_end/src/app/(main)/midterms-2026/components/insights_carousel.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/insights_carousel.tsx @@ -6,22 +6,45 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useTranslations } from "next-intl"; -import { FC, useRef } from "react"; +import { FC, ReactNode, useCallback, useEffect, useRef, useState } from "react"; import cn from "@/utils/core/cn"; import InsightCard from "./insight_card"; import { CommunityInsight } from "../helpers/fetch_community_insights"; -const SCROLL_DELTA = 320; +const SCROLL_DELTA = 340; type Props = { insights: CommunityInsight[]; + title: ReactNode; }; -const InsightsCarousel: FC = ({ insights }) => { +const InsightsCarousel: FC = ({ insights, title }) => { const t = useTranslations(); const scrollRef = useRef(null); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(true); + + const updateScrollState = useCallback(() => { + const el = scrollRef.current; + if (!el) return; + setCanScrollLeft(el.scrollLeft > 1); + setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1); + }, []); + + useEffect(() => { + const el = scrollRef.current; + if (!el) return; + updateScrollState(); + el.addEventListener("scroll", updateScrollState, { passive: true }); + const observer = new ResizeObserver(updateScrollState); + observer.observe(el); + return () => { + el.removeEventListener("scroll", updateScrollState); + observer.disconnect(); + }; + }, [updateScrollState, insights.length]); const scroll = (direction: "left" | "right") => { const el = scrollRef.current; @@ -33,38 +56,58 @@ const InsightsCarousel: FC = ({ insights }) => { }; return ( -
-
- - +
+
+ {title} +
+ + +
-
- {insights.map((insight, i) => ( - - ))} + +
+
+ {insights.map((insight, i) => ( + + ))} +
+
+
); }; const scrollButtonClass = - "flex h-8 w-8 items-center justify-center rounded-full border border-gray-300 bg-gray-0 text-gray-700 transition-colors hover:border-gray-400 hover:text-gray-900 dark:border-gray-300-dark dark:bg-gray-0-dark dark:text-gray-700-dark dark:hover:border-gray-400-dark dark:hover:text-gray-900-dark"; + "flex h-8 w-8 items-center justify-center rounded-full border border-blue-400 bg-gray-0 text-blue-700 transition-colors hover:border-blue-500 hover:text-blue-800 disabled:cursor-not-allowed dark:border-blue-400-dark dark:bg-gray-0-dark dark:text-blue-700-dark dark:hover:border-blue-500-dark dark:hover:text-blue-800-dark"; export default InsightsCarousel; diff --git a/front_end/src/app/(main)/midterms-2026/components/live_badge.tsx b/front_end/src/app/(main)/midterms-2026/components/live_badge.tsx new file mode 100644 index 0000000000..5b6dd71a82 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/live_badge.tsx @@ -0,0 +1,14 @@ +import { getTranslations } from "next-intl/server"; + +export default async function LiveBadge() { + const t = await getTranslations(); + return ( + + + + + + {t("midtermsHubUpdatedRealtime")} + + ); +} diff --git a/front_end/src/app/(main)/midterms-2026/components/map_legend.tsx b/front_end/src/app/(main)/midterms-2026/components/map_legend.tsx new file mode 100644 index 0000000000..aa7806a473 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/map_legend.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { FC } from "react"; + +import cn from "@/utils/core/cn"; + +import { MIDTERMS_COLORS } from "../constants"; + +type Props = { + className?: string; +}; + +const MapLegend: FC = ({ className }) => { + const t = useTranslations(); + return ( +
+ + + {t("midtermsHubDemocrat")} + + + + {t("midtermsHubRepublican")} + + + + {t("midtermsHubNotContested")} + +
+ ); +}; + +export default MapLegend; diff --git a/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx b/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx index 94cfcf5ee7..51b1326d78 100644 --- a/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx @@ -1,20 +1,21 @@ import { FC } from "react"; +import ChamberTabs from "./chamber_tabs"; import GeographicMap from "./geographic_map"; import TileMap from "./tile_map"; -import { SenateRaceWithPost } from "../helpers/post_utils"; +import { SenateRaceWithQuestion } from "../helpers/post_utils"; type Props = { - races: SenateRaceWithPost[]; + races: SenateRaceWithQuestion[]; }; const ResponsiveMap: FC = ({ races }) => { return ( <> -
- +
+ } />
-
+
diff --git a/front_end/src/app/(main)/midterms-2026/components/semicircle_gauge.tsx b/front_end/src/app/(main)/midterms-2026/components/semicircle_gauge.tsx deleted file mode 100644 index 0f968dfaeb..0000000000 --- a/front_end/src/app/(main)/midterms-2026/components/semicircle_gauge.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { getTranslations } from "next-intl/server"; - -import { MIDTERMS_COLORS } from "../constants"; - -type Props = { - value: number; - color?: string; -}; - -const RADIUS = 45; -const STROKE_WIDTH = 10; -const CIRCUMFERENCE = Math.PI * RADIUS; - -export default async function SemicircleGauge({ - value, - color = MIDTERMS_COLORS.repPrimary, -}: Props) { - const t = await getTranslations(); - const fillLength = (value / 100) * CIRCUMFERENCE; - - return ( -
- -
- - {value}% - - - {t("midtermsHubChance")} - -
-
- ); -} diff --git a/front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx b/front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx index 5e2008c913..8121d0cf9f 100644 --- a/front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/state_tooltip.tsx @@ -1,27 +1,17 @@ "use client"; -import { faComment, faUsers } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Link from "next/link"; import { useTranslations } from "next-intl"; import { FC } from "react"; import { MIDTERMS_COLORS, STATE_NAMES } from "../constants"; -import { SenateRaceWithPost } from "../helpers/post_utils"; +import { SenateRaceWithQuestion } from "../helpers/post_utils"; type Props = { - race: SenateRaceWithPost; + race: SenateRaceWithQuestion; demWinPct: number | null; - forecasters: number; - comments: number; }; -const StateTooltipContent: FC = ({ - race, - demWinPct, - forecasters, - comments, -}) => { +const StateTooltipContent: FC = ({ race, demWinPct }) => { const t = useTranslations(); const stateName = STATE_NAMES[race.state] ?? race.name; @@ -34,11 +24,11 @@ const StateTooltipContent: FC = ({ : t("midtermsHubRepPct", { pct: 100 - demWinPct }); return ( -
-

+
+

{stateName}

-
+
= ({ {probLabel}
-
- - - {comments} - - - - {forecasters} - -
- {race.post && ( - - {t("midtermsHubViewQuestion")} - - )} +

+ {t("midtermsHubClickToView")} +

); }; diff --git a/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx b/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx index 143e73f546..4ff61a5221 100644 --- a/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx @@ -1,23 +1,18 @@ "use client"; -import { useTranslations } from "next-intl"; import { FC, MouseEvent, useState } from "react"; import cn from "@/utils/core/cn"; -import { MIDTERMS_COLORS, STATE_NAMES } from "../constants"; +import { STATE_NAMES } from "../constants"; import { US_TILE_GRID } from "../data"; +import MapLegend from "./map_legend"; import StateTooltipContent from "./state_tooltip"; -import { - getCommentsCount, - getDemWinPct, - getForecastersCount, - SenateRaceWithPost, -} from "../helpers/post_utils"; +import { getDemWinPct, SenateRaceWithQuestion } from "../helpers/post_utils"; import { getStateColor } from "../helpers/state_color"; type Props = { - races: SenateRaceWithPost[]; + races: SenateRaceWithQuestion[]; }; type HoverState = { @@ -45,6 +40,12 @@ const TileMap: FC = ({ races }) => { }); }; + const handleClick = (race: SenateRaceWithQuestion | undefined) => { + if (!race?.parentPost || !race.question) return; + const url = `/questions/${race.parentPost.id}/?sub-question=${race.question.id}`; + window.open(url, "_blank", "noopener,noreferrer"); + }; + const hoveredRace = hovered ? racesByState.get(hovered.abbr) : null; return ( @@ -58,7 +59,7 @@ const TileMap: FC = ({ races }) => { > {US_TILE_GRID.map(({ abbr, row, col }) => { const race = racesByState.get(abbr); - const demWinPct = getDemWinPct(race?.post ?? null); + const demWinPct = getDemWinPct(race?.question ?? null); const fillColor = getStateColor(demWinPct); const isContested = race !== undefined; @@ -68,6 +69,7 @@ const TileMap: FC = ({ races }) => { type="button" onMouseEnter={(e) => isContested && handleEnter(abbr, e)} onMouseLeave={() => setHovered(null)} + onClick={() => isContested && handleClick(race)} disabled={!isContested} aria-label={STATE_NAMES[abbr] ?? abbr} className={cn( @@ -100,40 +102,12 @@ const TileMap: FC = ({ races }) => { >
)} - -

- ); -}; - -const Legend: FC = () => { - const t = useTranslations(); - return ( -
- - - {t("midtermsHubDemocrat")} - - - - {t("midtermsHubRepublican")} - - - - {t("midtermsHubNotContested")} - +
); }; diff --git a/front_end/src/app/(main)/midterms-2026/components/updated_badge.tsx b/front_end/src/app/(main)/midterms-2026/components/updated_badge.tsx deleted file mode 100644 index 09f46fb19e..0000000000 --- a/front_end/src/app/(main)/midterms-2026/components/updated_badge.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import { useTranslations } from "next-intl"; -import { FC, useEffect, useState } from "react"; - -type Props = { - /** Latest update timestamp (ISO or epoch ms) */ - timestamp: string | number | null; -}; - -const UpdatedBadge: FC = ({ timestamp }) => { - const t = useTranslations(); - const [label, setLabel] = useState(() => formatRelative(timestamp)); - - useEffect(() => { - if (!timestamp) return; - const id = setInterval(() => setLabel(formatRelative(timestamp)), 60_000); - return () => clearInterval(id); - }, [timestamp]); - - if (!timestamp) { - return null; - } - - return ( - - - - - - {t("midtermsHubUpdatedAgo", { time: label })} - - ); -}; - -function formatRelative(timestamp: string | number | null): string { - if (!timestamp) return ""; - const then = - typeof timestamp === "number" ? timestamp : Date.parse(timestamp); - if (Number.isNaN(then)) return ""; - const diffSec = Math.max(0, Math.round((Date.now() - then) / 1000)); - if (diffSec < 60) return `${diffSec}s`; - const diffMin = Math.round(diffSec / 60); - if (diffMin < 60) return `${diffMin}m`; - const diffHr = Math.round(diffMin / 60); - if (diffHr < 24) return `${diffHr}h`; - const diffDay = Math.round(diffHr / 24); - return `${diffDay}d`; -} - -export default UpdatedBadge; diff --git a/front_end/src/app/(main)/midterms-2026/components/watch_card.tsx b/front_end/src/app/(main)/midterms-2026/components/watch_card.tsx new file mode 100644 index 0000000000..b2d5860ca4 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/watch_card.tsx @@ -0,0 +1,95 @@ +import { + faChartArea, + faClockRotateLeft, +} from "@fortawesome/free-solid-svg-icons"; +import { ReactNode, Suspense } from "react"; + +import { BasicQuestionContent } from "@/app/(main)/labor-hub/components/question_cards/basic_question"; +import { FlippableQuestionCard } from "@/app/(main)/labor-hub/components/question_cards/flippable_question_card"; +import { NoQuestionPlaceholder } from "@/app/(main)/labor-hub/components/question_cards/placeholder"; +import { + QuestionCard, + QuestionCardSkeleton, +} from "@/app/(main)/labor-hub/components/question_cards/question_card"; +import ServerPostsApi from "@/services/api/posts/posts.server"; +import { QuestionWithForecasts } from "@/types/question"; + +import ConsumerTileClient from "./consumer_tile_client"; + +type Props = { + questionId: number; + fallbackTitle: string; + contextNote?: ReactNode; +}; + +const CARD_BODY_HEIGHT = 180; +const CONSUMER_TILE_SCALE = 1.25; + +async function WatchCardContent({ questionId, fallbackTitle }: Props) { + let postData; + try { + postData = await ServerPostsApi.getPost(questionId, true); + } catch { + return ( + + + + ); + } + + const question = postData.question as QuestionWithForecasts | undefined; + + return ( + + {question ? ( +
+ +
+ ) : ( + + )} +
+ } + rightContent={ +
+ +
+ } + /> + ); +} + +export default function WatchCard(props: Props) { + return ( + }> + + + ); +} diff --git a/front_end/src/app/(main)/midterms-2026/constants.ts b/front_end/src/app/(main)/midterms-2026/constants.ts index 045fdd6acc..9e84b93bf9 100644 --- a/front_end/src/app/(main)/midterms-2026/constants.ts +++ b/front_end/src/app/(main)/midterms-2026/constants.ts @@ -8,7 +8,12 @@ export const MIDTERMS_COLORS = { tossUp: "#D3D1C7", leanR: "#EDA8A4", likelyR: "#D4504A", - notContested: "#FFFFFF", + // Matches the page body background (blue-200 in light mode) so uncontested + // states recede into the page; contested states pop above them. + notContested: "#eff4f4", + notContestedDark: "#2f4155", + stateStroke: "#ffffff", + stateStrokeDark: "#22262b", } as const; export const STATE_NAMES: Record = { diff --git a/front_end/src/app/(main)/midterms-2026/data.ts b/front_end/src/app/(main)/midterms-2026/data.ts index b2328287e9..83180bede0 100644 --- a/front_end/src/app/(main)/midterms-2026/data.ts +++ b/front_end/src/app/(main)/midterms-2026/data.ts @@ -1,64 +1,56 @@ export const MIDTERMS_PROJECT_ID = 32840; +// Single group-of-questions post that holds one binary subquestion per +// contested 2026 senate race. Subquestions are matched by `subQuestionLabel`. +export const SENATE_GROUP_POST_ID = 40598; + export type SenateRace = { state: string; name: string; - postId: number; + /** Matches `question.label` on a subquestion of SENATE_GROUP_POST_ID. */ + subQuestionLabel: string; }; -// Placeholder post IDs — real values supplied later from project 32840 +// Only contested 2026 senate races (matching subquestions of post 40598). export const SENATE_RACES: SenateRace[] = [ - { state: "AL", name: "Alabama Senate", postId: 0 }, - { state: "AK", name: "Alaska Senate", postId: 0 }, - { state: "AR", name: "Arkansas Senate", postId: 0 }, - { state: "CO", name: "Colorado Senate", postId: 0 }, - { state: "DE", name: "Delaware Senate", postId: 0 }, - { state: "FL", name: "Florida Senate", postId: 0 }, - { state: "GA", name: "Georgia Senate", postId: 0 }, - { state: "ID", name: "Idaho Senate", postId: 0 }, - { state: "IL", name: "Illinois Senate", postId: 0 }, - { state: "IA", name: "Iowa Senate", postId: 0 }, - { state: "KS", name: "Kansas Senate", postId: 0 }, - { state: "KY", name: "Kentucky Senate", postId: 0 }, - { state: "LA", name: "Louisiana Senate", postId: 0 }, - { state: "ME", name: "Maine Senate", postId: 0 }, - { state: "MA", name: "Massachusetts Senate", postId: 0 }, - { state: "MI", name: "Michigan Senate", postId: 0 }, - { state: "MN", name: "Minnesota Senate", postId: 0 }, - { state: "MS", name: "Mississippi Senate", postId: 0 }, - { state: "MT", name: "Montana Senate", postId: 0 }, - { state: "NE", name: "Nebraska Senate", postId: 0 }, - { state: "NH", name: "New Hampshire Senate", postId: 0 }, - { state: "NJ", name: "New Jersey Senate", postId: 0 }, - { state: "NM", name: "New Mexico Senate", postId: 0 }, - { state: "NC", name: "North Carolina Senate", postId: 0 }, - { state: "OH", name: "Ohio Senate", postId: 0 }, - { state: "OK", name: "Oklahoma Senate", postId: 0 }, - { state: "OR", name: "Oregon Senate", postId: 0 }, - { state: "RI", name: "Rhode Island Senate", postId: 0 }, - { state: "SC", name: "South Carolina Senate", postId: 0 }, - { state: "SD", name: "South Dakota Senate", postId: 0 }, - { state: "TN", name: "Tennessee Senate", postId: 0 }, - { state: "TX", name: "Texas Senate", postId: 0 }, - { state: "VA", name: "Virginia Senate", postId: 0 }, - { state: "WV", name: "West Virginia Senate", postId: 0 }, - { state: "WY", name: "Wyoming Senate", postId: 0 }, + { state: "GA", name: "Georgia Senate", subQuestionLabel: "Georgia" }, + { state: "IA", name: "Iowa Senate", subQuestionLabel: "Iowa" }, + { state: "ME", name: "Maine Senate", subQuestionLabel: "Maine" }, + { state: "MI", name: "Michigan Senate", subQuestionLabel: "Michigan" }, + { state: "MN", name: "Minnesota Senate", subQuestionLabel: "Minnesota" }, + { + state: "NC", + name: "North Carolina Senate", + subQuestionLabel: "North Carolina", + }, + { + state: "NH", + name: "New Hampshire Senate", + subQuestionLabel: "New Hampshire", + }, + { state: "OH", name: "Ohio Senate", subQuestionLabel: "Ohio" }, + { state: "TX", name: "Texas Senate", subQuestionLabel: "Texas" }, ]; export type ChamberQuestionIds = { + /** Multiple-choice: "Democrats" / "Republicans" / "Other" */ senateControl: number; + /** Multiple-choice: "Democrats" / "Republicans" / "Other" */ houseControl: number; - congressOutcomeGroup: number; + /** Multiple-choice with 4 options: "Dem Senate / Dem House" etc. */ + congressOutcome: number; + /** Numeric distribution */ voterTurnout: number; + /** Binary */ electionIntegrity: number; }; export const CHAMBER_QUESTIONS: ChamberQuestionIds = { - senateControl: 0, - houseControl: 0, - congressOutcomeGroup: 0, - voterTurnout: 0, - electionIntegrity: 0, + senateControl: 36370, + houseControl: 36369, + congressOutcome: 34484, + voterTurnout: 41177, + electionIntegrity: 36327, }; export type ConsequenceRow = { diff --git a/front_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.ts b/front_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.ts index a5f9926673..0ac6fa3c20 100644 --- a/front_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.ts +++ b/front_end/src/app/(main)/midterms-2026/helpers/fetch_community_insights.ts @@ -2,79 +2,60 @@ import "server-only"; import { cache } from "react"; import ServerCommentsApi from "@/services/api/comments/comments.server"; -import { CommentType, KeyFactor } from "@/types/comment"; +import { CommentType } from "@/types/comment"; +import { PostWithForecasts } from "@/types/post"; -import { fetchSenateRaces } from "./fetch_dashboard_data"; -import { SenateRaceWithPost } from "./post_utils"; - -export type KeyFactorInsight = { - type: "key-factor"; - keyFactor: KeyFactor; - race: SenateRaceWithPost; -}; +import { fetchChamberData, fetchSenateRaces } from "./fetch_dashboard_data"; export type TopCommentInsight = { type: "top-comment"; comment: CommentType; - race: SenateRaceWithPost; + sourcePost: PostWithForecasts; }; -export type CommunityInsight = KeyFactorInsight | TopCommentInsight; +export type CommunityInsight = TopCommentInsight; -const MAX_KEY_FACTORS = 4; -const MAX_TOP_COMMENTS = 4; +const MAX_TOP_COMMENTS = 8; export const fetchCommunityInsights = cache( async (): Promise => { - const races = await fetchSenateRaces(); - const racesWithPosts = races.filter((r) => r.post); - - if (!racesWithPosts.length) return []; - - const keyFactorCards: KeyFactorInsight[] = racesWithPosts - .flatMap((race) => - (race.post?.key_factors ?? []).map((kf) => ({ - type: "key-factor" as const, - keyFactor: kf, - race, - })) - ) - .sort((a, b) => b.keyFactor.vote.score - a.keyFactor.vote.score) - .slice(0, MAX_KEY_FACTORS); + const [{ parentPost: senateParent }, chamber] = await Promise.all([ + fetchSenateRaces(), + fetchChamberData(), + ]); + + const sourcePosts: PostWithForecasts[] = [ + senateParent, + chamber.senateControl, + chamber.houseControl, + chamber.congressOutcome, + chamber.voterTurnout, + chamber.electionIntegrity, + ].filter((p): p is PostWithForecasts => p !== null); + + if (!sourcePosts.length) return []; const topCommentResults = await Promise.all( - racesWithPosts.map(async (race) => { + sourcePosts.map(async (post) => { try { const { results } = await ServerCommentsApi.getComments({ - post: race.postId, + post: post.id, sort: "-vote_score", limit: 1, parent_isnull: true, }); const comment = results[0]; if (!comment) return null; - return { type: "top-comment" as const, comment, race }; + return { type: "top-comment" as const, comment, sourcePost: post }; } catch { return null; } }) ); - const topCommentCards: TopCommentInsight[] = topCommentResults + return topCommentResults .filter((c): c is TopCommentInsight => c !== null) .sort((a, b) => (b.comment.vote_score ?? 0) - (a.comment.vote_score ?? 0)) .slice(0, MAX_TOP_COMMENTS); - - return interleave(keyFactorCards, topCommentCards); } ); - -function interleave(a: T[], b: T[]): T[] { - const out: T[] = []; - const len = Math.max(a.length, b.length); - for (let i = 0; i < len; i++) { - if (i < a.length) out.push(a[i] as T); - if (i < b.length) out.push(b[i] as T); - } - return out; -} diff --git a/front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts b/front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts index 7b353c9496..e62540a21e 100644 --- a/front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts +++ b/front_end/src/app/(main)/midterms-2026/helpers/fetch_dashboard_data.ts @@ -3,34 +3,54 @@ import { cache } from "react"; import ServerPostsApi from "@/services/api/posts/posts.server"; import { PostWithForecasts } from "@/types/post"; +import { QuestionWithNumericForecasts } from "@/types/question"; -import { CHAMBER_QUESTIONS, SENATE_RACES } from "../data"; -import { SenateRaceWithPost } from "./post_utils"; +import { CHAMBER_QUESTIONS, SENATE_GROUP_POST_ID, SENATE_RACES } from "../data"; +import { SenateRaceWithQuestion } from "./post_utils"; export const fetchSenateRaces = cache( - async (): Promise => { - const ids = SENATE_RACES.map((r) => r.postId).filter((id) => id > 0); - if (!ids.length) { - return SENATE_RACES.map((r) => ({ ...r, post: null })); + async (): Promise<{ + races: SenateRaceWithQuestion[]; + parentPost: PostWithForecasts | null; + }> => { + if (!SENATE_GROUP_POST_ID) { + return { + races: SENATE_RACES.map((r) => ({ + ...r, + parentPost: null, + question: null, + })), + parentPost: null, + }; } - const { results } = await ServerPostsApi.getPostsWithCP({ - ids, - limit: ids.length, - }); + let parentPost: PostWithForecasts | null = null; + try { + parentPost = await ServerPostsApi.getPost(SENATE_GROUP_POST_ID, true); + } catch { + parentPost = null; + } + + const subQuestions = + (parentPost?.group_of_questions?.questions as + | QuestionWithNumericForecasts[] + | undefined) ?? []; + const byLabel = new Map(subQuestions.map((q) => [q.label, q])); - const byId = new Map(results.map((p) => [p.id, p])); - return SENATE_RACES.map((r) => ({ + const races: SenateRaceWithQuestion[] = SENATE_RACES.map((r) => ({ ...r, - post: byId.get(r.postId) ?? null, + parentPost, + question: byLabel.get(r.subQuestionLabel) ?? null, })); + + return { races, parentPost }; } ); export type ChamberData = { senateControl: PostWithForecasts | null; houseControl: PostWithForecasts | null; - congressOutcomeGroup: PostWithForecasts | null; + congressOutcome: PostWithForecasts | null; voterTurnout: PostWithForecasts | null; electionIntegrity: PostWithForecasts | null; }; @@ -41,7 +61,7 @@ export const fetchChamberData = cache(async (): Promise => { return { senateControl: null, houseControl: null, - congressOutcomeGroup: null, + congressOutcome: null, voterTurnout: null, electionIntegrity: null, }; @@ -56,8 +76,7 @@ export const fetchChamberData = cache(async (): Promise => { return { senateControl: byId.get(CHAMBER_QUESTIONS.senateControl) ?? null, houseControl: byId.get(CHAMBER_QUESTIONS.houseControl) ?? null, - congressOutcomeGroup: - byId.get(CHAMBER_QUESTIONS.congressOutcomeGroup) ?? null, + congressOutcome: byId.get(CHAMBER_QUESTIONS.congressOutcome) ?? null, voterTurnout: byId.get(CHAMBER_QUESTIONS.voterTurnout) ?? null, electionIntegrity: byId.get(CHAMBER_QUESTIONS.electionIntegrity) ?? null, }; diff --git a/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts b/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts index ddecb778d7..542bd731ad 100644 --- a/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts +++ b/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts @@ -4,18 +4,52 @@ import { scaleInternalLocation } from "@/utils/math"; import { SenateRace } from "../data"; -export type SenateRaceWithPost = SenateRace & { - post: PostWithForecasts | null; +export type SenateRaceWithQuestion = SenateRace & { + /** The parent group post (shared across all races). */ + parentPost: PostWithForecasts | null; + /** This race's specific binary subquestion. */ + question: QuestionWithNumericForecasts | null; }; +export function getQuestionBinaryProbability( + question: QuestionWithNumericForecasts | null +): number | null { + if (!question) return null; + if (question.type !== QuestionType.Binary) return null; + const center = + question.aggregations[question.default_aggregation_method]?.latest + ?.centers?.[0]; + return center ?? null; +} + export function getBinaryProbability( post: PostWithForecasts | null ): number | null { if (!post?.question) return null; - const q = post.question as QuestionWithNumericForecasts; - if (q.type !== QuestionType.Binary) return null; - const center = - q.aggregations[q.default_aggregation_method]?.latest?.centers?.[0]; + return getQuestionBinaryProbability( + post.question as QuestionWithNumericForecasts + ); +} + +/** + * For multiple_choice questions, returns the latest aggregated probability + * for the option matching `optionLabel` (case-insensitive). Returns null if + * the post isn't multiple_choice or the option isn't found. + */ +export function getMultipleChoiceOptionProbability( + post: PostWithForecasts | null, + optionLabel: string +): number | null { + if (!post?.question) return null; + const q = post.question; + if (q.type !== QuestionType.MultipleChoice) return null; + const options = q.options ?? []; + const idx = options.findIndex( + (opt) => opt.toLowerCase() === optionLabel.toLowerCase() + ); + if (idx < 0) return null; + const aggs = (q as unknown as QuestionWithNumericForecasts).aggregations; + const center = aggs?.[q.default_aggregation_method]?.latest?.centers?.[idx]; return center ?? null; } @@ -33,7 +67,7 @@ export function getNumericForecast( export function getForecastersCount(post: PostWithForecasts | null): number { if (!post) return 0; - return post.forecasts_count ?? 0; + return post.nr_forecasters ?? post.forecasts_count ?? 0; } export function getCommentsCount(post: PostWithForecasts | null): number { @@ -41,9 +75,10 @@ export function getCommentsCount(post: PostWithForecasts | null): number { return post.comment_count ?? 0; } -export function getDemWinPct(post: PostWithForecasts | null): number | null { - if (!post) return null; - const prob = getBinaryProbability(post); +export function getDemWinPct( + question: QuestionWithNumericForecasts | null +): number | null { + const prob = getQuestionBinaryProbability(question); if (prob == null) return null; return Math.round(prob * 100); } @@ -56,7 +91,7 @@ export function getLatestUpdateTime( if (!post?.question) continue; const q = post.question as QuestionWithNumericForecasts; const startTime = - q.aggregations[q.default_aggregation_method]?.latest?.start_time; + q.aggregations?.[q.default_aggregation_method]?.latest?.start_time; if (!startTime) continue; const d = new Date(startTime); if (!latest || d > latest) latest = d; diff --git a/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts b/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts index c7ff839d0b..67309cc6ac 100644 --- a/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts +++ b/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts @@ -8,24 +8,17 @@ export type StateTier = | "likelyR" | "notContested"; +// Toss-up is a tight 48-52% band; everything outside leans one way or the +// other. Likely is reserved for >65 / <35 (clear blowouts). export function getStateTier(demWinPct: number | null | undefined): StateTier { if (demWinPct == null) return "notContested"; - if (demWinPct > 70) return "likelyD"; - if (demWinPct >= 55) return "leanD"; - if (demWinPct >= 45) return "tossUp"; - if (demWinPct >= 30) return "leanR"; + if (demWinPct > 65) return "likelyD"; + if (demWinPct > 52) return "leanD"; + if (demWinPct >= 48) return "tossUp"; + if (demWinPct >= 35) return "leanR"; return "likelyR"; } export function getStateColor(demWinPct: number | null | undefined): string { return MIDTERMS_COLORS[getStateTier(demWinPct)]; } - -export const STATE_TIER_LABEL_KEY: Record = { - likelyD: "midtermsHubTierLikelyD", - leanD: "midtermsHubTierLeanD", - tossUp: "midtermsHubTierTossUp", - leanR: "midtermsHubTierLeanR", - likelyR: "midtermsHubTierLikelyR", - notContested: "midtermsHubTierNotContested", -}; diff --git a/front_end/src/app/(main)/midterms-2026/page.tsx b/front_end/src/app/(main)/midterms-2026/page.tsx index cd5f056faf..afe52b3a9c 100644 --- a/front_end/src/app/(main)/midterms-2026/page.tsx +++ b/front_end/src/app/(main)/midterms-2026/page.tsx @@ -36,13 +36,17 @@ export async function generateMetadata(): Promise { export default function MidtermsHubPage() { return ( -
- - - - - - +
+
+ +
+
+ + + + + +
); } diff --git a/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx b/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx index a57564a6dc..c668a8ef44 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx @@ -1,8 +1,11 @@ -import Link from "next/link"; import { getTranslations } from "next-intl/server"; +import { + SectionCard, + SectionHeader, +} from "@/app/(main)/labor-hub/components/section"; + import InsightsCarousel from "../components/insights_carousel"; -import { MIDTERMS_PROJECT_ID } from "../data"; import { fetchCommunityInsights } from "../helpers/fetch_community_insights"; export default async function CommunityInsightsSection() { @@ -12,24 +15,13 @@ export default async function CommunityInsightsSection() { if (!insights.length) return null; return ( -
-
-
-

- {t("midtermsHubCommunityInsights")} -

-

- {t("midtermsHubInsightsSubtitle")} -

-
- - {t("midtermsHubViewAllQuestions")} → - -
- -
+ + {t("midtermsHubCommunityInsights")} + } + /> + ); } diff --git a/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx b/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx index 8f6faa7b88..18dff401c3 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx @@ -1,53 +1,34 @@ -import { format } from "date-fns"; -import { getTranslations } from "next-intl/server"; +import { SectionCard } from "@/app/(main)/labor-hub/components/section"; import ChamberControlCard from "../components/chamber_control_card"; -import ChamberTabs from "../components/chamber_tabs"; import CongressOutcomeCard from "../components/congress_outcome_card"; import ResponsiveMap from "../components/responsive_map"; import { fetchChamberData, fetchSenateRaces, } from "../helpers/fetch_dashboard_data"; -import { getLatestUpdateTime } from "../helpers/post_utils"; export default async function ElectionsMapSection() { - const t = await getTranslations(); - const [races, chamber] = await Promise.all([ + const [{ races }, chamber] = await Promise.all([ fetchSenateRaces(), fetchChamberData(), ]); - const latest = getLatestUpdateTime([ - ...races.map((r) => r.post), - chamber.senateControl, - chamber.houseControl, - ]); - return ( -
-
- - {t("midtermsHubMapHeading")} - - {latest && ( - - {t("midtermsHubLastUpdated", { - date: format(latest, "MMM d, yyyy"), - })} - - )} -
-
-
- + +
+ {/* Map column: edge-to-edge of the SectionCard on lg+. */} +
-
+ {/* Sidebar column: provides its own padding so the cards stay inset + from the white card edges. lg:p-10 keeps the same visual padding + the SectionCard used to provide. */} +
- +
-
+ ); } diff --git a/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx b/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx index 737cbbe33d..50f6ff0f44 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx @@ -1,26 +1,33 @@ import { getTranslations } from "next-intl/server"; +import { + ContentParagraph, + SectionCard, + SectionHeader, +} from "@/app/(main)/labor-hub/components/section"; + import ConsequenceRow from "../components/consequence_row"; import { MOCK_CONSEQUENCES } from "../data"; export default async function ElectoralConsequencesSection() { const t = await getTranslations(); return ( -
-
-

- {t("midtermsHubElectoralConsequences")} -

-
-
-
- + + + {t("midtermsHubElectoralConsequences")} + + + {t("midtermsHubConsequencesSubtitle")} + +
+
+ {t("midtermsHubConsequenceQuestion")} - + {t("midtermsHubConsequenceIfRep")} - + {t("midtermsHubConsequenceIfDem")}
@@ -28,6 +35,6 @@ export default async function ElectoralConsequencesSection() { ))}
-
+ ); } diff --git a/front_end/src/app/(main)/midterms-2026/sections/footer.tsx b/front_end/src/app/(main)/midterms-2026/sections/footer.tsx index 1739d582dd..72ec6fcc09 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/footer.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/footer.tsx @@ -10,19 +10,19 @@ import { getLatestUpdateTime } from "../helpers/post_utils"; export default async function FooterSection() { const t = await getTranslations(); - const [races, chamber] = await Promise.all([ + const [{ parentPost: senateParent }, chamber] = await Promise.all([ fetchSenateRaces(), fetchChamberData(), ]); const latest = getLatestUpdateTime([ - ...races.map((r) => r.post), + senateParent, chamber.senateControl, chamber.houseControl, ]); return ( -
-
+
+

{t("midtermsHubFooterDisclaimer")}

{latest && ( -

+

{t("midtermsHubLastUpdatedFull", { date: format(latest, "MMMM d, yyyy, HH:mm 'UTC'"), })} diff --git a/front_end/src/app/(main)/midterms-2026/sections/hero.tsx b/front_end/src/app/(main)/midterms-2026/sections/hero.tsx index 7af95acfe0..69145b47c8 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/hero.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/hero.tsx @@ -1,36 +1,26 @@ import { getTranslations } from "next-intl/server"; -import UpdatedBadge from "../components/updated_badge"; -import { - fetchChamberData, - fetchSenateRaces, -} from "../helpers/fetch_dashboard_data"; -import { getLatestUpdateTime } from "../helpers/post_utils"; +import LiveBadge from "../components/live_badge"; export default async function HeroSection() { const t = await getTranslations(); - const [races, chamber] = await Promise.all([ - fetchSenateRaces(), - fetchChamberData(), - ]); - const latest = getLatestUpdateTime([ - ...races.map((r) => r.post), - chamber.senateControl, - chamber.houseControl, - ]); return ( -

-
-

- - metaculus - - | - {t("midtermsHubPageTitle")} -

- +
+

+ {t("midtermsHubHeroTitleLine1")}{" "} + + {t("midtermsHubHeroTitleLine2")} + +

+
+

+ {t("midtermsHubHeroSubtitle")} +

+
+ +
-
+ ); } diff --git a/front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx b/front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx index 5569d49ecb..7efe76df5f 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/things_to_watch.tsx @@ -1,113 +1,45 @@ -import { faUsers } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Link from "next/link"; import { getTranslations } from "next-intl/server"; -import DistributionCurve from "../components/distribution_curve"; -import SemicircleGauge from "../components/semicircle_gauge"; -import { MIDTERMS_COLORS } from "../constants"; -import { fetchChamberData } from "../helpers/fetch_dashboard_data"; import { - getBinaryProbability, - getForecastersCount, - getNumericForecast, -} from "../helpers/post_utils"; + ContentParagraph, + SectionCard, + SectionHeader, +} from "@/app/(main)/labor-hub/components/section"; -const PLACEHOLDER_TURNOUT = 49.3; -const PLACEHOLDER_INTEGRITY = 9; +import WatchCard from "../components/watch_card"; +import { CHAMBER_QUESTIONS } from "../data"; export default async function ThingsToWatchSection() { const t = await getTranslations(); - const chamber = await fetchChamberData(); - - const turnoutValue = - getNumericForecast(chamber.voterTurnout) ?? PLACEHOLDER_TURNOUT; - const turnoutForecasters = getForecastersCount(chamber.voterTurnout); - - const integrityProb = getBinaryProbability(chamber.electionIntegrity); - const integrityValue = - integrityProb != null - ? Math.round(integrityProb * 100) - : PLACEHOLDER_INTEGRITY; - const integrityForecasters = getForecastersCount(chamber.electionIntegrity); - const integrityLink = chamber.electionIntegrity - ? `/questions/${chamber.electionIntegrity.id}` - : null; - - const integrityKeyFactors: string[] = [ - t("midtermsHubIntegrityFactor1"), - t("midtermsHubIntegrityFactor2"), - t("midtermsHubIntegrityFactor3"), - ]; return ( -
-
- - {t("midtermsHubThingsToWatch")} - -
-
-
-
-

- {t("midtermsHubVoterTurnout")} -

- - - {t("midtermsHubForecastersCount", { count: turnoutForecasters })} - -
- -

+ + + {t("midtermsHubThingsToWatch")} + + + {t("midtermsHubThingsToWatchSubtitle")} + +

+
+ + {t("midtermsHubTurnoutContext")} -

-
- -
-
-

- {t("midtermsHubElectionIntegrity")} -

- - - {t("midtermsHubForecastersCount", { - count: integrityForecasters, - })} - -
-
- -
-

- {t("midtermsHubIntegrityQuestion")} -

-

- {t("midtermsHubIntegrityContext")} -

-
-
-
- {integrityKeyFactors.map((factor, i) => ( - - {factor} - - ))} -
- {integrityLink && ( - - {t("midtermsHubViewQuestion")} → - - )} -
+ +
+
+ + + {t("midtermsHubIntegrityContext")} + +
- + ); } From d1f100bd857ff5ea18978ed122d8873e032f7016 Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Wed, 6 May 2026 15:22:07 +0200 Subject: [PATCH 03/18] feat: midterms hub UI/UX polish + dark mode + i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- front_end/messages/cs.json | 92 +++++++-------- front_end/messages/en.json | 2 +- front_end/messages/es.json | 92 +++++++-------- front_end/messages/pt.json | 92 +++++++-------- front_end/messages/zh-TW.json | 92 +++++++-------- front_end/messages/zh.json | 92 +++++++-------- .../labor-hub/components/activity_card.tsx | 14 ++- .../components/chamber_control_card.tsx | 68 +++++++---- .../components/congress_outcome_card.tsx | 94 ++++++++++----- .../components/consequence_row.tsx | 20 ++-- .../midterms-2026/components/cv_bar.tsx | 67 +++++++++++ .../components/geographic_map.tsx | 76 ++++++++----- .../midterms-2026/components/insight_card.tsx | 2 +- .../midterms-2026/components/map_legend.tsx | 18 ++- .../components/map_tooltip_portal.tsx | 107 ++++++++++++++++++ .../components/responsive_map.tsx | 11 +- .../midterms-2026/components/tile_map.tsx | 84 +++++++++----- .../src/app/(main)/midterms-2026/constants.ts | 34 +++--- .../midterms-2026/helpers/state_color.ts | 39 +++---- .../sections/community_insights.tsx | 18 ++- .../sections/elections_map_section.tsx | 12 +- .../(main)/midterms-2026/sections/footer.tsx | 4 +- .../(main)/midterms-2026/sections/hero.tsx | 8 +- .../src/app/og/midterms-2026/route/route.ts | 30 +++-- 24 files changed, 755 insertions(+), 413 deletions(-) create mode 100644 front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/map_tooltip_portal.tsx diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index c5a6fd2524..51b85fb931 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -2186,50 +2186,50 @@ "whyTrustMetaculusLessNoise": "Předpovědi Metaculus, poháněné davem, prořezávají šum tím, že zakládají každou předpověď na transparentních důkazech, odpovědném skóre a desetiletí doložené přesnosti. Metaculus vybavuje politiky, výzkumníky, novináře a korporátní organizace předpověďmi založenými na důkazech, které poskytují jasný, kvantifikovatelný vhled do nejkritičtějších nejistot světa. Prozkoumejte naši nabídku Business řešení a zjistěte, jak může Metaculus zlepšit rozhodování vaší organizace.", "publishTimeLockedDescription": "Čas publikace nelze po vytvoření změnit.", "thousandsOfOpenQuestions": "20 000+ otevřených otázek", - "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", - "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubChamberSenate": "Senate", - "midtermsHubChamberHouse": "House", - "midtermsHubChamberGovernor": "Governor", - "midtermsHubChamberControl": "Chamber control", - "midtermsHubCongressForecast": "Congress control forecast", - "midtermsHubDemsNeed": "Dems need +{count}", - "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", - "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", - "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", - "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", - "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", - "midtermsHubThingsToWatch": "Things to Watch", - "midtermsHubVoterTurnout": "Voter turnout forecast", - "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", - "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubElectoralConsequences": "Electoral Consequences", - "midtermsHubConsequenceQuestion": "Question", - "midtermsHubConsequenceIfRep": "if Rep Congress", - "midtermsHubConsequenceIfDem": "if Dem Congress", - "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", - "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", - "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", - "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", - "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubScrollLeft": "Scroll insights left", - "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubDemocrat": "Democrat", - "midtermsHubRepublican": "Republican", - "midtermsHubNotContested": "Not contested", - "midtermsHubDemPct": "{pct}% Democrat", - "midtermsHubRepPct": "{pct}% Republican", - "midtermsHubNoForecast": "No forecast", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", - "midtermsHubHeroTitleLine1": "2026 US", - "midtermsHubHeroTitleLine2": "Midterm Elections", - "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", - "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", - "midtermsHubUpdatedRealtime": "Updated real-time", - "midtermsHubMetaculusUser": "Metaculus User", - "midtermsHubComingSoon": "Coming Soon", - "midtermsHubClickToView": "Click to view full question" + "midtermsHubMetaTitle": "Mezičasové volby USA 2026 | Metaculus", + "midtermsHubMetaDescription": "Předpovědi komunity Metaculus v reálném čase k mezičasovým volbám USA v roce 2026.", + "midtermsHubLastUpdatedFull": "Naposledy aktualizováno {date}", + "midtermsHubChamberSenate": "Senát", + "midtermsHubChamberHouse": "Sněmovna reprezentantů", + "midtermsHubChamberGovernor": "Guvernér", + "midtermsHubChamberControl": "Kontrola komory", + "midtermsHubCongressForecast": "Kontrola Kongresu", + "midtermsHubDemsNeed": "Demokraté potřebují +{count}", + "midtermsHubOutcomeRepRep": "Senát Rep / Sněmovna Rep", + "midtermsHubOutcomeRepDem": "Senát Rep / Sněmovna Dem", + "midtermsHubOutcomeDemRep": "Senát Dem / Sněmovna Rep", + "midtermsHubOutcomeDemDem": "Senát Dem / Sněmovna Dem", + "midtermsHubCongressSummary": "Předpovídači očekávají rozdělený Kongres – republikáni si pravděpodobně udrží Senát, zatímco demokraté jsou favority na získání Sněmovny reprezentantů.", + "midtermsHubThingsToWatch": "Co sledovat", + "midtermsHubVoterTurnout": "Předpověď volební účasti", + "midtermsHubTurnoutContext": "Vyšší účast u mezičasových voleb historicky souvisela se silnějšími výsledky demokratů.", + "midtermsHubElectionIntegrity": "Sledování integrity voleb", + "midtermsHubIntegrityContext": "Cyklus 2026 zahrnuje spory o překreslování volebních obvodů uprostřed dekády a probíhající soudní spory o pravidla hlasování v několika státech.", + "midtermsHubElectoralConsequences": "Volební důsledky", + "midtermsHubConsequenceQuestion": "Otázka", + "midtermsHubConsequenceIfRep": "při Republikánském Kongresu", + "midtermsHubConsequenceIfDem": "při Demokratickém Kongresu", + "midtermsHubConsequenceClimate": "Bude před rokem 2028 přijata významná federální klimatická legislativa?", + "midtermsHubConsequenceMinWage": "Bude před rokem 2028 zvýšena federální minimální mzda?", + "midtermsHubConsequenceImmigration": "Bude před rokem 2028 přijat významný zákon o imigrační reformě?", + "midtermsHubConsequenceShutdown": "Dojde před rokem 2028 k uzavření vlády na více než 14 dní?", + "midtermsHubCommunityInsights": "Pohledy komunity", + "midtermsHubScrollLeft": "Posunout poznatky doleva", + "midtermsHubScrollRight": "Posunout poznatky doprava", + "midtermsHubDemocrat": "Demokraté", + "midtermsHubRepublican": "Republikáni", + "midtermsHubNotContested": "Bez voleb", + "midtermsHubDemPct": "{pct}% demokraté", + "midtermsHubRepPct": "{pct}% republikáni", + "midtermsHubNoForecast": "Bez předpovědi", + "midtermsHubFooterDisclaimer": "Předpovědi komunity Metaculus. Bez vazby na jakoukoli politickou stranu.", + "midtermsHubHeroTitleLine1": "Mezičasové volby", + "midtermsHubHeroTitleLine2": "USA 2026", + "midtermsHubHeroSubtitle": "Předpovědi komunity Metaculus v reálném čase k mezičasovým volbám USA v roce 2026.", + "midtermsHubThingsToWatchSubtitle": "Ukazatele, které stojí za sledování v průběhu cyklu.", + "midtermsHubConsequencesSubtitle": "Jak se předpovídané politické výsledky mění v závislosti na tom, která strana ovládá Kongres.", + "midtermsHubUpdatedRealtime": "Aktualizováno v reálném čase", + "midtermsHubMetaculusUser": "Uživatel Metaculus", + "midtermsHubComingSoon": "Již brzy", + "midtermsHubClickToView": "Kliknutím zobrazíte celou otázku" } diff --git a/front_end/messages/en.json b/front_end/messages/en.json index e25242cd68..658e7c0c1d 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2182,7 +2182,7 @@ "midtermsHubChamberHouse": "House", "midtermsHubChamberGovernor": "Governor", "midtermsHubChamberControl": "Chamber control", - "midtermsHubCongressForecast": "Congress control forecast", + "midtermsHubCongressForecast": "Congress Control", "midtermsHubDemsNeed": "Dems need +{count}", "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", diff --git a/front_end/messages/es.json b/front_end/messages/es.json index d52a72ce75..cb4027a2bf 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -2186,50 +2186,50 @@ "whyTrustMetaculusLessNoise": "Los pronósticos impulsados por la multitud de Metaculus cortan el ruido al basar cada predicción en evidencia transparente, puntuaciones responsables y una década de precisión demostrada. Metaculus equipa a los responsables de políticas, investigadores, periodistas y organizaciones corporativas con pronósticos basados en evidencia que ofrecen una visión clara y cuantificable de las incertidumbres más críticas del mundo. Explora nuestra suite de Soluciones Empresariales para aprender cómo Metaculus puede mejorar la toma de decisiones de tu organización.", "publishTimeLockedDescription": "La hora de publicación no puede cambiarse después de la creación.", "thousandsOfOpenQuestions": "20,000+ preguntas abiertas", - "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", - "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubChamberSenate": "Senate", - "midtermsHubChamberHouse": "House", - "midtermsHubChamberGovernor": "Governor", - "midtermsHubChamberControl": "Chamber control", - "midtermsHubCongressForecast": "Congress control forecast", - "midtermsHubDemsNeed": "Dems need +{count}", - "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", - "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", - "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", - "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", - "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", - "midtermsHubThingsToWatch": "Things to Watch", - "midtermsHubVoterTurnout": "Voter turnout forecast", - "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", - "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubElectoralConsequences": "Electoral Consequences", - "midtermsHubConsequenceQuestion": "Question", - "midtermsHubConsequenceIfRep": "if Rep Congress", - "midtermsHubConsequenceIfDem": "if Dem Congress", - "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", - "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", - "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", - "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", - "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubScrollLeft": "Scroll insights left", - "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubDemocrat": "Democrat", - "midtermsHubRepublican": "Republican", - "midtermsHubNotContested": "Not contested", - "midtermsHubDemPct": "{pct}% Democrat", - "midtermsHubRepPct": "{pct}% Republican", - "midtermsHubNoForecast": "No forecast", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", - "midtermsHubHeroTitleLine1": "2026 US", - "midtermsHubHeroTitleLine2": "Midterm Elections", - "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", - "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", - "midtermsHubUpdatedRealtime": "Updated real-time", - "midtermsHubMetaculusUser": "Metaculus User", - "midtermsHubComingSoon": "Coming Soon", - "midtermsHubClickToView": "Click to view full question" + "midtermsHubMetaTitle": "Elecciones intermedias de EE. UU. de 2026 | Metaculus", + "midtermsHubMetaDescription": "Pronósticos en tiempo real de la comunidad de Metaculus sobre las elecciones intermedias de EE. UU. de 2026.", + "midtermsHubLastUpdatedFull": "Última actualización {date}", + "midtermsHubChamberSenate": "Senado", + "midtermsHubChamberHouse": "Cámara de Representantes", + "midtermsHubChamberGovernor": "Gobernador", + "midtermsHubChamberControl": "Control de cámara", + "midtermsHubCongressForecast": "Control del Congreso", + "midtermsHubDemsNeed": "Los demócratas necesitan +{count}", + "midtermsHubOutcomeRepRep": "Senado Rep / Cámara Rep", + "midtermsHubOutcomeRepDem": "Senado Rep / Cámara Dem", + "midtermsHubOutcomeDemRep": "Senado Dem / Cámara Rep", + "midtermsHubOutcomeDemDem": "Senado Dem / Cámara Dem", + "midtermsHubCongressSummary": "Los pronosticadores esperan un Congreso dividido, con los republicanos probablemente manteniendo el Senado y los demócratas favorecidos para recuperar la Cámara.", + "midtermsHubThingsToWatch": "Aspectos a observar", + "midtermsHubVoterTurnout": "Pronóstico de participación electoral", + "midtermsHubTurnoutContext": "Una mayor participación en las elecciones intermedias ha estado históricamente correlacionada con un mejor desempeño demócrata.", + "midtermsHubElectionIntegrity": "Vigilancia de la integridad electoral", + "midtermsHubIntegrityContext": "El ciclo de 2026 presenta disputas de redistribución a mitad de década y litigios en curso sobre las reglas de votación en varios estados.", + "midtermsHubElectoralConsequences": "Consecuencias electorales", + "midtermsHubConsequenceQuestion": "Pregunta", + "midtermsHubConsequenceIfRep": "si Congreso Rep", + "midtermsHubConsequenceIfDem": "si Congreso Dem", + "midtermsHubConsequenceClimate": "¿Se aprobará una legislación climática federal importante antes de 2028?", + "midtermsHubConsequenceMinWage": "¿Se aumentará el salario mínimo federal antes de 2028?", + "midtermsHubConsequenceImmigration": "¿Se aprobará un proyecto de ley importante de reforma migratoria antes de 2028?", + "midtermsHubConsequenceShutdown": "¿Habrá un cierre del gobierno que dure más de 14 días antes de 2028?", + "midtermsHubCommunityInsights": "Perspectivas de la comunidad", + "midtermsHubScrollLeft": "Desplazar perspectivas a la izquierda", + "midtermsHubScrollRight": "Desplazar perspectivas a la derecha", + "midtermsHubDemocrat": "Demócrata", + "midtermsHubRepublican": "Republicano", + "midtermsHubNotContested": "No disputado", + "midtermsHubDemPct": "{pct}% Demócrata", + "midtermsHubRepPct": "{pct}% Republicano", + "midtermsHubNoForecast": "Sin pronóstico", + "midtermsHubFooterDisclaimer": "Pronósticos de la comunidad de Metaculus. No afiliados a ningún partido político.", + "midtermsHubHeroTitleLine1": "EE. UU. 2026", + "midtermsHubHeroTitleLine2": "Elecciones intermedias", + "midtermsHubHeroSubtitle": "Pronósticos en tiempo real de la comunidad de Metaculus sobre las elecciones intermedias de EE. UU. de 2026.", + "midtermsHubThingsToWatchSubtitle": "Indicadores que vale la pena observar a medida que avanza el ciclo.", + "midtermsHubConsequencesSubtitle": "Cómo cambian los resultados políticos previstos según el partido que controle el Congreso.", + "midtermsHubUpdatedRealtime": "Actualizado en tiempo real", + "midtermsHubMetaculusUser": "Usuario de Metaculus", + "midtermsHubComingSoon": "Próximamente", + "midtermsHubClickToView": "Haz clic para ver la pregunta completa" } diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index 5214bd04f1..2c2699846b 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -2184,50 +2184,50 @@ "whyTrustMetaculusLessNoise": "As previsões baseadas na multidão do Metaculus cortam o ruído ao basear cada previsão em evidências transparentes, pontuações responsáveis e uma década de precisão demonstrada. O Metaculus equipa formuladores de políticas, pesquisadores, jornalistas e organizações corporativas com previsões baseadas em evidências que oferecem insights claros e quantificáveis sobre as incertezas mais críticas do mundo. Explore nosso conjunto de Soluções Empresariais para saber mais sobre como o Metaculus pode melhorar a tomada de decisões da sua organização.", "publishTimeLockedDescription": "O horário de publicação não pode ser alterado após a criação.", "thousandsOfOpenQuestions": "20.000+ perguntas abertas", - "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", - "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubChamberSenate": "Senate", - "midtermsHubChamberHouse": "House", - "midtermsHubChamberGovernor": "Governor", - "midtermsHubChamberControl": "Chamber control", - "midtermsHubCongressForecast": "Congress control forecast", - "midtermsHubDemsNeed": "Dems need +{count}", - "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", - "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", - "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", - "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", - "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", - "midtermsHubThingsToWatch": "Things to Watch", - "midtermsHubVoterTurnout": "Voter turnout forecast", - "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", - "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubElectoralConsequences": "Electoral Consequences", - "midtermsHubConsequenceQuestion": "Question", - "midtermsHubConsequenceIfRep": "if Rep Congress", - "midtermsHubConsequenceIfDem": "if Dem Congress", - "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", - "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", - "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", - "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", - "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubScrollLeft": "Scroll insights left", - "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubDemocrat": "Democrat", - "midtermsHubRepublican": "Republican", - "midtermsHubNotContested": "Not contested", - "midtermsHubDemPct": "{pct}% Democrat", - "midtermsHubRepPct": "{pct}% Republican", - "midtermsHubNoForecast": "No forecast", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", - "midtermsHubHeroTitleLine1": "2026 US", - "midtermsHubHeroTitleLine2": "Midterm Elections", - "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", - "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", - "midtermsHubUpdatedRealtime": "Updated real-time", - "midtermsHubMetaculusUser": "Metaculus User", - "midtermsHubComingSoon": "Coming Soon", - "midtermsHubClickToView": "Click to view full question" + "midtermsHubMetaTitle": "Eleições de meio de mandato dos EUA 2026 | Metaculus", + "midtermsHubMetaDescription": "Previsões em tempo real da comunidade Metaculus sobre as eleições de meio de mandato dos EUA de 2026.", + "midtermsHubLastUpdatedFull": "Última atualização {date}", + "midtermsHubChamberSenate": "Senado", + "midtermsHubChamberHouse": "Câmara dos Representantes", + "midtermsHubChamberGovernor": "Governador", + "midtermsHubChamberControl": "Controle da câmara", + "midtermsHubCongressForecast": "Controle do Congresso", + "midtermsHubDemsNeed": "Os democratas precisam de +{count}", + "midtermsHubOutcomeRepRep": "Senado Rep / Câmara Rep", + "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.", + "midtermsHubThingsToWatch": "O que observar", + "midtermsHubVoterTurnout": "Previsão de participação dos eleitores", + "midtermsHubTurnoutContext": "Uma maior participação nas eleições de meio de mandato historicamente esteve correlacionada com um melhor desempenho democrata.", + "midtermsHubElectionIntegrity": "Monitoramento da integridade eleitoral", + "midtermsHubIntegrityContext": "O ciclo de 2026 apresenta disputas de redesenho de distritos no meio da década e ações judiciais em andamento sobre regras de votação em vários estados.", + "midtermsHubElectoralConsequences": "Consequências eleitorais", + "midtermsHubConsequenceQuestion": "Pergunta", + "midtermsHubConsequenceIfRep": "se Congresso Rep", + "midtermsHubConsequenceIfDem": "se Congresso Dem", + "midtermsHubConsequenceClimate": "Uma legislação climática federal importante será aprovada antes de 2028?", + "midtermsHubConsequenceMinWage": "O salário mínimo federal será aumentado antes de 2028?", + "midtermsHubConsequenceImmigration": "Um projeto de reforma migratória importante será aprovado antes de 2028?", + "midtermsHubConsequenceShutdown": "Haverá uma paralisação do governo com mais de 14 dias antes de 2028?", + "midtermsHubCommunityInsights": "Insights da comunidade", + "midtermsHubScrollLeft": "Rolar insights para a esquerda", + "midtermsHubScrollRight": "Rolar insights para a direita", + "midtermsHubDemocrat": "Democrata", + "midtermsHubRepublican": "Republicano", + "midtermsHubNotContested": "Sem disputa", + "midtermsHubDemPct": "{pct}% Democrata", + "midtermsHubRepPct": "{pct}% Republicano", + "midtermsHubNoForecast": "Sem previsão", + "midtermsHubFooterDisclaimer": "Previsões pela comunidade Metaculus. Não afiliada a nenhum partido político.", + "midtermsHubHeroTitleLine1": "Eleições de Meio", + "midtermsHubHeroTitleLine2": "de Mandato dos EUA 2026", + "midtermsHubHeroSubtitle": "Previsões em tempo real da comunidade Metaculus sobre as eleições de meio de mandato dos EUA de 2026.", + "midtermsHubThingsToWatchSubtitle": "Indicadores que vale a pena acompanhar à medida que o ciclo avança.", + "midtermsHubConsequencesSubtitle": "Como os resultados políticos previstos mudam dependendo de qual partido controla o Congresso.", + "midtermsHubUpdatedRealtime": "Atualizado em tempo real", + "midtermsHubMetaculusUser": "Usuário Metaculus", + "midtermsHubComingSoon": "Em breve", + "midtermsHubClickToView": "Clique para ver a pergunta completa" } diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index a37200f644..6348bdafe1 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -2183,50 +2183,50 @@ "whyTrustMetaculusLessNoise": "Metaculus 的群眾驅動預測通過將每一個預測植根於透明的證據、負責的計分和十年的已證準確性,抑制了噪音。Metaculus 為政策制定者、研究人員、記者和企業組織提供基於證據的預測,為全球最重要的不確定性提供清晰、可量化的洞察。探索我們的 企業解決方案,了解 Metaculus 如何改善貴組織的決策。", "publishTimeLockedDescription": "建立後將無法更改發佈時間。", "thousandsOfOpenQuestions": "20,000+ 開放問題", - "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", - "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubChamberSenate": "Senate", - "midtermsHubChamberHouse": "House", - "midtermsHubChamberGovernor": "Governor", - "midtermsHubChamberControl": "Chamber control", - "midtermsHubCongressForecast": "Congress control forecast", - "midtermsHubDemsNeed": "Dems need +{count}", - "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", - "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", - "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", - "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", - "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", - "midtermsHubThingsToWatch": "Things to Watch", - "midtermsHubVoterTurnout": "Voter turnout forecast", - "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", - "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubElectoralConsequences": "Electoral Consequences", - "midtermsHubConsequenceQuestion": "Question", - "midtermsHubConsequenceIfRep": "if Rep Congress", - "midtermsHubConsequenceIfDem": "if Dem Congress", - "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", - "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", - "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", - "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", - "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubScrollLeft": "Scroll insights left", - "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubDemocrat": "Democrat", - "midtermsHubRepublican": "Republican", - "midtermsHubNotContested": "Not contested", - "midtermsHubDemPct": "{pct}% Democrat", - "midtermsHubRepPct": "{pct}% Republican", - "midtermsHubNoForecast": "No forecast", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", - "midtermsHubHeroTitleLine1": "2026 US", - "midtermsHubHeroTitleLine2": "Midterm Elections", - "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", - "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", - "midtermsHubUpdatedRealtime": "Updated real-time", - "midtermsHubMetaculusUser": "Metaculus User", - "midtermsHubComingSoon": "Coming Soon", - "midtermsHubClickToView": "Click to view full question" + "midtermsHubMetaTitle": "2026 年美國期中選舉 | Metaculus", + "midtermsHubMetaDescription": "Metaculus 社群對 2026 年美國期中選舉的即時預測。", + "midtermsHubLastUpdatedFull": "最後更新 {date}", + "midtermsHubChamberSenate": "參議院", + "midtermsHubChamberHouse": "眾議院", + "midtermsHubChamberGovernor": "州長", + "midtermsHubChamberControl": "議院控制權", + "midtermsHubCongressForecast": "國會控制權", + "midtermsHubDemsNeed": "民主黨需要 +{count} 席", + "midtermsHubOutcomeRepRep": "共和黨參議院 / 共和黨眾議院", + "midtermsHubOutcomeRepDem": "共和黨參議院 / 民主黨眾議院", + "midtermsHubOutcomeDemRep": "民主黨參議院 / 共和黨眾議院", + "midtermsHubOutcomeDemDem": "民主黨參議院 / 民主黨眾議院", + "midtermsHubCongressSummary": "預測者預期國會將分裂——共和黨可能掌握參議院,民主黨則有望奪回眾議院。", + "midtermsHubThingsToWatch": "值得關注的事項", + "midtermsHubVoterTurnout": "選民投票率預測", + "midtermsHubTurnoutContext": "期中選舉的高投票率歷來與民主黨更好的表現相關。", + "midtermsHubElectionIntegrity": "選舉誠信監測", + "midtermsHubIntegrityContext": "2026 年選舉週期涉及十年期中的選區重劃爭議,以及多州關於投票規則的訴訟。", + "midtermsHubElectoralConsequences": "選舉後果", + "midtermsHubConsequenceQuestion": "問題", + "midtermsHubConsequenceIfRep": "若共和黨掌握國會", + "midtermsHubConsequenceIfDem": "若民主黨掌握國會", + "midtermsHubConsequenceClimate": "2028 年之前是否會通過重要的聯邦氣候立法?", + "midtermsHubConsequenceMinWage": "2028 年之前是否會提高聯邦最低工資?", + "midtermsHubConsequenceImmigration": "2028 年之前是否會通過重要的移民改革法案?", + "midtermsHubConsequenceShutdown": "2028 年之前是否會發生持續超過 14 天的政府停擺?", + "midtermsHubCommunityInsights": "社群觀點", + "midtermsHubScrollLeft": "向左捲動觀點", + "midtermsHubScrollRight": "向右捲動觀點", + "midtermsHubDemocrat": "民主黨", + "midtermsHubRepublican": "共和黨", + "midtermsHubNotContested": "無競爭", + "midtermsHubDemPct": "{pct}% 民主黨", + "midtermsHubRepPct": "{pct}% 共和黨", + "midtermsHubNoForecast": "無預測", + "midtermsHubFooterDisclaimer": "由 Metaculus 社群進行的預測。不隸屬於任何政黨。", + "midtermsHubHeroTitleLine1": "2026 年美國", + "midtermsHubHeroTitleLine2": "期中選舉", + "midtermsHubHeroSubtitle": "Metaculus 社群對 2026 年美國期中選舉的即時預測。", + "midtermsHubThingsToWatchSubtitle": "選舉週期推進過程中值得關注的指標。", + "midtermsHubConsequencesSubtitle": "預測的政策結果如何根據掌握國會的政黨而變化。", + "midtermsHubUpdatedRealtime": "即時更新", + "midtermsHubMetaculusUser": "Metaculus 使用者", + "midtermsHubComingSoon": "即將推出", + "midtermsHubClickToView": "點擊查看完整問題" } diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index 577ad4509b..e37b5a1196 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -2188,50 +2188,50 @@ "whyTrustMetaculusLessNoise": "Metaculus 的众包预测通过以透明证据、可追溯的评分以及十年证明的准确性为基础来削减预测中的噪音。Metaculus 为政策制定者、研究人员、记者和企业组织提供基于证据的预测,为世界上最重要的不确定性提供清晰、可量化的洞察。了解我们的企业解决方案,了解 Metaculus 如何改善贵组织的决策。", "publishTimeLockedDescription": "创建后将无法更改发布时间。", "thousandsOfOpenQuestions": "20,000+ 开放问题", - "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", - "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubLastUpdatedFull": "Last updated {date}", - "midtermsHubChamberSenate": "Senate", - "midtermsHubChamberHouse": "House", - "midtermsHubChamberGovernor": "Governor", - "midtermsHubChamberControl": "Chamber control", - "midtermsHubCongressForecast": "Congress control forecast", - "midtermsHubDemsNeed": "Dems need +{count}", - "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", - "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", - "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", - "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", - "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", - "midtermsHubThingsToWatch": "Things to Watch", - "midtermsHubVoterTurnout": "Voter turnout forecast", - "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", - "midtermsHubElectionIntegrity": "Election integrity watch", - "midtermsHubIntegrityContext": "The 2026 cycle features mid-decade redistricting disputes and ongoing litigation over voting rules in several states.", - "midtermsHubElectoralConsequences": "Electoral Consequences", - "midtermsHubConsequenceQuestion": "Question", - "midtermsHubConsequenceIfRep": "if Rep Congress", - "midtermsHubConsequenceIfDem": "if Dem Congress", - "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", - "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", - "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", - "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", - "midtermsHubCommunityInsights": "Community Insights", - "midtermsHubScrollLeft": "Scroll insights left", - "midtermsHubScrollRight": "Scroll insights right", - "midtermsHubDemocrat": "Democrat", - "midtermsHubRepublican": "Republican", - "midtermsHubNotContested": "Not contested", - "midtermsHubDemPct": "{pct}% Democrat", - "midtermsHubRepPct": "{pct}% Republican", - "midtermsHubNoForecast": "No forecast", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", - "midtermsHubHeroTitleLine1": "2026 US", - "midtermsHubHeroTitleLine2": "Midterm Elections", - "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", - "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", - "midtermsHubUpdatedRealtime": "Updated real-time", - "midtermsHubMetaculusUser": "Metaculus User", - "midtermsHubComingSoon": "Coming Soon", - "midtermsHubClickToView": "Click to view full question" + "midtermsHubMetaTitle": "2026 年美国中期选举 | Metaculus", + "midtermsHubMetaDescription": "Metaculus 社区对 2026 年美国中期选举的实时预测。", + "midtermsHubLastUpdatedFull": "最后更新 {date}", + "midtermsHubChamberSenate": "参议院", + "midtermsHubChamberHouse": "众议院", + "midtermsHubChamberGovernor": "州长", + "midtermsHubChamberControl": "议院控制权", + "midtermsHubCongressForecast": "国会控制权", + "midtermsHubDemsNeed": "民主党需要 +{count} 席", + "midtermsHubOutcomeRepRep": "共和党参议院 / 共和党众议院", + "midtermsHubOutcomeRepDem": "共和党参议院 / 民主党众议院", + "midtermsHubOutcomeDemRep": "民主党参议院 / 共和党众议院", + "midtermsHubOutcomeDemDem": "民主党参议院 / 民主党众议院", + "midtermsHubCongressSummary": "预测者预计国会将分裂——共和党可能控制参议院,民主党有望夺回众议院。", + "midtermsHubThingsToWatch": "值得关注", + "midtermsHubVoterTurnout": "选民投票率预测", + "midtermsHubTurnoutContext": "中期选举的高投票率历来与民主党的更好表现相关。", + "midtermsHubElectionIntegrity": "选举诚信监测", + "midtermsHubIntegrityContext": "2026 年选举周期涉及十年中期的选区重划争议,以及若干州关于投票规则的诉讼。", + "midtermsHubElectoralConsequences": "选举后果", + "midtermsHubConsequenceQuestion": "问题", + "midtermsHubConsequenceIfRep": "若共和党控制国会", + "midtermsHubConsequenceIfDem": "若民主党控制国会", + "midtermsHubConsequenceClimate": "2028 年之前是否会通过重要的联邦气候立法?", + "midtermsHubConsequenceMinWage": "2028 年之前是否会提高联邦最低工资?", + "midtermsHubConsequenceImmigration": "2028 年之前是否会通过重要的移民改革法案?", + "midtermsHubConsequenceShutdown": "2028 年之前是否会发生持续超过 14 天的政府停摆?", + "midtermsHubCommunityInsights": "社区洞察", + "midtermsHubScrollLeft": "向左滚动洞察", + "midtermsHubScrollRight": "向右滚动洞察", + "midtermsHubDemocrat": "民主党", + "midtermsHubRepublican": "共和党", + "midtermsHubNotContested": "无竞争", + "midtermsHubDemPct": "{pct}% 民主党", + "midtermsHubRepPct": "{pct}% 共和党", + "midtermsHubNoForecast": "无预测", + "midtermsHubFooterDisclaimer": "由 Metaculus 社区进行的预测。不隶属于任何政党。", + "midtermsHubHeroTitleLine1": "2026 年美国", + "midtermsHubHeroTitleLine2": "中期选举", + "midtermsHubHeroSubtitle": "Metaculus 社区对 2026 年美国中期选举的实时预测。", + "midtermsHubThingsToWatchSubtitle": "选举周期推进过程中值得关注的指标。", + "midtermsHubConsequencesSubtitle": "预测的政策结果如何根据控制国会的政党而变化。", + "midtermsHubUpdatedRealtime": "实时更新", + "midtermsHubMetaculusUser": "Metaculus 用户", + "midtermsHubComingSoon": "即将推出", + "midtermsHubClickToView": "点击查看完整问题" } diff --git a/front_end/src/app/(main)/labor-hub/components/activity_card.tsx b/front_end/src/app/(main)/labor-hub/components/activity_card.tsx index 88345d7aab..caf917b1b8 100644 --- a/front_end/src/app/(main)/labor-hub/components/activity_card.tsx +++ b/front_end/src/app/(main)/labor-hub/components/activity_card.tsx @@ -39,7 +39,7 @@ export function ActivityCard({ subtitle?: string; children: React.ReactNode; degradeIndex?: number; - variant?: "purple" | "mint"; + variant?: "purple" | "mint" | "blue"; link?: string; highlighted?: boolean; className?: string; @@ -76,12 +76,17 @@ export function ActivityCard({ "border-purple-400 bg-purple-200 dark:border-purple-200-dark dark:bg-purple-100-dark", variant === "mint" && "border-mint-500 bg-mint-200 dark:border-mint-300-dark dark:bg-mint-200-dark", + variant === "blue" && + "border-blue-400 bg-blue-200 dark:border-blue-400-dark dark:bg-blue-200-dark", highlighted && variant === "purple" && "border-purple-700 ring-2 ring-purple-700 dark:border-purple-700-dark dark:ring-purple-700-dark", highlighted && variant === "mint" && "border-mint-700 ring-2 ring-mint-700 dark:border-mint-700-dark dark:ring-mint-700-dark", + highlighted && + variant === "blue" && + "border-blue-700 ring-2 ring-blue-700 dark:border-blue-700-dark dark:ring-blue-700-dark", isClickable && "cursor-pointer focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2", link && "pr-12", @@ -101,6 +106,8 @@ export function ActivityCard({ "text-purple-700 hover:text-purple-800 dark:text-purple-700-dark dark:hover:text-purple-800-dark", variant === "mint" && "text-mint-700 hover:text-mint-800 dark:text-mint-700-dark dark:hover:text-mint-800-dark", + variant === "blue" && + "text-blue-700 hover:text-blue-800 dark:text-blue-700-dark dark:hover:text-blue-800-dark", "absolute right-3.5 top-3 inline-flex size-5 items-center justify-center opacity-70 transition-opacity hover:opacity-100" )} > @@ -116,7 +123,8 @@ export function ActivityCard({ "size-3.5 shrink-0", variant === "purple" && "text-purple-700 dark:text-purple-700-dark", - variant === "mint" && "text-mint-700 dark:text-mint-700-dark" + variant === "mint" && "text-mint-700 dark:text-mint-700-dark", + variant === "blue" && "text-blue-700 dark:text-blue-700-dark" )} /> )} @@ -137,6 +145,7 @@ export function ActivityCard({ variant === "purple" && "text-purple-700 dark:text-purple-700-dark", variant === "mint" && "text-mint-700 dark:text-mint-700-dark", + variant === "blue" && "text-blue-700 dark:text-blue-700-dark", "flex min-w-0 flex-1 items-start justify-between gap-2 text-xs lg:text-sm" )} > @@ -157,6 +166,7 @@ export function ActivityCard({ className={cn( variant === "purple" && "text-purple-800 dark:text-purple-800-dark", variant === "mint" && "text-mint-800 dark:text-mint-800-dark", + variant === "blue" && "text-blue-800 dark:text-blue-800-dark", "text-xs leading-normal lg:text-sm" )} > diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx index 85155f02c2..e085a78f30 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx @@ -1,6 +1,10 @@ import { getTranslations } from "next-intl/server"; +import { PostWithForecasts } from "@/types/post"; +import cn from "@/utils/core/cn"; + import { MIDTERMS_COLORS } from "../constants"; +import CvBar from "./cv_bar"; import { ChamberData } from "../helpers/fetch_dashboard_data"; import { getMultipleChoiceOptionProbability } from "../helpers/post_utils"; @@ -49,6 +53,7 @@ export default async function ChamberControlCard({ data }: Props) { currentDem={CURRENT_SENATE.dem} currentRep={CURRENT_SENATE.rep} demNeededLabel={t("midtermsHubDemsNeed", { count: senateDemNeeded })} + sourcePost={data.senateControl} />
@@ -70,6 +76,7 @@ type RowProps = { currentDem: number; currentRep: number; demNeededLabel: string; + sourcePost: PostWithForecasts | null; }; function ChamberRow({ @@ -79,7 +86,10 @@ function ChamberRow({ currentDem, currentRep, demNeededLabel, + sourcePost, }: RowProps) { + // Normalize Dem+Rep so the two bars together represent ~100% (ignores + // the small "Other" slice from the underlying multiple-choice question). const total = demProb != null && repProb != null ? demProb + repProb : null; const demShare = demProb != null && total && total > 0 ? (demProb / total) * 100 : null; @@ -88,8 +98,10 @@ function ChamberRow({ const demPct = demProb != null ? Math.round(demProb * 1000) / 10 : null; const repPct = repProb != null ? Math.round(repProb * 1000) / 10 : null; - return ( -
+ const href = sourcePost ? `/questions/${sourcePost.id}` : undefined; + + const inner = ( + <>
{chamberLabel} @@ -104,29 +116,20 @@ function ChamberRow({ )}
-
+
{demShare != null && repShare != null && ( <> -
-
-
)} @@ -143,6 +146,25 @@ function ChamberRow({ {demNeededLabel}
-
+ + ); + + const groupClass = cn( + "group/cv block", + href && "cursor-pointer no-underline" ); + + if (href) { + return ( + + {inner} + + ); + } + return
{inner}
; } diff --git a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx index 676c135c22..b2ff6aa0ac 100644 --- a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx @@ -1,8 +1,10 @@ import { getTranslations } from "next-intl/server"; import { PostWithForecasts } from "@/types/post"; +import cn from "@/utils/core/cn"; import { MIDTERMS_COLORS } from "../constants"; +import CvBar from "./cv_bar"; import { getMultipleChoiceOptionProbability } from "../helpers/post_utils"; type OutcomeKey = "RR" | "RD" | "DR" | "DD"; @@ -17,8 +19,8 @@ const OUTCOME_OPTION_LABEL: Record = { type Outcome = { key: OutcomeKey; pct: number | null; + color: string; borderColor: string; - bgColor: string; }; type Props = { @@ -30,8 +32,8 @@ export default async function CongressOutcomeCard({ post }: Props) { const buildOutcome = ( key: OutcomeKey, - borderColor: string, - bgColor: string + color: string, + borderColor: string ): Outcome => { const prob = getMultipleChoiceOptionProbability( post, @@ -40,16 +42,16 @@ export default async function CongressOutcomeCard({ post }: Props) { return { key, pct: prob != null ? Math.round(prob * 1000) / 10 : null, + color, borderColor, - bgColor, }; }; const outcomes: Outcome[] = [ - buildOutcome("RR", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repLight), - buildOutcome("RD", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repLight), - buildOutcome("DR", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demLight), - buildOutcome("DD", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demLight), + buildOutcome("RR", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repBorder), + buildOutcome("RD", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repBorder), + buildOutcome("DR", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demBorder), + buildOutcome("DD", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demBorder), ]; const labels: Record = { @@ -59,34 +61,21 @@ export default async function CongressOutcomeCard({ post }: Props) { DD: t("midtermsHubOutcomeDemDem"), }; + const href = post ? `/questions/${post.id}` : undefined; + return (

{t("midtermsHubCongressForecast")}

-
+
{outcomes.map((o) => ( -
- - {labels[o.key]} - -
-
- - {o.pct != null ? `${o.pct.toFixed(1)}%` : "—"} - -
-
+ outcome={o} + label={labels[o.key]} + href={href} + /> ))}

@@ -95,3 +84,50 @@ export default async function CongressOutcomeCard({ post }: Props) {

); } + +function OutcomeRow({ + outcome, + label, + href, +}: { + outcome: Outcome; + label: string; + href?: string; +}) { + const inner = ( + <> + + {label} + +
+ + + {outcome.pct != null ? `${outcome.pct.toFixed(1)}%` : "—"} + +
+ + ); + + const className = cn( + "group/cv block rounded-sm", + href && "cursor-pointer no-underline" + ); + + if (href) { + return ( + + {inner} + + ); + } + return
{inner}
; +} diff --git a/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx b/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx index ac97e58b42..8b3669cc17 100644 --- a/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx @@ -2,6 +2,7 @@ import { getTranslations } from "next-intl/server"; import { MIDTERMS_COLORS } from "../constants"; import { ConsequenceRow as ConsequenceRowData } from "../data"; +import CvBar from "./cv_bar"; type Props = { row: ConsequenceRowData; @@ -23,20 +24,20 @@ export default async function ConsequenceRow({ row }: Props) { })(); return ( -
+

{question}

@@ -46,12 +47,12 @@ export default async function ConsequenceRow({ row }: Props) { function ConsequenceBar({ pct, color, - bg, + borderColor, mobileLabel, }: { pct: number; color: string; - bg: string; + borderColor: string; mobileLabel: string; }) { return ( @@ -60,14 +61,7 @@ function ConsequenceBar({ {mobileLabel}
-
+ {pct}% diff --git a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx new file mode 100644 index 0000000000..d92685a4b6 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { CSSProperties, FC } from "react"; + +import useAppTheme from "@/hooks/use_app_theme"; +import cn from "@/utils/core/cn"; +import { addOpacityToHex } from "@/utils/core/colors"; + +type Props = { + /** 0-100 — how much of the parent track this bar fills */ + pct: number; + /** Solid hex color (e.g. #6B7AE8). Drives the filled bg at 40% opacity. */ + color: string; + /** Border color override for light mode. Defaults to `color`. Use a + * darker shade for sharp contrast against the soft fill. */ + borderColor?: string; + /** Tailwind height class (default `h-5`). */ + heightClassName?: string; + className?: string; +}; + +const BG_OPACITY_DEFAULT = 0.4; +const BG_OPACITY_HOVER = 0.7; + +/** + * Visual primitive shaped like the consumer view multiple-choice bar: + * rounded, full-color border, semi-transparent fill in the same color. + * + * Hover styling is *driven by the nearest `group/cv` ancestor* — wrap + * the parent row in `className="group/cv"` so hovering anywhere in the + * row brightens every bar inside. + * + * In dark mode the darker `borderColor` would blend with the card bg, + * so we fall back to the primary `color` (which is a brighter shade) + * for the border instead. + */ +const CvBar: FC = ({ + pct, + color, + borderColor, + heightClassName = "h-5", + className, +}) => { + const { theme } = useAppTheme(); + const resolvedBorder = theme === "dark" ? color : borderColor ?? color; + + 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), + }; + + return ( +
+ ); +}; + +export default CvBar; diff --git a/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx b/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx index 58dfc8b5c9..3253a76ccb 100644 --- a/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx @@ -1,7 +1,7 @@ "use client"; import { geoAlbersUsa } from "d3-geo"; -import { FC, MouseEvent, ReactNode, useMemo, useState } from "react"; +import { FC, MouseEvent, ReactNode, useMemo, useRef, useState } from "react"; import { ComposableMap, Geographies, @@ -9,8 +9,11 @@ import { GeographyType, } from "react-simple-maps"; +import useAppTheme from "@/hooks/use_app_theme"; + import { MIDTERMS_COLORS } from "../constants"; import MapLegend from "./map_legend"; +import MapTooltipPortal from "./map_tooltip_portal"; import StateTooltipContent from "./state_tooltip"; import { getDemWinPct, SenateRaceWithQuestion } from "../helpers/post_utils"; import { getStateColor } from "../helpers/state_color"; @@ -73,7 +76,7 @@ const FIPS_TO_ABBR: Record = { type Props = { races: SenateRaceWithQuestion[]; - /** Tabs slot (rendered absolute top-left on md+) */ + /** Tabs slot (rendered absolute top-left). */ tabsSlot?: ReactNode; }; @@ -89,13 +92,31 @@ const MAP_VIEWBOX_HEIGHT = 540; const MAP_SCALE = 970; const MAP_TRANSLATE: [number, number] = [385, 290]; +// Light-mode default/hover opacities for uncontested states. +const UNCONTESTED_OPACITY_DEFAULT = 0.75; +const UNCONTESTED_OPACITY_HOVER = 1; + const GeographicMap: FC = ({ races, tabsSlot }) => { + const { theme } = useAppTheme(); + const isDark = theme === "dark"; + + const strokeColor = isDark + ? MIDTERMS_COLORS.cardBgDark + : MIDTERMS_COLORS.cardBgLight; + const uncontestedFill = isDark + ? MIDTERMS_COLORS.uncontestedDark + : MIDTERMS_COLORS.uncontestedLight; + const uncontestedHoverFill = isDark + ? MIDTERMS_COLORS.uncontestedHoverDark + : MIDTERMS_COLORS.uncontestedHoverLight; + const racesByState = useMemo( () => new Map(races.map((r) => [r.state, r])), [races] ); const [hovered, setHovered] = useState(null); + const containerRef = useRef(null); // react-simple-maps' MapProvider hardcodes translate to viewbox center, so // we provide a fully-configured projection function instead. @@ -106,13 +127,10 @@ const GeographicMap: FC = ({ races, tabsSlot }) => { const handleEnter = (abbr: string, e: MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); - const parent = e.currentTarget.closest(".geo-map-container"); - if (!parent) return; - const parentRect = parent.getBoundingClientRect(); setHovered({ abbr, - x: rect.left - parentRect.left + rect.width / 2, - y: rect.top - parentRect.top, + x: rect.left + rect.width / 2 + window.scrollX, + y: rect.top + window.scrollY, }); }; @@ -125,7 +143,10 @@ const GeographicMap: FC = ({ races, tabsSlot }) => { const hoveredRace = hovered ? racesByState.get(hovered.abbr) : null; return ( -
+
{tabsSlot && (
{tabsSlot} @@ -147,13 +168,15 @@ const GeographicMap: FC = ({ races, tabsSlot }) => { geographies.map((geo) => { const abbr = FIPS_TO_ABBR[String(geo.id ?? "")]; const race = abbr ? racesByState.get(abbr) : undefined; - const demWinPct = getDemWinPct(race?.question ?? null); - const fillColor = race - ? getStateColor(demWinPct) - : MIDTERMS_COLORS.notContested; const isContested = race !== undefined; const isHovered = hovered?.abbr === abbr; + const fillColor = isContested + ? getStateColor(getDemWinPct(race?.question ?? null)) + : isHovered + ? uncontestedHoverFill + : uncontestedFill; + return ( = ({ races, tabsSlot }) => { style={{ default: { fill: fillColor, - stroke: MIDTERMS_COLORS.stateStroke, + stroke: strokeColor, strokeWidth: 1.5, outline: "none", cursor: isContested ? "pointer" : "default", - opacity: isContested ? 1 : 0.5, + opacity: isContested ? 1 : UNCONTESTED_OPACITY_DEFAULT, transition: "fill 150ms ease-out, opacity 150ms ease-out, filter 150ms ease-out", filter: @@ -179,20 +202,20 @@ const GeographicMap: FC = ({ races, tabsSlot }) => { : undefined, }, hover: { - fill: fillColor, - stroke: MIDTERMS_COLORS.stateStroke, + fill: isContested ? fillColor : uncontestedHoverFill, + stroke: strokeColor, strokeWidth: isContested ? 2 : 1.5, outline: "none", cursor: isContested ? "pointer" : "default", - opacity: isContested ? 1 : 0.75, + opacity: isContested ? 1 : UNCONTESTED_OPACITY_HOVER, filter: isContested ? "brightness(0.9)" : undefined, }, pressed: { fill: fillColor, - stroke: MIDTERMS_COLORS.stateStroke, + stroke: strokeColor, strokeWidth: isContested ? 2 : 1.5, outline: "none", - opacity: isContested ? 1 : 0.75, + opacity: isContested ? 1 : UNCONTESTED_OPACITY_HOVER, }, }} /> @@ -204,19 +227,18 @@ const GeographicMap: FC = ({ races, tabsSlot }) => {
{hoveredRace && hovered && ( -
handleClick(hoveredRace)} + insideRef={containerRef} + onDismiss={() => setHovered(null)} > -
+ )}
); diff --git a/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx b/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx index 5152538ddb..d47b21c2e5 100644 --- a/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/insight_card.tsx @@ -19,7 +19,7 @@ const InsightCard: FC = ({ insight }) => { return (
= ({ className }) => { const t = useTranslations(); + const { theme } = useAppTheme(); + const uncontestedFill = + theme === "dark" + ? MIDTERMS_COLORS.uncontestedDark + : MIDTERMS_COLORS.uncontestedLight; + return (
{t("midtermsHubDemocrat")} {t("midtermsHubRepublican")} {t("midtermsHubNotContested")} diff --git a/front_end/src/app/(main)/midterms-2026/components/map_tooltip_portal.tsx b/front_end/src/app/(main)/midterms-2026/components/map_tooltip_portal.tsx new file mode 100644 index 0000000000..1efade5d0e --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/map_tooltip_portal.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { + FC, + ReactNode, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; +import { createPortal } from "react-dom"; + +import useMounted from "@/hooks/use_mounted"; + +type Props = { + /** Document-space x (viewport rect.left + width/2 + scrollX). */ + x: number; + /** Document-space y (viewport rect.top + scrollY). */ + y: number; + /** Optional click handler for the wrapper button. */ + onClick?: () => void; + /** Element used to detect "click outside" — taps inside this ref keep + * the tooltip open (e.g. the map container); the portal contents are + * also automatically excluded. */ + insideRef?: React.RefObject; + /** Called when the user taps anywhere outside the map and tooltip. */ + onDismiss?: () => void; + children: ReactNode; +}; + +const VIEWPORT_PADDING = 8; + +/** + * Renders the map tooltip into `document.body` via a portal so it escapes + * any `overflow-hidden` clipping on its ancestors (e.g. the SectionCard + * that clips the geographic map's zoomed paths). + */ +const MapTooltipPortal: FC = ({ + x, + y, + onClick, + insideRef, + onDismiss, + children, +}) => { + const mounted = useMounted(); + const tooltipRef = useRef(null); + const [adjustment, setAdjustment] = useState({ + leftOffset: 0, + placeBelow: false, + }); + + // After the tooltip is rendered, measure it and if it's clipped by the + // viewport edges, shift it horizontally and/or flip it below the anchor. + 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]); + + 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]); + + if (!mounted || typeof document === "undefined") return null; + + const transform = adjustment.placeBelow + ? `translate(calc(-50% + ${adjustment.leftOffset}px), 16px)` + : `translate(calc(-50% + ${adjustment.leftOffset}px), -100%)`; + const top = adjustment.placeBelow ? y + 32 : y - 8; + + return createPortal( + , + document.body + ); +}; + +export default MapTooltipPortal; diff --git a/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx b/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx index 51b1326d78..60fff00bdd 100644 --- a/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/responsive_map.tsx @@ -12,11 +12,16 @@ type Props = { const ResponsiveMap: FC = ({ races }) => { return ( <> -
+ {/* Geographic map shown only at lg+ where the map column has enough + room to render the country comfortably. Below lg the layout + collapses to a single column and the tile map takes over. */} +
} />
-
- +
+
+ +
); diff --git a/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx b/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx index 4ff61a5221..35840e7091 100644 --- a/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/tile_map.tsx @@ -1,12 +1,14 @@ "use client"; -import { FC, MouseEvent, useState } from "react"; +import { FC, MouseEvent, useRef, useState } from "react"; +import useAppTheme from "@/hooks/use_app_theme"; import cn from "@/utils/core/cn"; -import { STATE_NAMES } from "../constants"; +import { MIDTERMS_COLORS, STATE_NAMES } from "../constants"; import { US_TILE_GRID } from "../data"; import MapLegend from "./map_legend"; +import MapTooltipPortal from "./map_tooltip_portal"; import StateTooltipContent from "./state_tooltip"; import { getDemWinPct, SenateRaceWithQuestion } from "../helpers/post_utils"; import { getStateColor } from "../helpers/state_color"; @@ -17,6 +19,8 @@ type Props = { type HoverState = { abbr: string; + /** Document-space coordinates so the portalled tooltip can position + * outside the SectionCard's overflow-hidden boundary. */ x: number; y: number; } | null; @@ -24,32 +28,60 @@ type HoverState = { const MAX_COL = Math.max(...US_TILE_GRID.map((c) => c.col)); const MAX_ROW = Math.max(...US_TILE_GRID.map((c) => c.row)); +const UNCONTESTED_OPACITY_DEFAULT = 0.75; + const TileMap: FC = ({ races }) => { + const { theme } = useAppTheme(); + const isDark = theme === "dark"; + const uncontestedFill = isDark + ? MIDTERMS_COLORS.uncontestedDark + : MIDTERMS_COLORS.uncontestedLight; + const [hovered, setHovered] = useState(null); + const containerRef = useRef(null); const racesByState = new Map(races.map((r) => [r.state, r])); - const handleEnter = (abbr: string, e: MouseEvent) => { + const showTooltipFor = (abbr: string, e: MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect(); - const parent = e.currentTarget.closest(".tile-map-container"); - if (!parent) return; - const parentRect = parent.getBoundingClientRect(); setHovered({ abbr, - x: rect.left - parentRect.left + rect.width / 2, - y: rect.top - parentRect.top, + x: rect.left + rect.width / 2 + window.scrollX, + y: rect.top + window.scrollY, }); }; - const handleClick = (race: SenateRaceWithQuestion | undefined) => { + const navigate = (race: SenateRaceWithQuestion | undefined) => { if (!race?.parentPost || !race.question) return; const url = `/questions/${race.parentPost.id}/?sub-question=${race.question.id}`; window.open(url, "_blank", "noopener,noreferrer"); }; + const handleTileClick = ( + abbr: string, + race: SenateRaceWithQuestion | undefined, + e: MouseEvent + ) => { + if (!race) return; + // On touch devices, first tap reveals the tooltip; navigation happens + // when the user taps the tooltip itself. + if ( + typeof window !== "undefined" && + window.matchMedia("(hover: none)").matches + ) { + if (hovered?.abbr === abbr) { + setHovered(null); + } else { + showTooltipFor(abbr, e); + } + return; + } + navigate(race); + }; + const hoveredRace = hovered ? racesByState.get(hovered.abbr) : null; return ( -
+
= ({ races }) => { > {US_TILE_GRID.map(({ abbr, row, col }) => { const race = racesByState.get(abbr); - const demWinPct = getDemWinPct(race?.question ?? null); - const fillColor = getStateColor(demWinPct); const isContested = race !== undefined; + const fillColor = isContested + ? getStateColor(getDemWinPct(race.question)) + : uncontestedFill; return (
{hoveredRace && hovered && ( -
navigate(hoveredRace)} + insideRef={containerRef} + onDismiss={() => setHovered(null)} > -
+ )} - +
); }; diff --git a/front_end/src/app/(main)/midterms-2026/constants.ts b/front_end/src/app/(main)/midterms-2026/constants.ts index 9e84b93bf9..19a1ebda2d 100644 --- a/front_end/src/app/(main)/midterms-2026/constants.ts +++ b/front_end/src/app/(main)/midterms-2026/constants.ts @@ -1,19 +1,27 @@ export const MIDTERMS_COLORS = { demPrimary: "#6B7AE8", - demLight: "#CECBF6", + // Slightly darker accents, used for borders / strokes so they read + // sharply against the semi-transparent fills. + demBorder: "#3A4DD0", repPrimary: "#E8827A", - repLight: "#FCCAC7", - likelyD: "#4A5CD4", - leanD: "#8B97EE", - tossUp: "#D3D1C7", - leanR: "#EDA8A4", - likelyR: "#D4504A", - // Matches the page body background (blue-200 in light mode) so uncontested - // states recede into the page; contested states pop above them. - notContested: "#eff4f4", - notContestedDark: "#2f4155", - stateStroke: "#ffffff", - stateStrokeDark: "#22262b", + repBorder: "#C53B33", + // Continuous-gradient anchors. `tossUp` is the neutral midpoint at 50%, + // `likelyR` and `likelyD` are the extremes; intermediate colors are + // generated via getColorInSpectrum. + spectrumDem: "#4A5CD4", + spectrumNeutral: "#D3D1C7", + spectrumRep: "#D4504A", + // Theme-aware tokens for state stroke + uncontested fill. + // Stroke matches the SectionCard background color so state borders blend + // with the surrounding card. + cardBgLight: "#ffffff", + cardBgDark: "#262f38", + // Uncontested fill is one elevation step lighter than the card bg so + // uncontested states are subtly visible without dominating. + uncontestedLight: "#eff4f4", + uncontestedDark: "#2d3845", + uncontestedHoverLight: "#e0e9eb", + uncontestedHoverDark: "#394450", } as const; export const STATE_NAMES: Record = { diff --git a/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts b/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts index 67309cc6ac..bebecf328f 100644 --- a/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts +++ b/front_end/src/app/(main)/midterms-2026/helpers/state_color.ts @@ -1,24 +1,25 @@ -import { MIDTERMS_COLORS } from "../constants"; +import { getColorInSpectrum, parseHexColor } from "@/utils/core/colors"; -export type StateTier = - | "likelyD" - | "leanD" - | "tossUp" - | "leanR" - | "likelyR" - | "notContested"; +import { MIDTERMS_COLORS } from "../constants"; -// Toss-up is a tight 48-52% band; everything outside leans one way or the -// other. Likely is reserved for >65 / <35 (clear blowouts). -export function getStateTier(demWinPct: number | null | undefined): StateTier { - if (demWinPct == null) return "notContested"; - if (demWinPct > 65) return "likelyD"; - if (demWinPct > 52) return "leanD"; - if (demWinPct >= 48) return "tossUp"; - if (demWinPct >= 35) return "leanR"; - return "likelyR"; -} +const RED_RGB = parseHexColor(MIDTERMS_COLORS.spectrumRep) ?? [ + 0xd4, 0x50, 0x4a, +]; +const NEUTRAL_RGB = parseHexColor(MIDTERMS_COLORS.spectrumNeutral) ?? [ + 0xd3, 0xd1, 0xc7, +]; +const BLUE_RGB = parseHexColor(MIDTERMS_COLORS.spectrumDem) ?? [ + 0x4a, 0x5c, 0xd4, +]; +/** + * Returns a continuous color along a Republican → neutral → Democrat gradient + * given the Democrat win probability (0-100). Smooth interpolation produces + * perceptible differences between adjacent forecasts (e.g. 37% vs 45%) which + * a tier-based bucketing cannot. + */ export function getStateColor(demWinPct: number | null | undefined): string { - return MIDTERMS_COLORS[getStateTier(demWinPct)]; + if (demWinPct == null) return MIDTERMS_COLORS.uncontestedLight; + const value = Math.max(0, Math.min(1, demWinPct / 100)); + return getColorInSpectrum(RED_RGB, NEUTRAL_RGB, BLUE_RGB, value); } diff --git a/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx b/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx index c668a8ef44..29ebb2ab62 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/community_insights.tsx @@ -6,11 +6,25 @@ import { } from "@/app/(main)/labor-hub/components/section"; import InsightsCarousel from "../components/insights_carousel"; -import { fetchCommunityInsights } from "../helpers/fetch_community_insights"; +import { + CommunityInsight, + fetchCommunityInsights, +} from "../helpers/fetch_community_insights"; export default async function CommunityInsightsSection() { const t = await getTranslations(); - const insights = await fetchCommunityInsights(); + + // The dashboard should keep rendering even if the insights fetch errors + // (e.g. comments API hiccup) — treat any failure as a benign empty state. + let insights: CommunityInsight[] = []; + try { + const fetched = await fetchCommunityInsights(); + if (Array.isArray(fetched)) { + insights = fetched; + } + } catch { + insights = []; + } if (!insights.length) return null; diff --git a/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx b/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx index 18dff401c3..6240ea7d54 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/elections_map_section.tsx @@ -16,15 +16,15 @@ export default async function ElectionsMapSection() { return ( -
- {/* Map column: edge-to-edge of the SectionCard on lg+. */} +
+ {/* Map column: edge-to-edge of the SectionCard on md+. */}
- {/* Sidebar column: provides its own padding so the cards stay inset - from the white card edges. lg:p-10 keeps the same visual padding - the SectionCard used to provide. */} -
+ {/* Sidebar column: capped at 30% of the container with its own + internal padding so the cards stay inset from the white card + edges. */} +
diff --git a/front_end/src/app/(main)/midterms-2026/sections/footer.tsx b/front_end/src/app/(main)/midterms-2026/sections/footer.tsx index 72ec6fcc09..ee55e750fc 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/footer.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/footer.tsx @@ -1,4 +1,4 @@ -import { format } from "date-fns"; +import { formatInTimeZone } from "date-fns-tz"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; @@ -34,7 +34,7 @@ export default async function FooterSection() { {latest && (

{t("midtermsHubLastUpdatedFull", { - date: format(latest, "MMMM d, yyyy, HH:mm 'UTC'"), + date: formatInTimeZone(latest, "UTC", "MMMM d, yyyy, HH:mm 'UTC'"), })}

)} diff --git a/front_end/src/app/(main)/midterms-2026/sections/hero.tsx b/front_end/src/app/(main)/midterms-2026/sections/hero.tsx index 69145b47c8..7a804e4a26 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/hero.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/hero.tsx @@ -6,18 +6,18 @@ export default async function HeroSection() { const t = await getTranslations(); return ( -
+

{t("midtermsHubHeroTitleLine1")}{" "} {t("midtermsHubHeroTitleLine2")}

-
-

+

+

{t("midtermsHubHeroSubtitle")}

-
+
diff --git a/front_end/src/app/og/midterms-2026/route/route.ts b/front_end/src/app/og/midterms-2026/route/route.ts index d24cff8080..090fc1a259 100644 --- a/front_end/src/app/og/midterms-2026/route/route.ts +++ b/front_end/src/app/og/midterms-2026/route/route.ts @@ -7,10 +7,15 @@ export async function GET(req: NextRequest) { const { PUBLIC_APP_URL } = getPublicSettings(); const pageUrl = `${PUBLIC_APP_URL}/og/midterms-2026?theme=${theme}&non-interactive=true`; - const screenshotEndpoint = new URL( - "/api/screenshot/", - process.env.SCREENSHOT_SERVICE_API_URL - ).toString(); + const screenshotServiceUrl = process.env.SCREENSHOT_SERVICE_API_URL; + const screenshotApiKey = process.env.SCREENSHOT_SERVICE_API_KEY; + + if (!screenshotServiceUrl) { + return NextResponse.json( + { error: "screenshot service is not configured" }, + { status: 503 } + ); + } const payload = { url: pageUrl, @@ -21,12 +26,21 @@ export async function GET(req: NextRequest) { }; try { + const screenshotEndpoint = new URL( + "/api/screenshot/", + screenshotServiceUrl + ).toString(); + + const headers: Record = { + "Content-Type": "application/json", + }; + if (screenshotApiKey) { + headers.api_key = screenshotApiKey; + } + const r = await fetch(screenshotEndpoint, { method: "POST", - headers: { - "Content-Type": "application/json", - api_key: process.env.SCREENSHOT_SERVICE_API_KEY || "", - }, + headers, body: JSON.stringify(payload), }); From 4cd9e95ad39cf0ccbbcfba2ae49d7c62dd1ad812 Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 14 May 2026 09:59:02 +0200 Subject: [PATCH 04/18] fix: address PR feedback on midterms hub - og route: add 15s AbortController timeout; return generic 502 on upstream failure to avoid leaking screenshot service status codes; surface AbortError as 504. - map tooltip portal: viewport-local placeBelow check (was mixing rect.top with window.scrollY); switch outside-click listener from click to mousedown to avoid racing the opener; expose onHoverChange so parents can keep the tooltip alive while it is hovered. - geographic map: add keyboard accessibility to contested states (tabIndex/role/onKeyDown/onFocus/onBlur) and opt uncontested states out of the tab order; defer the path's onMouseLeave via requestAnimationFrame so a pointer transition into the tooltip portal no longer unmounts it before the tooltip's onClick can fire. - locale files: replace the hardcoded forecast-outcome summary in midtermsHubCongressSummary with neutral copy across en/es/cs/pt/zh/ zh-TW so the sentence does not go stale. Co-Authored-By: Claude Opus 4.7 (1M context) --- front_end/messages/cs.json | 2 +- front_end/messages/en.json | 2 +- front_end/messages/es.json | 2 +- front_end/messages/pt.json | 2 +- front_end/messages/zh-TW.json | 2 +- front_end/messages/zh.json | 2 +- .../components/geographic_map.tsx | 70 +++++++++++++++++-- .../components/map_tooltip_portal.tsx | 18 ++++- .../src/app/og/midterms-2026/route/route.ts | 43 ++++++++---- 9 files changed, 113 insertions(+), 30 deletions(-) diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index 5e976df5a5..940c8bc2d4 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -2214,7 +2214,7 @@ "midtermsHubOutcomeRepDem": "Senát Rep / Sněmovna Dem", "midtermsHubOutcomeDemRep": "Senát Dem / Sněmovna Rep", "midtermsHubOutcomeDemDem": "Senát Dem / Sněmovna Dem", - "midtermsHubCongressSummary": "Předpovídači očekávají rozdělený Kongres – republikáni si pravděpodobně udrží Senát, zatímco demokraté jsou favority na získání Sněmovny reprezentantů.", + "midtermsHubCongressSummary": "Aktuální názory předpovídačů na kontrolu Kongresu jsou zachyceny v pravděpodobnostech uvedených výše.", "midtermsHubThingsToWatch": "Co sledovat", "midtermsHubVoterTurnout": "Předpověď volební účasti", "midtermsHubTurnoutContext": "Vyšší účast u mezičasových voleb historicky souvisela se silnějšími výsledky demokratů.", diff --git a/front_end/messages/en.json b/front_end/messages/en.json index df802058d9..f03a91b25b 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2203,7 +2203,7 @@ "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", "midtermsHubOutcomeDemDem": "Dem Senate / Dem House", - "midtermsHubCongressSummary": "Forecasters expect a split Congress, with Republicans likely to hold the Senate and Democrats favored to retake the House.", + "midtermsHubCongressSummary": "Forecasters' current views on Congress control are reflected in the probabilities above.", "midtermsHubThingsToWatch": "Things to Watch", "midtermsHubVoterTurnout": "Voter turnout forecast", "midtermsHubTurnoutContext": "Higher turnout in midterms has historically correlated with stronger Democratic performance.", diff --git a/front_end/messages/es.json b/front_end/messages/es.json index d606ded263..4365e98a3c 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -2214,7 +2214,7 @@ "midtermsHubOutcomeRepDem": "Senado Rep / Cámara Dem", "midtermsHubOutcomeDemRep": "Senado Dem / Cámara Rep", "midtermsHubOutcomeDemDem": "Senado Dem / Cámara Dem", - "midtermsHubCongressSummary": "Los pronosticadores esperan un Congreso dividido, con los republicanos probablemente manteniendo el Senado y los demócratas favorecidos para recuperar la Cámara.", + "midtermsHubCongressSummary": "Las opiniones actuales de los pronosticadores sobre el control del Congreso se reflejan en las probabilidades anteriores.", "midtermsHubThingsToWatch": "Aspectos a observar", "midtermsHubVoterTurnout": "Pronóstico de participación electoral", "midtermsHubTurnoutContext": "Una mayor participación en las elecciones intermedias ha estado históricamente correlacionada con un mejor desempeño demócrata.", diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index 6e96843807..13ce832c3d 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -2212,7 +2212,7 @@ "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.", + "midtermsHubCongressSummary": "As opiniões atuais dos previsores sobre o controle do Congresso estão refletidas nas probabilidades acima.", "midtermsHubThingsToWatch": "O que observar", "midtermsHubVoterTurnout": "Previsão de participação dos eleitores", "midtermsHubTurnoutContext": "Uma maior participação nas eleições de meio de mandato historicamente esteve correlacionada com um melhor desempenho democrata.", diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index 99942aaf22..adaaa2d85a 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -2211,7 +2211,7 @@ "midtermsHubOutcomeRepDem": "共和黨參議院 / 民主黨眾議院", "midtermsHubOutcomeDemRep": "民主黨參議院 / 共和黨眾議院", "midtermsHubOutcomeDemDem": "民主黨參議院 / 民主黨眾議院", - "midtermsHubCongressSummary": "預測者預期國會將分裂——共和黨可能掌握參議院,民主黨則有望奪回眾議院。", + "midtermsHubCongressSummary": "預測者目前對國會控制權的看法反映在上方的機率中。", "midtermsHubThingsToWatch": "值得關注的事項", "midtermsHubVoterTurnout": "選民投票率預測", "midtermsHubTurnoutContext": "期中選舉的高投票率歷來與民主黨更好的表現相關。", diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index d081826572..5513a7a246 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -2216,7 +2216,7 @@ "midtermsHubOutcomeRepDem": "共和党参议院 / 民主党众议院", "midtermsHubOutcomeDemRep": "民主党参议院 / 共和党众议院", "midtermsHubOutcomeDemDem": "民主党参议院 / 民主党众议院", - "midtermsHubCongressSummary": "预测者预计国会将分裂——共和党可能控制参议院,民主党有望夺回众议院。", + "midtermsHubCongressSummary": "预测者当前对国会控制权的看法反映在上述概率中。", "midtermsHubThingsToWatch": "值得关注", "midtermsHubVoterTurnout": "选民投票率预测", "midtermsHubTurnoutContext": "中期选举的高投票率历来与民主党的更好表现相关。", diff --git a/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx b/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx index 3253a76ccb..e9fa1708a9 100644 --- a/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx @@ -1,7 +1,17 @@ "use client"; import { geoAlbersUsa } from "d3-geo"; -import { FC, MouseEvent, ReactNode, useMemo, useRef, useState } from "react"; +import { + FC, + FocusEvent, + KeyboardEvent, + MouseEvent, + ReactNode, + useCallback, + useMemo, + useRef, + useState, +} from "react"; import { ComposableMap, Geographies, @@ -117,6 +127,10 @@ const GeographicMap: FC = ({ races, tabsSlot }) => { const [hovered, setHovered] = useState(null); const containerRef = useRef(null); + // True while the pointer is over the tooltip portal; the SVG path's + // onMouseLeave defers to this so the tooltip stays mounted long enough + // for its own onClick to fire. + const tooltipHoveredRef = useRef(false); // react-simple-maps' MapProvider hardcodes translate to viewbox center, so // we provide a fully-configured projection function instead. @@ -125,7 +139,10 @@ const GeographicMap: FC = ({ races, tabsSlot }) => { [] ); - const handleEnter = (abbr: string, e: MouseEvent) => { + const handleEnter = ( + abbr: string, + e: MouseEvent | FocusEvent + ) => { const rect = e.currentTarget.getBoundingClientRect(); setHovered({ abbr, @@ -140,6 +157,29 @@ const GeographicMap: FC = ({ races, tabsSlot }) => { window.open(url, "_blank", "noopener,noreferrer"); }; + const handleLeave = useCallback(() => { + // Defer clearing so a pointer transition into the tooltip portal has a + // chance to flip tooltipHoveredRef before we unmount it. + requestAnimationFrame(() => { + if (!tooltipHoveredRef.current) setHovered(null); + }); + }, []); + + const handleTooltipHoverChange = useCallback((hovering: boolean) => { + tooltipHoveredRef.current = hovering; + if (!hovering) setHovered(null); + }, []); + + const handleKeyDown = ( + e: KeyboardEvent, + race: SenateRaceWithQuestion + ) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleClick(race); + } + }; + const hoveredRace = hovered ? racesByState.get(hovered.abbr) : null; return ( @@ -177,15 +217,30 @@ const GeographicMap: FC = ({ races, tabsSlot }) => { ? uncontestedHoverFill : uncontestedFill; + // react-simple-maps' hardcodes tabIndex="0" on + // every path. For uncontested states we override with -1 so + // they don't enter the tab order, since they have no action. + const interactiveProps = isContested + ? { + tabIndex: 0, + role: "button", + onMouseEnter: (e: MouseEvent) => + abbr && handleEnter(abbr, e), + onMouseLeave: handleLeave, + onFocus: (e: FocusEvent) => + abbr && handleEnter(abbr, e), + onBlur: () => setHovered(null), + onKeyDown: (e: KeyboardEvent) => + race && handleKeyDown(e, race), + onClick: () => handleClick(race), + } + : { tabIndex: -1 }; + return ( - isContested && abbr && handleEnter(abbr, e) - } - onMouseLeave={() => setHovered(null)} - onClick={() => isContested && handleClick(race)} + {...interactiveProps} style={{ default: { fill: fillColor, @@ -233,6 +288,7 @@ const GeographicMap: FC = ({ races, tabsSlot }) => { onClick={() => handleClick(hoveredRace)} insideRef={containerRef} onDismiss={() => setHovered(null)} + onHoverChange={handleTooltipHoverChange} > ; /** Called when the user taps anywhere outside the map and tooltip. */ onDismiss?: () => void; + /** Called when the pointer enters/leaves the portal. Parents use this to + * keep the tooltip alive while the pointer hovers it (the SVG path's + * onMouseLeave would otherwise unmount the tooltip before its onClick + * could fire). */ + onHoverChange?: (hovering: boolean) => void; children: ReactNode; }; @@ -41,6 +46,7 @@ const MapTooltipPortal: FC = ({ onClick, insideRef, onDismiss, + onHoverChange, children, }) => { const mounted = useMounted(); @@ -62,7 +68,9 @@ const MapTooltipPortal: FC = ({ let leftOffset = 0; if (overflowRight > 0) leftOffset = -overflowRight; if (overflowLeft > 0) leftOffset = overflowLeft; - const placeBelow = rect.top - VIEWPORT_PADDING < window.scrollY; + // Viewport-local check: rect.top is already in viewport coordinates, so + // comparing it against window.scrollY would mix coordinate spaces. + const placeBelow = rect.top < VIEWPORT_PADDING; setAdjustment({ leftOffset, placeBelow }); }, [x, y, mounted]); @@ -75,8 +83,10 @@ const MapTooltipPortal: FC = ({ if (tooltipRef.current?.contains(target)) return; onDismiss(); }; - document.addEventListener("click", handle); - return () => document.removeEventListener("click", handle); + // mousedown fires before the opener's click, avoiding a race where a + // fresh click that opens the tooltip is also treated as an outside click. + document.addEventListener("mousedown", handle); + return () => document.removeEventListener("mousedown", handle); }, [onDismiss, insideRef]); if (!mounted || typeof document === "undefined") return null; @@ -91,6 +101,8 @@ const MapTooltipPortal: FC = ({ ref={tooltipRef} type="button" onClick={onClick} + onMouseEnter={() => onHoverChange?.(true)} + onMouseLeave={() => onHoverChange?.(false)} className="absolute z-[60] cursor-pointer" style={{ left: x, diff --git a/front_end/src/app/og/midterms-2026/route/route.ts b/front_end/src/app/og/midterms-2026/route/route.ts index 090fc1a259..85bc702222 100644 --- a/front_end/src/app/og/midterms-2026/route/route.ts +++ b/front_end/src/app/og/midterms-2026/route/route.ts @@ -25,28 +25,35 @@ export async function GET(req: NextRequest) { height: 630, }; - try { - const screenshotEndpoint = new URL( - "/api/screenshot/", - screenshotServiceUrl - ).toString(); + const screenshotEndpoint = new URL( + "/api/screenshot/", + screenshotServiceUrl + ).toString(); - const headers: Record = { - "Content-Type": "application/json", - }; - if (screenshotApiKey) { - headers.api_key = screenshotApiKey; - } + const headers: Record = { + "Content-Type": "application/json", + }; + if (screenshotApiKey) { + headers.api_key = screenshotApiKey; + } + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 15_000); + + try { const r = await fetch(screenshotEndpoint, { method: "POST", headers, body: JSON.stringify(payload), + signal: controller.signal, }); if (!r.ok) { - const text = await r.text(); - return NextResponse.json({ error: text }, { status: r.status }); + // Do not leak upstream screenshot service status codes/bodies. + return NextResponse.json( + { error: "Upstream service error" }, + { status: 502 } + ); } const buf = await r.arrayBuffer(); @@ -60,7 +67,15 @@ export async function GET(req: NextRequest) { : "no-store", }, }); - } catch { + } catch (err) { + if (err instanceof Error && err.name === "AbortError") { + return NextResponse.json( + { error: "screenshot request timed out" }, + { status: 504 } + ); + } return NextResponse.json({ error: "screenshot failed" }, { status: 500 }); + } finally { + clearTimeout(timeoutId); } } From 5263940c25094e1553296d78b5898bbba5026655 Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 14 May 2026 11:00:45 +0200 Subject: [PATCH 05/18] feat: round-2 midterms hub design refinements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - map: re-center projection and drop the right-side crop so the full US is visible now that more states will be contested. - chamber tabs: remove the House tab (kept Senate active, Governor disabled). - chamber control card: bump bars to h-5 (match Congress Outcome); reorder seat totals to Dems first ("Current: D 47 — R 53"); prefix the percentages with "Forecast:"; drop the inline "Dems need +N" text. The seats-needed line now lives in a hover tooltip below the row with a "Click to view forecasting question" disclaimer; the tooltip subject auto-picks the trailing party. - congress outcome card: render the split-control rows (Rep Senate / Dem House and Dem Senate / Rep House) in neutral purple so they no longer read as either party. - electoral consequences: add inline donkey + elephant SVG icons in the "IF DEM / IF REP CONGRESS" header cells so the fills tint to the party color via currentColor. - cv bar: theme-aware opacity (bumped in dark mode for contrast); remove the 1px→2px border thickening on hover, keep the color shift. - footer: replace the mock "Last updated UTC" line with a static "Forecasts are updated real-time." copy; drop the now-unused getLatestUpdateTime helper and the LastUpdatedFull / DemsNeed i18n keys. - i18n: add ChamberCurrent / ChamberForecast / ChamberTooltipBody / ChamberTooltipDisclaimer / PartyDemocrats / PartyRepublicans / ForecastsRealtime keys across en/es/cs/pt/zh/zh-TW. Co-Authored-By: Claude Opus 4.7 (1M context) --- front_end/messages/cs.json | 9 +- front_end/messages/en.json | 9 +- front_end/messages/es.json | 9 +- front_end/messages/pt.json | 9 +- front_end/messages/zh-TW.json | 9 +- front_end/messages/zh.json | 9 +- front_end/public/images/party-donkey.svg | 3 + front_end/public/images/party-elephant.svg | 3 + .../components/chamber_control_card.tsx | 159 ++++++++++++++---- .../components/chamber_row_tooltip.tsx | 32 ++++ .../midterms-2026/components/chamber_tabs.tsx | 1 - .../components/congress_outcome_card.tsx | 12 +- .../midterms-2026/components/cv_bar.tsx | 22 ++- .../components/geographic_map.tsx | 10 +- .../midterms-2026/components/party_icons.tsx | 31 ++++ .../src/app/(main)/midterms-2026/constants.ts | 4 + .../midterms-2026/helpers/post_utils.ts | 16 -- .../sections/electoral_consequences.tsx | 18 +- .../(main)/midterms-2026/sections/footer.tsx | 26 +-- 19 files changed, 286 insertions(+), 105 deletions(-) create mode 100644 front_end/public/images/party-donkey.svg create mode 100644 front_end/public/images/party-elephant.svg create mode 100644 front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx create mode 100644 front_end/src/app/(main)/midterms-2026/components/party_icons.tsx diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index 940c8bc2d4..284577310d 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -2203,13 +2203,18 @@ "thousandsOfOpenQuestions": "20 000+ otevřených otázek", "midtermsHubMetaTitle": "Mezičasové volby USA 2026 | Metaculus", "midtermsHubMetaDescription": "Předpovědi komunity Metaculus v reálném čase k mezičasovým volbám USA v roce 2026.", - "midtermsHubLastUpdatedFull": "Naposledy aktualizováno {date}", + "midtermsHubChamberCurrent": "Aktuálně:", + "midtermsHubChamberForecast": "Předpověď:", + "midtermsHubChamberTooltipBody": "{party} potřebují o {count} mandátů více pro získání {chamber}; předpovídači tomu dávají {pct}% šanci.", + "midtermsHubChamberTooltipDisclaimer": "Klikněte pro zobrazení předpovědní otázky", + "midtermsHubPartyDemocrats": "Demokraté", + "midtermsHubPartyRepublicans": "Republikáni", + "midtermsHubForecastsRealtime": "Předpovědi se aktualizují v reálném čase.", "midtermsHubChamberSenate": "Senát", "midtermsHubChamberHouse": "Sněmovna reprezentantů", "midtermsHubChamberGovernor": "Guvernér", "midtermsHubChamberControl": "Kontrola komory", "midtermsHubCongressForecast": "Kontrola Kongresu", - "midtermsHubDemsNeed": "Demokraté potřebují +{count}", "midtermsHubOutcomeRepRep": "Senát Rep / Sněmovna Rep", "midtermsHubOutcomeRepDem": "Senát Rep / Sněmovna Dem", "midtermsHubOutcomeDemRep": "Senát Dem / Sněmovna Rep", diff --git a/front_end/messages/en.json b/front_end/messages/en.json index f03a91b25b..a6cca8ea67 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2192,13 +2192,18 @@ "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)", "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", - "midtermsHubLastUpdatedFull": "Last updated {date}", "midtermsHubChamberSenate": "Senate", "midtermsHubChamberHouse": "House", "midtermsHubChamberGovernor": "Governor", "midtermsHubChamberControl": "Chamber control", "midtermsHubCongressForecast": "Congress Control", - "midtermsHubDemsNeed": "Dems need +{count}", + "midtermsHubChamberCurrent": "Current:", + "midtermsHubChamberForecast": "Forecast:", + "midtermsHubChamberTooltipBody": "{party} need {count} more seats to secure the {chamber}; forecasters give this a {pct}% chance.", + "midtermsHubChamberTooltipDisclaimer": "Click to view forecasting question", + "midtermsHubPartyDemocrats": "Democrats", + "midtermsHubPartyRepublicans": "Republicans", + "midtermsHubForecastsRealtime": "Forecasts are updated real-time.", "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", diff --git a/front_end/messages/es.json b/front_end/messages/es.json index 4365e98a3c..b18d19ae1b 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -2203,13 +2203,18 @@ "thousandsOfOpenQuestions": "20,000+ preguntas abiertas", "midtermsHubMetaTitle": "Elecciones intermedias de EE. UU. de 2026 | Metaculus", "midtermsHubMetaDescription": "Pronósticos en tiempo real de la comunidad de Metaculus sobre las elecciones intermedias de EE. UU. de 2026.", - "midtermsHubLastUpdatedFull": "Última actualización {date}", + "midtermsHubChamberCurrent": "Actual:", + "midtermsHubChamberForecast": "Pronóstico:", + "midtermsHubChamberTooltipBody": "{party} necesitan {count} escaños más para asegurar el {chamber}; los pronosticadores le dan a esto un {pct}% de probabilidad.", + "midtermsHubChamberTooltipDisclaimer": "Haz clic para ver la pregunta de pronóstico", + "midtermsHubPartyDemocrats": "Los demócratas", + "midtermsHubPartyRepublicans": "Los republicanos", + "midtermsHubForecastsRealtime": "Los pronósticos se actualizan en tiempo real.", "midtermsHubChamberSenate": "Senado", "midtermsHubChamberHouse": "Cámara de Representantes", "midtermsHubChamberGovernor": "Gobernador", "midtermsHubChamberControl": "Control de cámara", "midtermsHubCongressForecast": "Control del Congreso", - "midtermsHubDemsNeed": "Los demócratas necesitan +{count}", "midtermsHubOutcomeRepRep": "Senado Rep / Cámara Rep", "midtermsHubOutcomeRepDem": "Senado Rep / Cámara Dem", "midtermsHubOutcomeDemRep": "Senado Dem / Cámara Rep", diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index 13ce832c3d..25718dc82c 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -2201,13 +2201,18 @@ "thousandsOfOpenQuestions": "20.000+ perguntas abertas", "midtermsHubMetaTitle": "Eleições de meio de mandato dos EUA 2026 | Metaculus", "midtermsHubMetaDescription": "Previsões em tempo real da comunidade Metaculus sobre as eleições de meio de mandato dos EUA de 2026.", - "midtermsHubLastUpdatedFull": "Última atualização {date}", + "midtermsHubChamberCurrent": "Atual:", + "midtermsHubChamberForecast": "Previsão:", + "midtermsHubChamberTooltipBody": "{party} precisam de mais {count} cadeiras para garantir o {chamber}; os previsores dão a isso uma chance de {pct}%.", + "midtermsHubChamberTooltipDisclaimer": "Clique para ver a pergunta de previsão", + "midtermsHubPartyDemocrats": "Os democratas", + "midtermsHubPartyRepublicans": "Os republicanos", + "midtermsHubForecastsRealtime": "As previsões são atualizadas em tempo real.", "midtermsHubChamberSenate": "Senado", "midtermsHubChamberHouse": "Câmara dos Representantes", "midtermsHubChamberGovernor": "Governador", "midtermsHubChamberControl": "Controle da câmara", "midtermsHubCongressForecast": "Controle do Congresso", - "midtermsHubDemsNeed": "Os democratas precisam de +{count}", "midtermsHubOutcomeRepRep": "Senado Rep / Câmara Rep", "midtermsHubOutcomeRepDem": "Senado Rep / Câmara Dem", "midtermsHubOutcomeDemRep": "Senado Dem / Câmara Rep", diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index adaaa2d85a..4dc429f663 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -2200,13 +2200,18 @@ "thousandsOfOpenQuestions": "20,000+ 開放問題", "midtermsHubMetaTitle": "2026 年美國期中選舉 | Metaculus", "midtermsHubMetaDescription": "Metaculus 社群對 2026 年美國期中選舉的即時預測。", - "midtermsHubLastUpdatedFull": "最後更新 {date}", + "midtermsHubChamberCurrent": "目前:", + "midtermsHubChamberForecast": "預測:", + "midtermsHubChamberTooltipBody": "{party}還需要 {count} 個席位才能掌控{chamber};預測者認為這一機率為 {pct}%。", + "midtermsHubChamberTooltipDisclaimer": "點擊查看預測問題", + "midtermsHubPartyDemocrats": "民主黨", + "midtermsHubPartyRepublicans": "共和黨", + "midtermsHubForecastsRealtime": "預測即時更新。", "midtermsHubChamberSenate": "參議院", "midtermsHubChamberHouse": "眾議院", "midtermsHubChamberGovernor": "州長", "midtermsHubChamberControl": "議院控制權", "midtermsHubCongressForecast": "國會控制權", - "midtermsHubDemsNeed": "民主黨需要 +{count} 席", "midtermsHubOutcomeRepRep": "共和黨參議院 / 共和黨眾議院", "midtermsHubOutcomeRepDem": "共和黨參議院 / 民主黨眾議院", "midtermsHubOutcomeDemRep": "民主黨參議院 / 共和黨眾議院", diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index 5513a7a246..41ef101ad6 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -2205,13 +2205,18 @@ "thousandsOfOpenQuestions": "20,000+ 开放问题", "midtermsHubMetaTitle": "2026 年美国中期选举 | Metaculus", "midtermsHubMetaDescription": "Metaculus 社区对 2026 年美国中期选举的实时预测。", - "midtermsHubLastUpdatedFull": "最后更新 {date}", + "midtermsHubChamberCurrent": "当前:", + "midtermsHubChamberForecast": "预测:", + "midtermsHubChamberTooltipBody": "{party}需要再赢得 {count} 个席位才能掌控{chamber};预测者认为这一可能性为 {pct}%。", + "midtermsHubChamberTooltipDisclaimer": "点击查看预测问题", + "midtermsHubPartyDemocrats": "民主党", + "midtermsHubPartyRepublicans": "共和党", + "midtermsHubForecastsRealtime": "预测实时更新。", "midtermsHubChamberSenate": "参议院", "midtermsHubChamberHouse": "众议院", "midtermsHubChamberGovernor": "州长", "midtermsHubChamberControl": "议院控制权", "midtermsHubCongressForecast": "国会控制权", - "midtermsHubDemsNeed": "民主党需要 +{count} 席", "midtermsHubOutcomeRepRep": "共和党参议院 / 共和党众议院", "midtermsHubOutcomeRepDem": "共和党参议院 / 民主党众议院", "midtermsHubOutcomeDemRep": "民主党参议院 / 共和党众议院", diff --git a/front_end/public/images/party-donkey.svg b/front_end/public/images/party-donkey.svg new file mode 100644 index 0000000000..67dafe46d2 --- /dev/null +++ b/front_end/public/images/party-donkey.svg @@ -0,0 +1,3 @@ + diff --git a/front_end/public/images/party-elephant.svg b/front_end/public/images/party-elephant.svg new file mode 100644 index 0000000000..8679fcad00 --- /dev/null +++ b/front_end/public/images/party-elephant.svg @@ -0,0 +1,3 @@ + diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx index e085a78f30..667d935951 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx @@ -3,6 +3,7 @@ import { getTranslations } from "next-intl/server"; import { PostWithForecasts } from "@/types/post"; import cn from "@/utils/core/cn"; +import ChamberRowTooltip from "./chamber_row_tooltip"; import { MIDTERMS_COLORS } from "../constants"; import CvBar from "./cv_bar"; import { ChamberData } from "../helpers/fetch_dashboard_data"; @@ -20,6 +21,14 @@ type Props = { export default async function ChamberControlCard({ data }: Props) { const t = await getTranslations(); + const labels = { + forecast: t("midtermsHubChamberForecast"), + current: t("midtermsHubChamberCurrent"), + democrats: t("midtermsHubPartyDemocrats"), + republicans: t("midtermsHubPartyRepublicans"), + disclaimer: t("midtermsHubChamberTooltipDisclaimer"), + }; + const senateDemProb = getMultipleChoiceOptionProbability( data.senateControl, "Democrats" @@ -37,8 +46,8 @@ export default async function ChamberControlCard({ data }: Props) { "Republicans" ); - const senateDemNeeded = Math.floor(SENATE_TOTAL / 2) + 1 - CURRENT_SENATE.dem; - const houseDemNeeded = Math.floor(HOUSE_TOTAL / 2) + 1 - CURRENT_HOUSE.dem; + const senateLabel = t("midtermsHubChamberSenate"); + const houseLabel = t("midtermsHubChamberHouse"); return (
@@ -47,36 +56,108 @@ export default async function ChamberControlCard({ data }: Props) {

); } +type Labels = { + forecast: string; + current: string; + democrats: string; + republicans: string; + disclaimer: string; +}; + +function buildTooltipBody({ + t, + chamberLabel, + demProb, + repProb, + currentDem, + currentRep, + totalSeats, + labels, +}: { + t: Awaited>; + chamberLabel: string; + demProb: number | null; + repProb: number | null; + currentDem: number; + currentRep: number; + totalSeats: number; + labels: Labels; +}): string | null { + const demIsTrailing = currentDem <= currentRep; + const trailingParty = demIsTrailing ? labels.democrats : labels.republicans; + const trailingCurrent = demIsTrailing ? currentDem : currentRep; + const seatsNeeded = Math.floor(totalSeats / 2) + 1 - trailingCurrent; + const trailingProb = demIsTrailing ? demProb : repProb; + const trailingProbPct = + trailingProb != null ? Math.round(trailingProb * 1000) / 10 : null; + + if (trailingProbPct == null) return null; + + return t("midtermsHubChamberTooltipBody", { + party: trailingParty, + count: seatsNeeded, + chamber: chamberLabel, + pct: trailingProbPct, + }); +} + type RowProps = { chamberLabel: string; demProb: number | null; repProb: number | null; currentDem: number; currentRep: number; - demNeededLabel: string; + totalSeats: number; sourcePost: PostWithForecasts | null; + tooltipBody: string | null; + tooltipDisclaimer: string; + labels: Labels; }; function ChamberRow({ @@ -85,8 +166,10 @@ function ChamberRow({ repProb, currentDem, currentRep, - demNeededLabel, sourcePost, + tooltipBody, + tooltipDisclaimer, + labels, }: RowProps) { // Normalize Dem+Rep so the two bars together represent ~100% (ignores // the small "Other" slice from the underlying multiple-choice question). @@ -107,11 +190,10 @@ function ChamberRow({ {chamberLabel} {demPct != null && repPct != null && ( - + + {labels.forecast} {demPct}% - - {" / "} - + {" / "} {repPct}% )} @@ -123,28 +205,24 @@ function ChamberRow({ pct={demShare} color={MIDTERMS_COLORS.demPrimary} borderColor={MIDTERMS_COLORS.demBorder} - heightClassName="h-3" /> )}
-
- - - R {currentRep} - - {" — "} - - D {currentDem} - +
+ {labels.current} + + D {currentDem} + + {" — "} + + R {currentRep} - {demNeededLabel}
); @@ -154,17 +232,24 @@ function ChamberRow({ href && "cursor-pointer no-underline" ); - if (href) { - return ( - - {inner} - - ); - } - return
{inner}
; + const linkOrDiv = href ? ( + + {inner} + + ) : ( +
{inner}
+ ); + + if (!tooltipBody) return linkOrDiv; + + return ( + + {linkOrDiv} + + ); } diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx new file mode 100644 index 0000000000..7a333279a7 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { FC, ReactNode } from "react"; + +type Props = { + body: string; + disclaimer: string; + children: ReactNode; +}; + +/** + * Wraps a Chamber Control row. The body sentence + disclaimer appear in a + * tooltip directly below the row on hover. The wrapper exposes a `group/cr` + * Tailwind named group so the tooltip can react to hovering anywhere in the + * row. + */ +const ChamberRowTooltip: FC = ({ body, disclaimer, children }) => { + return ( +
+ {children} +
+ {body} + {disclaimer} +
+
+ ); +}; + +export default ChamberRowTooltip; diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx index 65e803642c..935370d13a 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_tabs.tsx @@ -10,7 +10,6 @@ export default async function ChamberTabs({ className }: Props) { const t = await getTranslations(); const tabs: { key: string; label: string; active: boolean }[] = [ { key: "senate", label: t("midtermsHubChamberSenate"), active: true }, - { key: "house", label: t("midtermsHubChamberHouse"), active: false }, { key: "governor", label: t("midtermsHubChamberGovernor"), active: false }, ]; diff --git a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx index b2ff6aa0ac..eec7973b6e 100644 --- a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx @@ -49,8 +49,16 @@ export default async function CongressOutcomeCard({ post }: Props) { const outcomes: Outcome[] = [ buildOutcome("RR", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repBorder), - buildOutcome("RD", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repBorder), - buildOutcome("DR", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demBorder), + buildOutcome( + "RD", + MIDTERMS_COLORS.splitPrimary, + MIDTERMS_COLORS.splitBorder + ), + buildOutcome( + "DR", + MIDTERMS_COLORS.splitPrimary, + MIDTERMS_COLORS.splitBorder + ), buildOutcome("DD", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demBorder), ]; diff --git a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx index d92685a4b6..270bc7ba45 100644 --- a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx @@ -19,8 +19,11 @@ type Props = { className?: string; }; -const BG_OPACITY_DEFAULT = 0.4; -const BG_OPACITY_HOVER = 0.7; +const BG_OPACITY_DEFAULT_LIGHT = 0.4; +const BG_OPACITY_HOVER_LIGHT = 0.7; +// Dark mode needs more saturation to read against the navy card bg. +const BG_OPACITY_DEFAULT_DARK = 0.55; +const BG_OPACITY_HOVER_DARK = 0.85; /** * Visual primitive shaped like the consumer view multiple-choice bar: @@ -42,20 +45,25 @@ const CvBar: FC = ({ className, }) => { const { theme } = useAppTheme(); - const resolvedBorder = theme === "dark" ? color : borderColor ?? color; + const isDark = theme === "dark"; + const resolvedBorder = isDark ? color : borderColor ?? color; + const defaultOpacity = isDark + ? BG_OPACITY_DEFAULT_DARK + : BG_OPACITY_DEFAULT_LIGHT; + const hoverOpacity = isDark ? BG_OPACITY_HOVER_DARK : BG_OPACITY_HOVER_LIGHT; 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), + backgroundColor: addOpacityToHex(color, defaultOpacity), + ["--cv-bar-hover-bg" as string]: addOpacityToHex(color, hoverOpacity), }; return (
= ({ races, tabsSlot }) => {
-
+
diff --git a/front_end/src/app/(main)/midterms-2026/components/party_icons.tsx b/front_end/src/app/(main)/midterms-2026/components/party_icons.tsx new file mode 100644 index 0000000000..146060b2c4 --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/party_icons.tsx @@ -0,0 +1,31 @@ +import { FC, SVGProps } from "react"; + +/** + * Inline donkey/elephant SVGs so the fill picks up via `color` / + * `currentColor` and tints to the active party color. next/image can't do + * that with external SVGs. + */ + +export const ElephantIcon: FC> = (props) => ( + +); + +export const DonkeyIcon: FC> = (props) => ( + +); diff --git a/front_end/src/app/(main)/midterms-2026/constants.ts b/front_end/src/app/(main)/midterms-2026/constants.ts index 19a1ebda2d..e9c64367f7 100644 --- a/front_end/src/app/(main)/midterms-2026/constants.ts +++ b/front_end/src/app/(main)/midterms-2026/constants.ts @@ -5,6 +5,10 @@ export const MIDTERMS_COLORS = { demBorder: "#3A4DD0", repPrimary: "#E8827A", repBorder: "#C53B33", + // Split-control outcomes (Rep Senate / Dem House and vice versa). Neutral + // purple so the bars don't read as either party. + splitPrimary: "#9B7AD6", + splitBorder: "#6F4DB8", // Continuous-gradient anchors. `tossUp` is the neutral midpoint at 50%, // `likelyR` and `likelyD` are the extremes; intermediate colors are // generated via getColorInSpectrum. diff --git a/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts b/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts index 542bd731ad..68ecd1e072 100644 --- a/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts +++ b/front_end/src/app/(main)/midterms-2026/helpers/post_utils.ts @@ -82,19 +82,3 @@ export function getDemWinPct( if (prob == null) return null; return Math.round(prob * 100); } - -export function getLatestUpdateTime( - posts: (PostWithForecasts | null)[] -): Date | null { - let latest: Date | null = null; - for (const post of posts) { - if (!post?.question) continue; - const q = post.question as QuestionWithNumericForecasts; - const startTime = - q.aggregations?.[q.default_aggregation_method]?.latest?.start_time; - if (!startTime) continue; - const d = new Date(startTime); - if (!latest || d > latest) latest = d; - } - return latest; -} diff --git a/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx b/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx index 50f6ff0f44..e3480ad6a6 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/electoral_consequences.tsx @@ -7,6 +7,8 @@ import { } from "@/app/(main)/labor-hub/components/section"; import ConsequenceRow from "../components/consequence_row"; +import { DonkeyIcon, ElephantIcon } from "../components/party_icons"; +import { MIDTERMS_COLORS } from "../constants"; import { MOCK_CONSEQUENCES } from "../data"; export default async function ElectoralConsequencesSection() { @@ -24,10 +26,22 @@ export default async function ElectoralConsequencesSection() { {t("midtermsHubConsequenceQuestion")} - + + {t("midtermsHubConsequenceIfRep")} - + + {t("midtermsHubConsequenceIfDem")}
diff --git a/front_end/src/app/(main)/midterms-2026/sections/footer.tsx b/front_end/src/app/(main)/midterms-2026/sections/footer.tsx index ee55e750fc..5aab8577b1 100644 --- a/front_end/src/app/(main)/midterms-2026/sections/footer.tsx +++ b/front_end/src/app/(main)/midterms-2026/sections/footer.tsx @@ -1,24 +1,8 @@ -import { formatInTimeZone } from "date-fns-tz"; import Link from "next/link"; import { getTranslations } from "next-intl/server"; -import { - fetchChamberData, - fetchSenateRaces, -} from "../helpers/fetch_dashboard_data"; -import { getLatestUpdateTime } from "../helpers/post_utils"; - export default async function FooterSection() { const t = await getTranslations(); - const [{ parentPost: senateParent }, chamber] = await Promise.all([ - fetchSenateRaces(), - fetchChamberData(), - ]); - const latest = getLatestUpdateTime([ - senateParent, - chamber.senateControl, - chamber.houseControl, - ]); return (
@@ -31,13 +15,9 @@ export default async function FooterSection() { metaculus.com
- {latest && ( -

- {t("midtermsHubLastUpdatedFull", { - date: formatInTimeZone(latest, "UTC", "MMMM d, yyyy, HH:mm 'UTC'"), - })} -

- )} +

+ {t("midtermsHubForecastsRealtime")} +

); } From 0500af7042fb534792e2077b7e47de16dd81bd4e Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 14 May 2026 11:48:21 +0200 Subject: [PATCH 06/18] feat: chamber control polish + mobile tooltip + map shift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chamber control card: right-align the Current row so the seat totals line up with the Forecast: percentages above; unify the D/R divider to "/" in both rows at 50% opacity; fix the Dem+Rep bar overflow by switching from flex+gap (which exceeded the container by the gap width) to a 2-column grid with `${dem}fr ${rep}fr` template columns; bold the seats count and probability in the tooltip body via `t.rich` + `` markup in each locale. - chamber row tooltip: stateful client component now. On touch devices (matchMedia '(hover: none)'), the first tap on a row opens the tooltip and swallows the click so the wrapped link doesn't navigate; a small mobile-only close button (×) dismisses, and clicking the tooltip body opens the question. Tapping outside the row dismisses via a document mousedown listener. Desktop still uses pure CSS hover. The wrapper exposes `data-open` so cv_bar can react to the tap-open state in addition to hover. - cv_bar: add `fill` prop so adjacent bars driven by a grid template can render at width:100% (used by Chamber Control). Bump hover opacities (0.7→0.85 light, 0.85→0.95 dark) so the active state reads more clearly. React to both `group/cr` hover and `group-data-[open]/cr` so the bars highlight when the tooltip is shown via touch tap as well as hover. - geographic map: shift projection translate from [380, 270] to [400, 270] so the country sits slightly past dead-center, giving the chamber tabs overlay breathing room on the left. Co-Authored-By: Claude Opus 4.7 (1M context) --- front_end/messages/cs.json | 2 +- front_end/messages/en.json | 2 +- front_end/messages/es.json | 2 +- front_end/messages/pt.json | 2 +- front_end/messages/zh-TW.json | 2 +- front_end/messages/zh.json | 2 +- .../components/chamber_control_card.tsx | 65 ++++++----- .../components/chamber_row_tooltip.tsx | 106 ++++++++++++++++-- .../midterms-2026/components/cv_bar.tsx | 32 ++++-- .../components/geographic_map.tsx | 7 +- 10 files changed, 167 insertions(+), 55 deletions(-) diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index 284577310d..da9800b952 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -2205,7 +2205,7 @@ "midtermsHubMetaDescription": "Předpovědi komunity Metaculus v reálném čase k mezičasovým volbám USA v roce 2026.", "midtermsHubChamberCurrent": "Aktuálně:", "midtermsHubChamberForecast": "Předpověď:", - "midtermsHubChamberTooltipBody": "{party} potřebují o {count} mandátů více pro získání {chamber}; předpovídači tomu dávají {pct}% šanci.", + "midtermsHubChamberTooltipBody": "{party} potřebují o {count} mandátů více pro získání {chamber}; předpovídači tomu dávají {pct}% šanci.", "midtermsHubChamberTooltipDisclaimer": "Klikněte pro zobrazení předpovědní otázky", "midtermsHubPartyDemocrats": "Demokraté", "midtermsHubPartyRepublicans": "Republikáni", diff --git a/front_end/messages/en.json b/front_end/messages/en.json index a6cca8ea67..95c720c2ce 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2199,7 +2199,7 @@ "midtermsHubCongressForecast": "Congress Control", "midtermsHubChamberCurrent": "Current:", "midtermsHubChamberForecast": "Forecast:", - "midtermsHubChamberTooltipBody": "{party} need {count} more seats to secure the {chamber}; forecasters give this a {pct}% chance.", + "midtermsHubChamberTooltipBody": "{party} need {count} more seats to secure the {chamber}; forecasters give this a {pct}% chance.", "midtermsHubChamberTooltipDisclaimer": "Click to view forecasting question", "midtermsHubPartyDemocrats": "Democrats", "midtermsHubPartyRepublicans": "Republicans", diff --git a/front_end/messages/es.json b/front_end/messages/es.json index b18d19ae1b..5c04d1f8e2 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -2205,7 +2205,7 @@ "midtermsHubMetaDescription": "Pronósticos en tiempo real de la comunidad de Metaculus sobre las elecciones intermedias de EE. UU. de 2026.", "midtermsHubChamberCurrent": "Actual:", "midtermsHubChamberForecast": "Pronóstico:", - "midtermsHubChamberTooltipBody": "{party} necesitan {count} escaños más para asegurar el {chamber}; los pronosticadores le dan a esto un {pct}% de probabilidad.", + "midtermsHubChamberTooltipBody": "{party} necesitan {count} escaños más para asegurar el {chamber}; los pronosticadores le dan a esto un {pct}% de probabilidad.", "midtermsHubChamberTooltipDisclaimer": "Haz clic para ver la pregunta de pronóstico", "midtermsHubPartyDemocrats": "Los demócratas", "midtermsHubPartyRepublicans": "Los republicanos", diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index 25718dc82c..fb4439a908 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -2203,7 +2203,7 @@ "midtermsHubMetaDescription": "Previsões em tempo real da comunidade Metaculus sobre as eleições de meio de mandato dos EUA de 2026.", "midtermsHubChamberCurrent": "Atual:", "midtermsHubChamberForecast": "Previsão:", - "midtermsHubChamberTooltipBody": "{party} precisam de mais {count} cadeiras para garantir o {chamber}; os previsores dão a isso uma chance de {pct}%.", + "midtermsHubChamberTooltipBody": "{party} precisam de mais {count} cadeiras para garantir o {chamber}; os previsores dão a isso uma chance de {pct}%.", "midtermsHubChamberTooltipDisclaimer": "Clique para ver a pergunta de previsão", "midtermsHubPartyDemocrats": "Os democratas", "midtermsHubPartyRepublicans": "Os republicanos", diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index 4dc429f663..7700b8ae91 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -2202,7 +2202,7 @@ "midtermsHubMetaDescription": "Metaculus 社群對 2026 年美國期中選舉的即時預測。", "midtermsHubChamberCurrent": "目前:", "midtermsHubChamberForecast": "預測:", - "midtermsHubChamberTooltipBody": "{party}還需要 {count} 個席位才能掌控{chamber};預測者認為這一機率為 {pct}%。", + "midtermsHubChamberTooltipBody": "{party}還需要 {count} 個席位 才能掌控{chamber};預測者認為這一機率為 {pct}%。", "midtermsHubChamberTooltipDisclaimer": "點擊查看預測問題", "midtermsHubPartyDemocrats": "民主黨", "midtermsHubPartyRepublicans": "共和黨", diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index 41ef101ad6..680b29f0ee 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -2207,7 +2207,7 @@ "midtermsHubMetaDescription": "Metaculus 社区对 2026 年美国中期选举的实时预测。", "midtermsHubChamberCurrent": "当前:", "midtermsHubChamberForecast": "预测:", - "midtermsHubChamberTooltipBody": "{party}需要再赢得 {count} 个席位才能掌控{chamber};预测者认为这一可能性为 {pct}%。", + "midtermsHubChamberTooltipBody": "{party}需要再赢得 {count} 个席位 才能掌控{chamber};预测者认为这一可能性为 {pct}%。", "midtermsHubChamberTooltipDisclaimer": "点击查看预测问题", "midtermsHubPartyDemocrats": "民主党", "midtermsHubPartyRepublicans": "共和党", diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx index 667d935951..a16126c59d 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx @@ -1,3 +1,4 @@ +import { ReactNode } from "react"; import { getTranslations } from "next-intl/server"; import { PostWithForecasts } from "@/types/post"; @@ -61,7 +62,6 @@ export default async function ChamberControlCard({ data }: Props) { repProb={senateRepProb} currentDem={CURRENT_SENATE.dem} currentRep={CURRENT_SENATE.rep} - totalSeats={SENATE_TOTAL} sourcePost={data.senateControl} tooltipBody={buildTooltipBody({ t, @@ -82,7 +82,6 @@ export default async function ChamberControlCard({ data }: Props) { repProb={houseRepProb} currentDem={CURRENT_HOUSE.dem} currentRep={CURRENT_HOUSE.rep} - totalSeats={HOUSE_TOTAL} sourcePost={data.houseControl} tooltipBody={buildTooltipBody({ t, @@ -128,7 +127,7 @@ function buildTooltipBody({ currentRep: number; totalSeats: number; labels: Labels; -}): string | null { +}): ReactNode | null { const demIsTrailing = currentDem <= currentRep; const trailingParty = demIsTrailing ? labels.democrats : labels.republicans; const trailingCurrent = demIsTrailing ? currentDem : currentRep; @@ -139,11 +138,12 @@ function buildTooltipBody({ if (trailingProbPct == null) return null; - return t("midtermsHubChamberTooltipBody", { + return t.rich("midtermsHubChamberTooltipBody", { party: trailingParty, count: seatsNeeded, chamber: chamberLabel, pct: trailingProbPct, + b: (chunks) => {chunks}, }); } @@ -153,9 +153,8 @@ type RowProps = { repProb: number | null; currentDem: number; currentRep: number; - totalSeats: number; sourcePost: PostWithForecasts | null; - tooltipBody: string | null; + tooltipBody: ReactNode | null; tooltipDisclaimer: string; labels: Labels; }; @@ -183,43 +182,49 @@ function ChamberRow({ const href = sourcePost ? `/questions/${sourcePost.id}` : undefined; + const slash = ( + // 50% opacity separator shared by Forecast and Current rows. + {" / "} + ); + const inner = ( <> -
+
{chamberLabel} {demPct != null && repPct != null && ( - + {labels.forecast} {demPct}% - {" / "} + {slash} {repPct}% )}
-
- {demShare != null && repShare != null && ( - <> - - - - )} -
-
+ {demShare != null && repShare != null && ( +
+ + +
+ )} +
{labels.current} D {currentDem} - {" — "} + {slash} R {currentRep} @@ -248,7 +253,11 @@ function ChamberRow({ if (!tooltipBody) return linkOrDiv; return ( - + {linkOrDiv} ); diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx index 7a333279a7..487848e952 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx @@ -1,29 +1,115 @@ "use client"; -import { FC, ReactNode } from "react"; +import { + FC, + MouseEvent as RMouseEvent, + ReactNode, + useEffect, + useRef, + useState, +} from "react"; + +import cn from "@/utils/core/cn"; type Props = { - body: string; + body: ReactNode; disclaimer: string; + /** Question URL — tapping the tooltip body on mobile opens this in a new + * tab; on desktop the wrapped link handles the click directly. */ + href?: string; children: ReactNode; }; /** - * Wraps a Chamber Control row. The body sentence + disclaimer appear in a - * tooltip directly below the row on hover. The wrapper exposes a `group/cr` - * Tailwind named group so the tooltip can react to hovering anywhere in the - * row. + * Wraps a Chamber Control row. + * + * Desktop (hover-capable): the tooltip appears via CSS on hover. + * Touch devices: the row toggles the tooltip on tap. While open: + * - tapping the tooltip body opens the question + * - tapping the close button dismisses + * - tapping anywhere outside the row dismisses + * + * Exposes `group/cr` so descendants (e.g. the CvBars) can react to hover + * via `group-hover/cr:*`, and `data-open` so they can also react to the + * touch-tap state via `group-data-[open]/cr:*`. */ -const ChamberRowTooltip: FC = ({ body, disclaimer, children }) => { +const ChamberRowTooltip: FC = ({ body, disclaimer, href, children }) => { + const [open, setOpen] = useState(false); + const [isTouch, setIsTouch] = useState(false); + const wrapperRef = useRef(null); + + useEffect(() => { + setIsTouch(window.matchMedia("(hover: none)").matches); + }, []); + + // Outside-tap dismiss (mousedown to avoid racing the opener's click). + useEffect(() => { + if (!open) return; + const handle = (e: globalThis.MouseEvent) => { + if (wrapperRef.current?.contains(e.target as Node)) return; + setOpen(false); + }; + document.addEventListener("mousedown", handle); + return () => document.removeEventListener("mousedown", handle); + }, [open]); + + // First tap on a touch device opens the tooltip and swallows the click so + // the wrapped link doesn't navigate. A subsequent tap anywhere in the + // row (including the tooltip body) navigates via window.open. + const handleWrapperClick = (e: RMouseEvent) => { + if (!isTouch) return; + if (!open) { + e.preventDefault(); + e.stopPropagation(); + setOpen(true); + } + }; + + const navigate = () => { + if (!href) return; + window.open(href, "_blank", "noopener,noreferrer"); + }; + + const handleTooltipClick = (e: RMouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + navigate(); + }; + return ( -
+
{children}
+ {open && isTouch && ( + + )} {body} - {disclaimer} + + {disclaimer} +
); diff --git a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx index 270bc7ba45..d9e59700d4 100644 --- a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx @@ -7,9 +7,14 @@ import cn from "@/utils/core/cn"; import { addOpacityToHex } from "@/utils/core/colors"; type Props = { - /** 0-100 — how much of the parent track this bar fills */ - pct: number; - /** Solid hex color (e.g. #6B7AE8). Drives the filled bg at 40% opacity. */ + /** 0-100 — how much of the parent track this bar fills. Ignored when + * `fill` is true. */ + pct?: number; + /** When true, the bar fills 100% of its parent so the parent grid / + * flex can drive its width. Required for adjacent bars that must + * exactly tile their container without gap overflow. */ + fill?: boolean; + /** Solid hex color (e.g. #6B7AE8). Drives the filled bg. */ color: string; /** Border color override for light mode. Defaults to `color`. Use a * darker shade for sharp contrast against the soft fill. */ @@ -20,18 +25,19 @@ type Props = { }; const BG_OPACITY_DEFAULT_LIGHT = 0.4; -const BG_OPACITY_HOVER_LIGHT = 0.7; +const BG_OPACITY_HOVER_LIGHT = 0.85; // Dark mode needs more saturation to read against the navy card bg. const BG_OPACITY_DEFAULT_DARK = 0.55; -const BG_OPACITY_HOVER_DARK = 0.85; +const BG_OPACITY_HOVER_DARK = 0.95; /** * Visual primitive shaped like the consumer view multiple-choice bar: * rounded, full-color border, semi-transparent fill in the same color. * - * Hover styling is *driven by the nearest `group/cv` ancestor* — wrap - * the parent row in `className="group/cv"` so hovering anywhere in the - * row brightens every bar inside. + * Hover/active styling is driven by the nearest named-group ancestor: + * - `group/cv` for direct hover over the row + * - `group/cr` for tooltip-shown state on Chamber Control rows + * (tapped open on touch devices, hovered on desktop) * * In dark mode the darker `borderColor` would blend with the card bg, * so we fall back to the primary `color` (which is a brighter shade) @@ -39,6 +45,7 @@ const BG_OPACITY_HOVER_DARK = 0.85; */ const CvBar: FC = ({ pct, + fill, color, borderColor, heightClassName = "h-5", @@ -52,8 +59,10 @@ const CvBar: FC = ({ : BG_OPACITY_DEFAULT_LIGHT; const hoverOpacity = isDark ? BG_OPACITY_HOVER_DARK : BG_OPACITY_HOVER_LIGHT; + const width = fill ? "100%" : `${Math.max(pct ?? 1, 1)}%`; + const style: CSSProperties = { - width: `${Math.max(pct, 1)}%`, + width, borderColor: resolvedBorder, backgroundColor: addOpacityToHex(color, defaultOpacity), ["--cv-bar-hover-bg" as string]: addOpacityToHex(color, hoverOpacity), @@ -63,7 +72,12 @@ const CvBar: FC = ({
Date: Thu, 14 May 2026 12:52:23 +0200 Subject: [PATCH 07/18] feat: refine chamber + consequences visuals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chamber control: center the "Current: D 47 / R 53" row (was right-aligned). - cv_bar: make the active state unmistakable. Hover / tooltip-shown now drives the bar to full color (no opacity), darkens the border to the deep `borderColor` variant, and wraps it in a colored glow ring via box-shadow. Three triggers fire the same look: `group-hover/cv`, `group-hover/cr`, and `group-data-[open]/cr` so the highlight reads identically whether the row is hovered on desktop or tapped open on touch. - electoral consequences: redesign the party header row to match the reference layout — two saturated colored cards (red for Republican Congress, navy blue-900 for Democratic Congress), centered party logo on top in white, bold title + small uppercase subtitle below. The "Question" column header above the rows is now empty, letting the row content speak for itself. - i18n: add HeaderRepTitle / HeaderRepSubtitle / HeaderDemTitle / HeaderDemSubtitle keys across all six locales for the new card copy. - chamber row tooltip: replace the SSR-unfriendly setState-in-effect touch detection with useSyncExternalStore around a (hover: none) matchMedia query (lint compliant; same behavior). Co-Authored-By: Claude Opus 4.7 (1M context) --- front_end/messages/cs.json | 4 ++ front_end/messages/en.json | 4 ++ front_end/messages/es.json | 4 ++ front_end/messages/pt.json | 4 ++ front_end/messages/zh-TW.json | 4 ++ front_end/messages/zh.json | 4 ++ .../components/chamber_control_card.tsx | 4 +- .../components/chamber_row_tooltip.tsx | 21 ++++-- .../midterms-2026/components/cv_bar.tsx | 40 ++++++----- .../sections/electoral_consequences.tsx | 72 +++++++++++++------ 10 files changed, 115 insertions(+), 46 deletions(-) diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index da9800b952..89938c6d23 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -2229,6 +2229,10 @@ "midtermsHubConsequenceQuestion": "Otázka", "midtermsHubConsequenceIfRep": "při Republikánském Kongresu", "midtermsHubConsequenceIfDem": "při Demokratickém Kongresu", + "midtermsHubConsequenceHeaderRepTitle": "Republikánský Kongres", + "midtermsHubConsequenceHeaderRepSubtitle": "Pokud má GOP většinu", + "midtermsHubConsequenceHeaderDemTitle": "Demokratický Kongres", + "midtermsHubConsequenceHeaderDemSubtitle": "Pokud mají Demokraté většinu", "midtermsHubConsequenceClimate": "Bude před rokem 2028 přijata významná federální klimatická legislativa?", "midtermsHubConsequenceMinWage": "Bude před rokem 2028 zvýšena federální minimální mzda?", "midtermsHubConsequenceImmigration": "Bude před rokem 2028 přijat významný zákon o imigrační reformě?", diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 95c720c2ce..5699490b39 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2218,6 +2218,10 @@ "midtermsHubConsequenceQuestion": "Question", "midtermsHubConsequenceIfRep": "if Rep Congress", "midtermsHubConsequenceIfDem": "if Dem Congress", + "midtermsHubConsequenceHeaderRepTitle": "Republican Congress", + "midtermsHubConsequenceHeaderRepSubtitle": "If GOP holds majority", + "midtermsHubConsequenceHeaderDemTitle": "Democratic Congress", + "midtermsHubConsequenceHeaderDemSubtitle": "If Dems hold majority", "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", diff --git a/front_end/messages/es.json b/front_end/messages/es.json index 5c04d1f8e2..7188a476df 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -2229,6 +2229,10 @@ "midtermsHubConsequenceQuestion": "Pregunta", "midtermsHubConsequenceIfRep": "si Congreso Rep", "midtermsHubConsequenceIfDem": "si Congreso Dem", + "midtermsHubConsequenceHeaderRepTitle": "Congreso Republicano", + "midtermsHubConsequenceHeaderRepSubtitle": "Si el GOP tiene la mayoría", + "midtermsHubConsequenceHeaderDemTitle": "Congreso Demócrata", + "midtermsHubConsequenceHeaderDemSubtitle": "Si los demócratas tienen la mayoría", "midtermsHubConsequenceClimate": "¿Se aprobará una legislación climática federal importante antes de 2028?", "midtermsHubConsequenceMinWage": "¿Se aumentará el salario mínimo federal antes de 2028?", "midtermsHubConsequenceImmigration": "¿Se aprobará un proyecto de ley importante de reforma migratoria antes de 2028?", diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index fb4439a908..22e50aea40 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -2227,6 +2227,10 @@ "midtermsHubConsequenceQuestion": "Pergunta", "midtermsHubConsequenceIfRep": "se Congresso Rep", "midtermsHubConsequenceIfDem": "se Congresso Dem", + "midtermsHubConsequenceHeaderRepTitle": "Congresso Republicano", + "midtermsHubConsequenceHeaderRepSubtitle": "Se o Partido Republicano tiver maioria", + "midtermsHubConsequenceHeaderDemTitle": "Congresso Democrata", + "midtermsHubConsequenceHeaderDemSubtitle": "Se os Democratas tiverem maioria", "midtermsHubConsequenceClimate": "Uma legislação climática federal importante será aprovada antes de 2028?", "midtermsHubConsequenceMinWage": "O salário mínimo federal será aumentado antes de 2028?", "midtermsHubConsequenceImmigration": "Um projeto de reforma migratória importante será aprovado antes de 2028?", diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index 7700b8ae91..4e31d9a226 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -2226,6 +2226,10 @@ "midtermsHubConsequenceQuestion": "問題", "midtermsHubConsequenceIfRep": "若共和黨掌握國會", "midtermsHubConsequenceIfDem": "若民主黨掌握國會", + "midtermsHubConsequenceHeaderRepTitle": "共和黨國會", + "midtermsHubConsequenceHeaderRepSubtitle": "若共和黨佔多數", + "midtermsHubConsequenceHeaderDemTitle": "民主黨國會", + "midtermsHubConsequenceHeaderDemSubtitle": "若民主黨佔多數", "midtermsHubConsequenceClimate": "2028 年之前是否會通過重要的聯邦氣候立法?", "midtermsHubConsequenceMinWage": "2028 年之前是否會提高聯邦最低工資?", "midtermsHubConsequenceImmigration": "2028 年之前是否會通過重要的移民改革法案?", diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index 680b29f0ee..bac0272d6a 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -2231,6 +2231,10 @@ "midtermsHubConsequenceQuestion": "问题", "midtermsHubConsequenceIfRep": "若共和党控制国会", "midtermsHubConsequenceIfDem": "若民主党控制国会", + "midtermsHubConsequenceHeaderRepTitle": "共和党国会", + "midtermsHubConsequenceHeaderRepSubtitle": "若共和党占多数", + "midtermsHubConsequenceHeaderDemTitle": "民主党国会", + "midtermsHubConsequenceHeaderDemSubtitle": "若民主党占多数", "midtermsHubConsequenceClimate": "2028 年之前是否会通过重要的联邦气候立法?", "midtermsHubConsequenceMinWage": "2028 年之前是否会提高联邦最低工资?", "midtermsHubConsequenceImmigration": "2028 年之前是否会通过重要的移民改革法案?", diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx index a16126c59d..ccd1bcdb93 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx @@ -1,5 +1,5 @@ -import { ReactNode } from "react"; import { getTranslations } from "next-intl/server"; +import { ReactNode } from "react"; import { PostWithForecasts } from "@/types/post"; import cn from "@/utils/core/cn"; @@ -219,7 +219,7 @@ function ChamberRow({ />
)} -
+
{labels.current} D {currentDem} diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx index 487848e952..d7c027a84a 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx @@ -7,6 +7,7 @@ import { useEffect, useRef, useState, + useSyncExternalStore, } from "react"; import cn from "@/utils/core/cn"; @@ -33,15 +34,25 @@ type Props = { * via `group-hover/cr:*`, and `data-open` so they can also react to the * touch-tap state via `group-data-[open]/cr:*`. */ +// Server snapshot: assume non-touch so the close button (mobile-only) +// is hidden during SSR and only appears after hydration on a touch device. +const subscribeHoverMQ = (callback: () => void) => { + const mql = window.matchMedia("(hover: none)"); + mql.addEventListener("change", callback); + return () => mql.removeEventListener("change", callback); +}; +const getHoverNoneSnapshot = () => window.matchMedia("(hover: none)").matches; +const getHoverNoneServerSnapshot = () => false; + const ChamberRowTooltip: FC = ({ body, disclaimer, href, children }) => { const [open, setOpen] = useState(false); - const [isTouch, setIsTouch] = useState(false); + const isTouch = useSyncExternalStore( + subscribeHoverMQ, + getHoverNoneSnapshot, + getHoverNoneServerSnapshot + ); const wrapperRef = useRef(null); - useEffect(() => { - setIsTouch(window.matchMedia("(hover: none)").matches); - }, []); - // Outside-tap dismiss (mousedown to avoid racing the opener's click). useEffect(() => { if (!open) return; diff --git a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx index d9e59700d4..67533534e3 100644 --- a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx @@ -25,23 +25,29 @@ type Props = { }; const BG_OPACITY_DEFAULT_LIGHT = 0.4; -const BG_OPACITY_HOVER_LIGHT = 0.85; // Dark mode needs more saturation to read against the navy card bg. const BG_OPACITY_DEFAULT_DARK = 0.55; -const BG_OPACITY_HOVER_DARK = 0.95; +// Glow ring radius (px) used on active state. +const ACTIVE_RING_PX = 3; +const ACTIVE_RING_OPACITY = 0.45; /** * Visual primitive shaped like the consumer view multiple-choice bar: * rounded, full-color border, semi-transparent fill in the same color. * - * Hover/active styling is driven by the nearest named-group ancestor: + * Active (hover or tooltip-shown) styling is driven by the nearest + * named-group ancestor: * - `group/cv` for direct hover over the row - * - `group/cr` for tooltip-shown state on Chamber Control rows - * (tapped open on touch devices, hovered on desktop) + * - `group/cr` for the Chamber Control tooltip wrapper, which fires + * either via hover (desktop) or `data-open` (touch tap) * - * In dark mode the darker `borderColor` would blend with the card bg, - * so we fall back to the primary `color` (which is a brighter shade) - * for the border instead. + * On active, the fill goes to 100% color (no opacity), the border + * darkens slightly via a CSS variable, and a colored glow ring is + * applied via box-shadow so the highlight is unmistakable. + * + * In dark mode the darker `borderColor` would blend with the card bg + * at rest, so we fall back to the primary `color` (which is a brighter + * shade) for the resting border instead. */ const CvBar: FC = ({ pct, @@ -57,7 +63,6 @@ const CvBar: FC = ({ const defaultOpacity = isDark ? BG_OPACITY_DEFAULT_DARK : BG_OPACITY_DEFAULT_LIGHT; - const hoverOpacity = isDark ? BG_OPACITY_HOVER_DARK : BG_OPACITY_HOVER_LIGHT; const width = fill ? "100%" : `${Math.max(pct ?? 1, 1)}%`; @@ -65,19 +70,20 @@ const CvBar: FC = ({ width, borderColor: resolvedBorder, backgroundColor: addOpacityToHex(color, defaultOpacity), - ["--cv-bar-hover-bg" as string]: addOpacityToHex(color, hoverOpacity), + // Active state: full color, slightly darker border, glow ring. + ["--cv-bar-active-bg" as string]: color, + ["--cv-bar-active-border" as string]: borderColor ?? color, + ["--cv-bar-active-ring" as string]: `0 0 0 ${ACTIVE_RING_PX}px ${addOpacityToHex(color, ACTIVE_RING_OPACITY)}`, }; return (
-
- - {t("midtermsHubConsequenceQuestion")} - - - - {t("midtermsHubConsequenceIfRep")} - - - - {t("midtermsHubConsequenceIfDem")} - +
+ {/* Empty header above the Question column — the row content + speaks for itself, matching the reference layout. */} +
+ +
{MOCK_CONSEQUENCES.map((row) => ( @@ -52,3 +51,32 @@ export default async function ElectoralConsequencesSection() { ); } + +type PartyHeaderProps = { + backgroundColor: string; + Icon: typeof ElephantIcon; + title: string; + subtitle: string; +}; + +function PartyHeader({ + backgroundColor, + Icon, + title, + subtitle, +}: PartyHeaderProps) { + return ( +
+
+ ); +} From 6583a8e3a30b86750789c451cac5b3ea7bbbf01f Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 14 May 2026 12:58:02 +0200 Subject: [PATCH 08/18] feat: column hover + horizontal headers + pink split bars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - electoral consequences: rewrite as a client component (ConsequenceGrid) that tracks a per-column hover state. Hovering the colored party header card or any bar cell in that column lights the entire column: the header card switches to a darker / more-saturated background, and the bars in that column receive the full active treatment (solid color fill, darkened border, glow ring) regardless of pointer position. - party headers reformatted to a horizontal layout — logo on the left, two stacked lines on the right (bold title, small uppercase subtitle). Cards are noticeably shorter as a result. - cv_bar: add an `active` prop that maps to `data-active` and a matching `data-[active]:` Tailwind variant, so external state (like the column-hover above) can force the active styling. Existing group-hover/cv, group-hover/cr, and group-data-[open]/cr triggers remain. - constants: swap the split-control color from purple (#9B7AD6 / #6F4DB8) to pink (#E879A6 / #BE3F7E). Affects the RD and DR rows in the Congress Outcome card. - consequence_row.tsx removed (its layout is now inlined inside the grid; the section file resolves the question copy and passes it through as a prop). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/consequence_grid.tsx | 189 ++++++++++++++++++ .../components/consequence_row.tsx | 71 ------- .../midterms-2026/components/cv_bar.tsx | 11 +- .../src/app/(main)/midterms-2026/constants.ts | 8 +- .../sections/electoral_consequences.tsx | 103 +++++----- 5 files changed, 248 insertions(+), 134 deletions(-) create mode 100644 front_end/src/app/(main)/midterms-2026/components/consequence_grid.tsx delete mode 100644 front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx diff --git a/front_end/src/app/(main)/midterms-2026/components/consequence_grid.tsx b/front_end/src/app/(main)/midterms-2026/components/consequence_grid.tsx new file mode 100644 index 0000000000..927b81190e --- /dev/null +++ b/front_end/src/app/(main)/midterms-2026/components/consequence_grid.tsx @@ -0,0 +1,189 @@ +"use client"; + +import { FC, useState } from "react"; + +import cn from "@/utils/core/cn"; + +import CvBar from "./cv_bar"; +import { DonkeyIcon, ElephantIcon } from "./party_icons"; +import { MIDTERMS_COLORS } from "../constants"; + +type Column = "rep" | "dem"; + +export type ConsequenceGridRow = { + key: string; + question: string; + repPct: number; + demPct: number; + ifRepLabel: string; + ifDemLabel: string; +}; + +export type ConsequenceHeaderCopy = { + title: string; + subtitle: string; +}; + +type Props = { + rows: ConsequenceGridRow[]; + repHeader: ConsequenceHeaderCopy; + demHeader: ConsequenceHeaderCopy; +}; + +// Header card colors. The "active" variants are darker / more saturated so +// the column reads as visibly lit when the user hovers anywhere inside it. +const REP_HEADER_BG = MIDTERMS_COLORS.repBorder; // #C53B33 +const REP_HEADER_BG_ACTIVE = "#A02B25"; +const DEM_HEADER_BG = "#1E3A8A"; // Tailwind blue-900 +const DEM_HEADER_BG_ACTIVE = "#152A66"; + +const ConsequenceGrid: FC = ({ rows, repHeader, demHeader }) => { + const [hovered, setHovered] = useState(null); + + const enter = (col: Column) => () => setHovered(col); + const leave = () => setHovered(null); + + return ( +
+
+ {/* Empty header above the Question column. */} +
+ + +
+ + {rows.map((row) => ( +
+

+ {row.question} +

+ + +
+ ))} +
+ ); +}; + +export default ConsequenceGrid; + +type PartyHeaderProps = { + backgroundColor: string; + activeBackgroundColor: string; + Icon: typeof ElephantIcon; + title: string; + subtitle: string; + active: boolean; + onMouseEnter: () => void; + onMouseLeave: () => void; +}; + +const PartyHeader: FC = ({ + backgroundColor, + activeBackgroundColor, + Icon, + title, + subtitle, + active, + onMouseEnter, + onMouseLeave, +}) => { + return ( +
+
+ ); +}; + +type BarCellProps = { + pct: number; + color: string; + borderColor: string; + mobileLabel: string; + active: boolean; + onMouseEnter: () => void; + onMouseLeave: () => void; +}; + +const BarCell: FC = ({ + pct, + color, + borderColor, + mobileLabel, + active, + onMouseEnter, + onMouseLeave, +}) => { + return ( +
+ + {mobileLabel} + +
+ + + {pct}% + +
+
+ ); +}; diff --git a/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx b/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx deleted file mode 100644 index 8b3669cc17..0000000000 --- a/front_end/src/app/(main)/midterms-2026/components/consequence_row.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { getTranslations } from "next-intl/server"; - -import { MIDTERMS_COLORS } from "../constants"; -import { ConsequenceRow as ConsequenceRowData } from "../data"; -import CvBar from "./cv_bar"; - -type Props = { - row: ConsequenceRowData; -}; - -export default async function ConsequenceRow({ row }: Props) { - const t = await getTranslations(); - const question = (() => { - switch (row.questionKey) { - case "climate": - return t("midtermsHubConsequenceClimate"); - case "minWage": - return t("midtermsHubConsequenceMinWage"); - case "immigration": - return t("midtermsHubConsequenceImmigration"); - case "shutdown": - return t("midtermsHubConsequenceShutdown"); - } - })(); - - return ( -
-

- {question} -

- - -
- ); -} - -function ConsequenceBar({ - pct, - color, - borderColor, - mobileLabel, -}: { - pct: number; - color: string; - borderColor: string; - mobileLabel: string; -}) { - return ( -
- - {mobileLabel} - -
- - - {pct}% - -
-
- ); -} diff --git a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx index 67533534e3..9c2cab26b2 100644 --- a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx @@ -21,6 +21,10 @@ type Props = { borderColor?: string; /** Tailwind height class (default `h-5`). */ heightClassName?: string; + /** Force the active (hover-like) styling regardless of cursor position. + * Used when external state (e.g. column-level hover in the Electoral + * Consequences table) decides the bar should appear lit. */ + active?: boolean; className?: string; }; @@ -55,6 +59,7 @@ const CvBar: FC = ({ color, borderColor, heightClassName = "h-5", + active, className, }) => { const { theme } = useAppTheme(); @@ -78,12 +83,16 @@ const CvBar: FC = ({ return (
({ + key: row.questionKey, + question: getQuestionText(row, t), + repPct: row.repCongressPct, + demPct: row.demCongressPct, + ifRepLabel, + ifDemLabel, + })); + return ( @@ -26,57 +37,33 @@ export default async function ElectoralConsequencesSection() { {t("midtermsHubConsequencesSubtitle")} -
-
- {/* Empty header above the Question column — the row content - speaks for itself, matching the reference layout. */} -
- - -
- {MOCK_CONSEQUENCES.map((row) => ( - - ))} -
+ ); } -type PartyHeaderProps = { - backgroundColor: string; - Icon: typeof ElephantIcon; - title: string; - subtitle: string; -}; - -function PartyHeader({ - backgroundColor, - Icon, - title, - subtitle, -}: PartyHeaderProps) { - return ( -
-
- ); +function getQuestionText( + row: ConsequenceRowData, + t: Awaited> +): string { + switch (row.questionKey) { + case "climate": + return t("midtermsHubConsequenceClimate"); + case "minWage": + return t("midtermsHubConsequenceMinWage"); + case "immigration": + return t("midtermsHubConsequenceImmigration"); + case "shutdown": + return t("midtermsHubConsequenceShutdown"); + } } From bbae90154ad343c31cb8c956a57f9e1ef12bc9c7 Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 14 May 2026 13:11:55 +0200 Subject: [PATCH 09/18] feat: alternating dem/rep dashed border on split-control bars Replace the pink fill on the RD and DR (Rep Senate / Dem House and Dem Senate / Rep House) rows in the Congress Outcome card with a transparent-fill bar wrapped in a multi-colored dashed border whose dashes alternate between the dem and rep colors. - cv_bar: add an `alternatingColors: [string, string]` prop. When set, the bar renders as inline SVG with two stacked rect outlines that share the same dash array but offset by one dash, producing a continuous alternating-color dashed border. Glow ring still fires on the same hover / data-active triggers, sourced from the first color in the pair. - congress outcome: model outcomes as a discriminated union (`solid` vs `alternating`), so the bar variant is type-driven. RD/DR now flow through the alternating path. - constants: drop the now-orphaned splitPrimary / splitBorder hex tokens. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/congress_outcome_card.tsx | 60 ++++++++----- .../midterms-2026/components/cv_bar.tsx | 86 +++++++++++++++++-- .../src/app/(main)/midterms-2026/constants.ts | 4 - 3 files changed, 116 insertions(+), 34 deletions(-) diff --git a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx index eec7973b6e..3ac772e380 100644 --- a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx @@ -16,12 +16,21 @@ const OUTCOME_OPTION_LABEL: Record = { DD: "Dem Senate / Dem House", }; +// Split outcomes (Rep Senate / Dem House and vice versa) use an alternating +// dem+rep dashed border instead of a single-color fill so neither party +// reads as the "owner" of the bar. +const SPLIT_COLORS: [string, string] = [ + MIDTERMS_COLORS.demPrimary, + MIDTERMS_COLORS.repPrimary, +]; + type Outcome = { key: OutcomeKey; pct: number | null; - color: string; - borderColor: string; -}; +} & ( + | { kind: "solid"; color: string; borderColor: string } + | { kind: "alternating" } +); type Props = { post: PostWithForecasts | null; @@ -30,7 +39,7 @@ type Props = { export default async function CongressOutcomeCard({ post }: Props) { const t = await getTranslations(); - const buildOutcome = ( + const buildSolid = ( key: OutcomeKey, color: string, borderColor: string @@ -42,24 +51,29 @@ export default async function CongressOutcomeCard({ post }: Props) { return { key, pct: prob != null ? Math.round(prob * 1000) / 10 : null, + kind: "solid", color, borderColor, }; }; + const buildAlternating = (key: OutcomeKey): Outcome => { + const prob = getMultipleChoiceOptionProbability( + post, + OUTCOME_OPTION_LABEL[key] + ); + return { + key, + pct: prob != null ? Math.round(prob * 1000) / 10 : null, + kind: "alternating", + }; + }; + const outcomes: Outcome[] = [ - buildOutcome("RR", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repBorder), - buildOutcome( - "RD", - MIDTERMS_COLORS.splitPrimary, - MIDTERMS_COLORS.splitBorder - ), - buildOutcome( - "DR", - MIDTERMS_COLORS.splitPrimary, - MIDTERMS_COLORS.splitBorder - ), - buildOutcome("DD", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demBorder), + buildSolid("RR", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repBorder), + buildAlternating("RD"), + buildAlternating("DR"), + buildSolid("DD", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demBorder), ]; const labels: Record = { @@ -108,11 +122,15 @@ function OutcomeRow({ {label}
- + {outcome.kind === "solid" ? ( + + ) : ( + + )} {outcome.pct != null ? `${outcome.pct.toFixed(1)}%` : "—"} diff --git a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx index 9c2cab26b2..42968ab8a5 100644 --- a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx @@ -14,11 +14,17 @@ type Props = { * flex can drive its width. Required for adjacent bars that must * exactly tile their container without gap overflow. */ fill?: boolean; - /** Solid hex color (e.g. #6B7AE8). Drives the filled bg. */ - color: string; + /** Solid hex color (e.g. #6B7AE8). Drives the filled bg. Ignored when + * `alternatingColors` is provided. */ + color?: string; /** Border color override for light mode. Defaults to `color`. Use a * darker shade for sharp contrast against the soft fill. */ borderColor?: string; + /** When provided, the bar renders with a transparent fill and a + * multi-colored dashed border alternating between the two colors. + * Used for split-control outcomes where neither party "owns" the + * bar. Overrides `color` / `borderColor`. */ + alternatingColors?: [string, string]; /** Tailwind height class (default `h-5`). */ heightClassName?: string; /** Force the active (hover-like) styling regardless of cursor position. @@ -34,6 +40,9 @@ const BG_OPACITY_DEFAULT_DARK = 0.55; // Glow ring radius (px) used on active state. const ACTIVE_RING_PX = 3; const ACTIVE_RING_OPACITY = 0.45; +// Alternating-bar stroke + dash sizing. +const ALT_STROKE_WIDTH = 3; // half visually clipped inside, ~1.5px visible +const ALT_DASH = 8; /** * Visual primitive shaped like the consumer view multiple-choice bar: @@ -52,33 +61,92 @@ const ACTIVE_RING_OPACITY = 0.45; * In dark mode the darker `borderColor` would blend with the card bg * at rest, so we fall back to the primary `color` (which is a brighter * shade) for the resting border instead. + * + * When `alternatingColors` is provided, the bar is rendered as inline + * SVG with two stacked rect outlines whose dashes are offset by one + * dash length, producing a continuous alternating dashed border. */ const CvBar: FC = ({ pct, fill, color, borderColor, + alternatingColors, heightClassName = "h-5", active, className, }) => { const { theme } = useAppTheme(); const isDark = theme === "dark"; - const resolvedBorder = isDark ? color : borderColor ?? color; + + const width = fill ? "100%" : `${Math.max(pct ?? 1, 1)}%`; + + if (alternatingColors) { + const [colorA, colorB] = alternatingColors; + return ( +
+ +
+ ); + } + + const solidColor = color ?? "#999999"; + const resolvedBorder = isDark ? solidColor : borderColor ?? solidColor; const defaultOpacity = isDark ? BG_OPACITY_DEFAULT_DARK : BG_OPACITY_DEFAULT_LIGHT; - const width = fill ? "100%" : `${Math.max(pct ?? 1, 1)}%`; - const style: CSSProperties = { width, borderColor: resolvedBorder, - backgroundColor: addOpacityToHex(color, defaultOpacity), + backgroundColor: addOpacityToHex(solidColor, defaultOpacity), // Active state: full color, slightly darker border, glow ring. - ["--cv-bar-active-bg" as string]: color, - ["--cv-bar-active-border" as string]: borderColor ?? color, - ["--cv-bar-active-ring" as string]: `0 0 0 ${ACTIVE_RING_PX}px ${addOpacityToHex(color, ACTIVE_RING_OPACITY)}`, + ["--cv-bar-active-bg" as string]: solidColor, + ["--cv-bar-active-border" as string]: borderColor ?? solidColor, + ["--cv-bar-active-ring" as string]: `0 0 0 ${ACTIVE_RING_PX}px ${addOpacityToHex(solidColor, ACTIVE_RING_OPACITY)}`, }; return ( diff --git a/front_end/src/app/(main)/midterms-2026/constants.ts b/front_end/src/app/(main)/midterms-2026/constants.ts index ecbac5d8f4..19a1ebda2d 100644 --- a/front_end/src/app/(main)/midterms-2026/constants.ts +++ b/front_end/src/app/(main)/midterms-2026/constants.ts @@ -5,10 +5,6 @@ export const MIDTERMS_COLORS = { demBorder: "#3A4DD0", repPrimary: "#E8827A", repBorder: "#C53B33", - // Split-control outcomes (Rep Senate / Dem House and vice versa). Pink - // so the bars don't read as either party. - splitPrimary: "#E879A6", - splitBorder: "#BE3F7E", // Continuous-gradient anchors. `tossUp` is the neutral midpoint at 50%, // `likelyR` and `likelyD` are the extremes; intermediate colors are // generated via getColorInSpectrum. From b6e6095b9b4d658033f27624b247695b698c7178 Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 14 May 2026 13:15:58 +0200 Subject: [PATCH 10/18] fix: uncontested map tiles match page background MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In dark mode the uncontested state fill was #2d3845 — visibly lighter than the page body (bg-blue-50-dark = #22262b), which made uncontested states stand out instead of receding. Drop the uncontested fill to the page bg color so the tiles read as "cutouts" of the SectionCard down to the page below. Hover variants shift to the previous resting shades so hover feedback stays visible. Light mode was already at #eff4f4 (= bg-blue-200); only the dark tokens move. Co-Authored-By: Claude Opus 4.7 (1M context) --- front_end/src/app/(main)/midterms-2026/constants.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/front_end/src/app/(main)/midterms-2026/constants.ts b/front_end/src/app/(main)/midterms-2026/constants.ts index 19a1ebda2d..e33b2c6c21 100644 --- a/front_end/src/app/(main)/midterms-2026/constants.ts +++ b/front_end/src/app/(main)/midterms-2026/constants.ts @@ -16,12 +16,14 @@ export const MIDTERMS_COLORS = { // with the surrounding card. cardBgLight: "#ffffff", cardBgDark: "#262f38", - // Uncontested fill is one elevation step lighter than the card bg so - // uncontested states are subtly visible without dominating. + // Uncontested fill matches the page background (blue-200 in light, + // blue-50-dark in dark) so uncontested states read as transparent + // "cutouts" of the SectionCard down to the page below. Hover variants + // are a small step away so hover feedback stays visible. uncontestedLight: "#eff4f4", - uncontestedDark: "#2d3845", + uncontestedDark: "#22262b", uncontestedHoverLight: "#e0e9eb", - uncontestedHoverDark: "#394450", + uncontestedHoverDark: "#2d3845", } as const; export const STATE_NAMES: Record = { From 62f1e885b6add587ade044c25f4e0d3a24f50ede Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 14 May 2026 13:20:08 +0200 Subject: [PATCH 11/18] =?UTF-8?q?feat:=20horizontal=20rep=E2=86=92dem=20gr?= =?UTF-8?q?adient=20for=20split-control=20bars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the SVG dashed-border approach and use a CSS linear gradient instead. Split-control rows (Rep Senate / Dem House and Dem Senate / Rep House) now render as a single bar whose fill transitions from the rep color on the left to the dem color on the right, with a matching darker gradient border. - cv_bar: replace `alternatingColors` with `gradientColors: [GradientColorStop, GradientColorStop]` where each stop carries its own fill + border hex. Implemented via the padding-box / border-box dual-gradient trick so a rounded border can hold its own color gradient. Two gradient values (rest + active) live on CSS variables; the active triggers (group-hover/cv, group-hover/cr, group-data-[open]/cr, data-[active]) swap which one the `background` shorthand reads, alongside the existing glow ring. - congress outcome: rename the discriminated union variant from `alternating` to `gradient`; RD and DR rows pass the rep + dem color stops (rep first → left edge). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/congress_outcome_card.tsx | 30 ++--- .../midterms-2026/components/cv_bar.tsx | 104 +++++++++--------- 2 files changed, 66 insertions(+), 68 deletions(-) diff --git a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx index 3ac772e380..1a023156c2 100644 --- a/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/congress_outcome_card.tsx @@ -16,20 +16,21 @@ const OUTCOME_OPTION_LABEL: Record = { DD: "Dem Senate / Dem House", }; -// Split outcomes (Rep Senate / Dem House and vice versa) use an alternating -// dem+rep dashed border instead of a single-color fill so neither party -// reads as the "owner" of the bar. -const SPLIT_COLORS: [string, string] = [ - MIDTERMS_COLORS.demPrimary, - MIDTERMS_COLORS.repPrimary, -]; +// Split outcomes (Rep Senate / Dem House and vice versa) use a horizontal +// gradient from the rep color (left) to the dem color (right) — including +// a matching gradient border — so neither party reads as the "owner" of +// the bar. +const SPLIT_GRADIENT = [ + { fill: MIDTERMS_COLORS.repPrimary, border: MIDTERMS_COLORS.repBorder }, + { fill: MIDTERMS_COLORS.demPrimary, border: MIDTERMS_COLORS.demBorder }, +] as const; type Outcome = { key: OutcomeKey; pct: number | null; } & ( | { kind: "solid"; color: string; borderColor: string } - | { kind: "alternating" } + | { kind: "gradient" } ); type Props = { @@ -57,7 +58,7 @@ export default async function CongressOutcomeCard({ post }: Props) { }; }; - const buildAlternating = (key: OutcomeKey): Outcome => { + const buildGradient = (key: OutcomeKey): Outcome => { const prob = getMultipleChoiceOptionProbability( post, OUTCOME_OPTION_LABEL[key] @@ -65,14 +66,14 @@ export default async function CongressOutcomeCard({ post }: Props) { return { key, pct: prob != null ? Math.round(prob * 1000) / 10 : null, - kind: "alternating", + kind: "gradient", }; }; const outcomes: Outcome[] = [ buildSolid("RR", MIDTERMS_COLORS.repPrimary, MIDTERMS_COLORS.repBorder), - buildAlternating("RD"), - buildAlternating("DR"), + buildGradient("RD"), + buildGradient("DR"), buildSolid("DD", MIDTERMS_COLORS.demPrimary, MIDTERMS_COLORS.demBorder), ]; @@ -129,7 +130,10 @@ function OutcomeRow({ borderColor={outcome.borderColor} /> ) : ( - + )} {outcome.pct != null ? `${outcome.pct.toFixed(1)}%` : "—"} diff --git a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx index 42968ab8a5..d89beceba8 100644 --- a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx @@ -6,6 +6,11 @@ import useAppTheme from "@/hooks/use_app_theme"; import cn from "@/utils/core/cn"; import { addOpacityToHex } from "@/utils/core/colors"; +export type GradientColorStop = { + fill: string; + border: string; +}; + type Props = { /** 0-100 — how much of the parent track this bar fills. Ignored when * `fill` is true. */ @@ -15,16 +20,17 @@ type Props = { * exactly tile their container without gap overflow. */ fill?: boolean; /** Solid hex color (e.g. #6B7AE8). Drives the filled bg. Ignored when - * `alternatingColors` is provided. */ + * `gradientColors` is provided. */ color?: string; /** Border color override for light mode. Defaults to `color`. Use a * darker shade for sharp contrast against the soft fill. */ borderColor?: string; - /** When provided, the bar renders with a transparent fill and a - * multi-colored dashed border alternating between the two colors. - * Used for split-control outcomes where neither party "owns" the - * bar. Overrides `color` / `borderColor`. */ - alternatingColors?: [string, string]; + /** When provided, the bar renders with a horizontal gradient fill and + * matching gradient border, transitioning from the first color stop + * on the left to the second on the right. Used for split-control + * outcomes where neither party "owns" the bar. Overrides `color` / + * `borderColor`. */ + gradientColors?: [GradientColorStop, GradientColorStop]; /** Tailwind height class (default `h-5`). */ heightClassName?: string; /** Force the active (hover-like) styling regardless of cursor position. @@ -40,9 +46,6 @@ const BG_OPACITY_DEFAULT_DARK = 0.55; // Glow ring radius (px) used on active state. const ACTIVE_RING_PX = 3; const ACTIVE_RING_OPACITY = 0.45; -// Alternating-bar stroke + dash sizing. -const ALT_STROKE_WIDTH = 3; // half visually clipped inside, ~1.5px visible -const ALT_DASH = 8; /** * Visual primitive shaped like the consumer view multiple-choice bar: @@ -62,16 +65,16 @@ const ALT_DASH = 8; * at rest, so we fall back to the primary `color` (which is a brighter * shade) for the resting border instead. * - * When `alternatingColors` is provided, the bar is rendered as inline - * SVG with two stacked rect outlines whose dashes are offset by one - * dash length, producing a continuous alternating dashed border. + * When `gradientColors` is provided, the bar renders with a horizontal + * `linear-gradient` fill + matching gradient border. The active state + * swaps the gradient to its full-opacity variant via a CSS variable. */ const CvBar: FC = ({ pct, fill, color, borderColor, - alternatingColors, + gradientColors, heightClassName = "h-5", active, className, @@ -81,55 +84,46 @@ const CvBar: FC = ({ const width = fill ? "100%" : `${Math.max(pct ?? 1, 1)}%`; - if (alternatingColors) { - const [colorA, colorB] = alternatingColors; + if (gradientColors) { + const [from, to] = gradientColors; + const defaultOpacity = isDark + ? BG_OPACITY_DEFAULT_DARK + : BG_OPACITY_DEFAULT_LIGHT; + // Same border-color rule as solid bars: in dark mode the darker + // `border` shade blends with the card, so use the brighter `fill`. + const restBorderFrom = isDark ? from.fill : from.border; + const restBorderTo = isDark ? to.fill : to.border; + + const restFill = `linear-gradient(to right, ${addOpacityToHex(from.fill, defaultOpacity)}, ${addOpacityToHex(to.fill, defaultOpacity)}) padding-box, linear-gradient(to right, ${restBorderFrom}, ${restBorderTo}) border-box`; + const activeFill = `linear-gradient(to right, ${from.fill}, ${to.fill}) padding-box, linear-gradient(to right, ${from.border}, ${to.border}) border-box`; + + // Glow ring uses the from-color (left edge) — single source so the + // ring reads cleanly. It's subtle either way. + const ringColor = from.fill; + + const style: CSSProperties = { + width, + background: "var(--cv-bar-bg, var(--cv-bar-bg-rest))", + ["--cv-bar-bg-rest" as string]: restFill, + ["--cv-bar-bg-active" as string]: activeFill, + ["--cv-bar-active-ring" as string]: `0 0 0 ${ACTIVE_RING_PX}px ${addOpacityToHex(ringColor, ACTIVE_RING_OPACITY)}`, + }; + return (
- -
+ style={style} + /> ); } From 6e763b941dd8067ee57c9326baab960808795093 Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 14 May 2026 13:27:52 +0200 Subject: [PATCH 12/18] fix: gradient bar fill now uses same opacity as solid bars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The padding-box/border-box dual-gradient trick painted the full- opacity border gradient behind the semi-transparent fill, so the fill composited over the border gradient and read as a much darker bar than its solid neighbors. Switch to a layered approach: the bar element itself paints the semi-transparent gradient fill directly (no underlying full-opacity layer), and a child overlay paints the gradient border into a 1px ring via a mask-composite trick — so the border lives only in the border region and never bleeds into the fill area. The Rep Senate / Dem House and Dem Senate / Rep House bars now read at the same lightness as the solid red and blue bars on either side. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../midterms-2026/components/cv_bar.tsx | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx index d89beceba8..b4cb720e31 100644 --- a/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/cv_bar.tsx @@ -94,18 +94,26 @@ const CvBar: FC = ({ const restBorderFrom = isDark ? from.fill : from.border; const restBorderTo = isDark ? to.fill : to.border; - const restFill = `linear-gradient(to right, ${addOpacityToHex(from.fill, defaultOpacity)}, ${addOpacityToHex(to.fill, defaultOpacity)}) padding-box, linear-gradient(to right, ${restBorderFrom}, ${restBorderTo}) border-box`; - const activeFill = `linear-gradient(to right, ${from.fill}, ${to.fill}) padding-box, linear-gradient(to right, ${from.border}, ${to.border}) border-box`; + // Fill = semi-transparent gradient (same opacity scheme as solid bars). + const fillRest = `linear-gradient(to right, ${addOpacityToHex(from.fill, defaultOpacity)}, ${addOpacityToHex(to.fill, defaultOpacity)})`; + const fillActive = `linear-gradient(to right, ${from.fill}, ${to.fill})`; + // Border = full-opacity gradient, painted only inside the 1px ring + // via a mask-composite trick on the overlay child so it doesn't + // bleed through and darken the semi-transparent fill. + const borderRest = `linear-gradient(to right, ${restBorderFrom}, ${restBorderTo})`; + const borderActive = `linear-gradient(to right, ${from.border}, ${to.border})`; // Glow ring uses the from-color (left edge) — single source so the - // ring reads cleanly. It's subtle either way. + // ring reads cleanly. const ringColor = from.fill; const style: CSSProperties = { width, - background: "var(--cv-bar-bg, var(--cv-bar-bg-rest))", - ["--cv-bar-bg-rest" as string]: restFill, - ["--cv-bar-bg-active" as string]: activeFill, + background: "var(--cv-bar-fill)", + ["--cv-bar-fill" as string]: fillRest, + ["--cv-bar-fill-active" as string]: fillActive, + ["--cv-bar-border" as string]: borderRest, + ["--cv-bar-border-active" as string]: borderActive, ["--cv-bar-active-ring" as string]: `0 0 0 ${ACTIVE_RING_PX}px ${addOpacityToHex(ringColor, ACTIVE_RING_OPACITY)}`, }; @@ -113,17 +121,36 @@ const CvBar: FC = ({
+ > + {/* Gradient-border overlay. Inner div is sized to the full bar via + inset:0 with 1px padding; the mask-composite trick paints the + border gradient only in that 1px padding ring, leaving the + interior transparent so the semi-transparent fill on the + parent composites directly with the card bg (not with this + full-opacity border gradient). */} + ); } From 6679fd7668af18530b22f0b849d61647b01c3b0f Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 14 May 2026 13:36:56 +0200 Subject: [PATCH 13/18] Revert "fix: uncontested map tiles match page background" This reverts commit b6e6095b9b4d658033f27624b247695b698c7178. --- front_end/src/app/(main)/midterms-2026/constants.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/front_end/src/app/(main)/midterms-2026/constants.ts b/front_end/src/app/(main)/midterms-2026/constants.ts index e33b2c6c21..19a1ebda2d 100644 --- a/front_end/src/app/(main)/midterms-2026/constants.ts +++ b/front_end/src/app/(main)/midterms-2026/constants.ts @@ -16,14 +16,12 @@ export const MIDTERMS_COLORS = { // with the surrounding card. cardBgLight: "#ffffff", cardBgDark: "#262f38", - // Uncontested fill matches the page background (blue-200 in light, - // blue-50-dark in dark) so uncontested states read as transparent - // "cutouts" of the SectionCard down to the page below. Hover variants - // are a small step away so hover feedback stays visible. + // Uncontested fill is one elevation step lighter than the card bg so + // uncontested states are subtly visible without dominating. uncontestedLight: "#eff4f4", - uncontestedDark: "#22262b", + uncontestedDark: "#2d3845", uncontestedHoverLight: "#e0e9eb", - uncontestedHoverDark: "#2d3845", + uncontestedHoverDark: "#394450", } as const; export const STATE_NAMES: Record = { From 2fabb4a36fb113b8d8ef910cb148a251a5328e69 Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Thu, 14 May 2026 14:29:44 +0200 Subject: [PATCH 14/18] =?UTF-8?q?fix:=20PR=20feedback=20=E2=80=94=20a11y?= =?UTF-8?q?=20label=20on=20map=20states=20+=20escape=20OG=20theme=20param?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - geographic_map: contested state paths previously exposed role="button" and tabIndex=0 with no accessible name. Add an aria-label derived from STATE_NAMES[abbr] (falling back to the abbreviation) so screen readers announce the state, and aria-haspopup="dialog" so users know activation opens the forecast question rather than mutating the current view. - og route: encodeURIComponent the user-controlled `theme` query param when interpolating it back into the screenshot pageUrl, so special characters can't break out of the query slot or inject additional params. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/(main)/midterms-2026/components/geographic_map.tsx | 5 ++++- front_end/src/app/og/midterms-2026/route/route.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx b/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx index e237203f35..bf2bcfe3fb 100644 --- a/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/geographic_map.tsx @@ -21,7 +21,7 @@ import { import useAppTheme from "@/hooks/use_app_theme"; -import { MIDTERMS_COLORS } from "../constants"; +import { MIDTERMS_COLORS, STATE_NAMES } from "../constants"; import MapLegend from "./map_legend"; import MapTooltipPortal from "./map_tooltip_portal"; import StateTooltipContent from "./state_tooltip"; @@ -223,10 +223,13 @@ const GeographicMap: FC = ({ races, tabsSlot }) => { // react-simple-maps' hardcodes tabIndex="0" on // every path. For uncontested states we override with -1 so // they don't enter the tab order, since they have no action. + const stateName = abbr ? STATE_NAMES[abbr] ?? abbr : ""; const interactiveProps = isContested ? { tabIndex: 0, role: "button", + "aria-label": `${stateName} — view forecast question`, + "aria-haspopup": "dialog" as const, onMouseEnter: (e: MouseEvent) => abbr && handleEnter(abbr, e), onMouseLeave: handleLeave, diff --git a/front_end/src/app/og/midterms-2026/route/route.ts b/front_end/src/app/og/midterms-2026/route/route.ts index 85bc702222..d9526092e8 100644 --- a/front_end/src/app/og/midterms-2026/route/route.ts +++ b/front_end/src/app/og/midterms-2026/route/route.ts @@ -5,7 +5,9 @@ import { getPublicSettings } from "@/utils/public_settings.server"; export async function GET(req: NextRequest) { const theme = req.nextUrl.searchParams.get("theme") ?? "dark"; const { PUBLIC_APP_URL } = getPublicSettings(); - const pageUrl = `${PUBLIC_APP_URL}/og/midterms-2026?theme=${theme}&non-interactive=true`; + // Encode the user-controlled theme value so special characters can't + // break out of the query slot or inject additional params. + const pageUrl = `${PUBLIC_APP_URL}/og/midterms-2026?theme=${encodeURIComponent(theme)}&non-interactive=true`; const screenshotServiceUrl = process.env.SCREENSHOT_SERVICE_API_URL; const screenshotApiKey = process.env.SCREENSHOT_SERVICE_API_KEY; From ef0feec3a5d64325aa8147030c7f69587197c300 Mon Sep 17 00:00:00 2001 From: Atakan Seckin Date: Fri, 15 May 2026 12:39:11 +0200 Subject: [PATCH 15/18] feat: round-6 midterms polish + PR a11y feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI/UX - party_icons: replace stylised donkey/elephant placeholders with the user-supplied official-style SVGs; viewBox 0 0 562 496; currentColor tinting preserved. - cv_bar: introduce a `ThemedColor = string | { light, dark }` union so every color prop can carry mode-specific values, resolved at render via useAppTheme. New hover spec replaces the full-saturation + glow ring with `border-width 1→2px` + additive `+0.2` fill opacity (cap 1.0). Drops the box-shadow ring on both solid and gradient bars; gradient bar's mask-ring padding doubles via a CSS variable. - constants: add dem/rep dark-mode color variants (demPrimaryDark/demBorderDark/repPrimaryDark/repBorderDark) and a tileTextDark token for the mobile tile labels. - consumers (chamber_control_card, congress_outcome_card, consequence_grid): pass ThemedColor pairs for fill + border so dark mode picks up the brighter shades. - electoral_consequences: strip the inner table chrome (border, bg-blue-100, padding) so the rows blend with the SectionCard. Move title + description into col 1 of the header grid; party header cards now sit in cols 2 & 3, matching the reference layout naturally with no negative margins. - consequence_grid: column hover now fires anywhere in the cell footprint — gap dropped in favor of `md:px-2` inside each BarCell, outer div stretches `h-full w-full` so the entire bounding box is the trigger. - community_insights: mobile (< lg) now uses the labor-hub MobileCarousel (Embla snap-to-center, peek-next-card, dot indicators below). Desktop (lg+) keeps the chevron-driven InsightsCarousel with edge gradients. insight_card no longer enforces its own w-[340px]; the desktop carousel wraps each card in a sizing div, and the mobile carousel slides do their own flex-[0_0_92%]. - tile_map: dark-mode contested-tile text switches to MIDTERMS_COLORS.tileTextDark (deep navy) so the labels stay legible against the pastel fills. - hero: split the subtitle into desktop + mobile variants. Desktop reads "Forecasts from the Metaculus community on contested races and the policies they will shape." Mobile reads "Real-time forecasts from the Metaculus community." - footer: drop the metaculus.com link and the realtime line; keep only the disclaimer, updated to "Not affiliated with any political party." PR feedback - chamber_row_tooltip: localise the Close button aria-label via `useTranslations`. The `close` key already exists in all six locale files. i18n - Add midtermsHubHeroSubtitleMobile; update midtermsHubHeroSubtitle and midtermsHubFooterDisclaimer; remove the now-unused midtermsHubForecastsRealtime key across en/es/cs/pt/zh/zh-TW. Co-Authored-By: Claude Opus 4.7 (1M context) --- front_end/messages/cs.json | 6 +- front_end/messages/en.json | 6 +- front_end/messages/es.json | 6 +- front_end/messages/pt.json | 6 +- front_end/messages/zh-TW.json | 6 +- front_end/messages/zh.json | 6 +- .../components/chamber_control_card.tsx | 31 ++-- .../components/chamber_row_tooltip.tsx | 4 +- .../components/congress_outcome_card.tsx | 43 +++-- .../components/consequence_grid.tsx | 104 ++++++++---- .../midterms-2026/components/cv_bar.tsx | 151 +++++++++--------- .../midterms-2026/components/insight_card.tsx | 2 +- .../components/insights_carousel.tsx | 4 +- .../midterms-2026/components/party_icons.tsx | 13 +- .../midterms-2026/components/tile_map.tsx | 5 + .../src/app/(main)/midterms-2026/constants.ts | 10 ++ .../sections/community_insights.tsx | 31 +++- .../sections/electoral_consequences.tsx | 14 +- .../(main)/midterms-2026/sections/footer.tsx | 14 +- .../(main)/midterms-2026/sections/hero.tsx | 7 +- 20 files changed, 291 insertions(+), 178 deletions(-) diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json index 89938c6d23..cde1772dc4 100644 --- a/front_end/messages/cs.json +++ b/front_end/messages/cs.json @@ -2209,7 +2209,6 @@ "midtermsHubChamberTooltipDisclaimer": "Klikněte pro zobrazení předpovědní otázky", "midtermsHubPartyDemocrats": "Demokraté", "midtermsHubPartyRepublicans": "Republikáni", - "midtermsHubForecastsRealtime": "Předpovědi se aktualizují v reálném čase.", "midtermsHubChamberSenate": "Senát", "midtermsHubChamberHouse": "Sněmovna reprezentantů", "midtermsHubChamberGovernor": "Guvernér", @@ -2246,10 +2245,11 @@ "midtermsHubDemPct": "{pct}% demokraté", "midtermsHubRepPct": "{pct}% republikáni", "midtermsHubNoForecast": "Bez předpovědi", - "midtermsHubFooterDisclaimer": "Předpovědi komunity Metaculus. Bez vazby na jakoukoli politickou stranu.", + "midtermsHubFooterDisclaimer": "Bez vazby na jakoukoli politickou stranu.", "midtermsHubHeroTitleLine1": "Mezičasové volby", "midtermsHubHeroTitleLine2": "USA 2026", - "midtermsHubHeroSubtitle": "Předpovědi komunity Metaculus v reálném čase k mezičasovým volbám USA v roce 2026.", + "midtermsHubHeroSubtitle": "Předpovědi komunity Metaculus o sporných závodech a politikách, které utvářejí.", + "midtermsHubHeroSubtitleMobile": "Předpovědi komunity Metaculus v reálném čase.", "midtermsHubThingsToWatchSubtitle": "Ukazatele, které stojí za sledování v průběhu cyklu.", "midtermsHubConsequencesSubtitle": "Jak se předpovídané politické výsledky mění v závislosti na tom, která strana ovládá Kongres.", "midtermsHubUpdatedRealtime": "Aktualizováno v reálném čase", diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 5699490b39..416ed37eae 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2203,7 +2203,6 @@ "midtermsHubChamberTooltipDisclaimer": "Click to view forecasting question", "midtermsHubPartyDemocrats": "Democrats", "midtermsHubPartyRepublicans": "Republicans", - "midtermsHubForecastsRealtime": "Forecasts are updated real-time.", "midtermsHubOutcomeRepRep": "Rep Senate / Rep House", "midtermsHubOutcomeRepDem": "Rep Senate / Dem House", "midtermsHubOutcomeDemRep": "Dem Senate / Rep House", @@ -2235,10 +2234,11 @@ "midtermsHubDemPct": "{pct}% Democrat", "midtermsHubRepPct": "{pct}% Republican", "midtermsHubNoForecast": "No forecast", - "midtermsHubFooterDisclaimer": "Forecasts by the Metaculus community. Not affiliated with any political party.", + "midtermsHubFooterDisclaimer": "Not affiliated with any political party.", "midtermsHubHeroTitleLine1": "2026 US", "midtermsHubHeroTitleLine2": "Midterm Elections", - "midtermsHubHeroSubtitle": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubHeroSubtitle": "Forecasts from the Metaculus community on contested races and the policies they will shape.", + "midtermsHubHeroSubtitleMobile": "Real-time forecasts from the Metaculus community.", "midtermsHubThingsToWatchSubtitle": "Indicators worth watching as the cycle unfolds.", "midtermsHubConsequencesSubtitle": "How forecasted policy outcomes shift depending on which party holds Congress.", "midtermsHubUpdatedRealtime": "Updated real-time", diff --git a/front_end/messages/es.json b/front_end/messages/es.json index 7188a476df..1244777892 100644 --- a/front_end/messages/es.json +++ b/front_end/messages/es.json @@ -2209,7 +2209,6 @@ "midtermsHubChamberTooltipDisclaimer": "Haz clic para ver la pregunta de pronóstico", "midtermsHubPartyDemocrats": "Los demócratas", "midtermsHubPartyRepublicans": "Los republicanos", - "midtermsHubForecastsRealtime": "Los pronósticos se actualizan en tiempo real.", "midtermsHubChamberSenate": "Senado", "midtermsHubChamberHouse": "Cámara de Representantes", "midtermsHubChamberGovernor": "Gobernador", @@ -2246,10 +2245,11 @@ "midtermsHubDemPct": "{pct}% Demócrata", "midtermsHubRepPct": "{pct}% Republicano", "midtermsHubNoForecast": "Sin pronóstico", - "midtermsHubFooterDisclaimer": "Pronósticos de la comunidad de Metaculus. No afiliados a ningún partido político.", + "midtermsHubFooterDisclaimer": "No afiliado a ningún partido político.", "midtermsHubHeroTitleLine1": "EE. UU. 2026", "midtermsHubHeroTitleLine2": "Elecciones intermedias", - "midtermsHubHeroSubtitle": "Pronósticos en tiempo real de la comunidad de Metaculus sobre las elecciones intermedias de EE. UU. de 2026.", + "midtermsHubHeroSubtitle": "Pronósticos de la comunidad de Metaculus sobre las contiendas disputadas y las políticas que moldearán.", + "midtermsHubHeroSubtitleMobile": "Pronósticos en tiempo real de la comunidad de Metaculus.", "midtermsHubThingsToWatchSubtitle": "Indicadores que vale la pena observar a medida que avanza el ciclo.", "midtermsHubConsequencesSubtitle": "Cómo cambian los resultados políticos previstos según el partido que controle el Congreso.", "midtermsHubUpdatedRealtime": "Actualizado en tiempo real", diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json index 22e50aea40..e0aab97dd2 100644 --- a/front_end/messages/pt.json +++ b/front_end/messages/pt.json @@ -2207,7 +2207,6 @@ "midtermsHubChamberTooltipDisclaimer": "Clique para ver a pergunta de previsão", "midtermsHubPartyDemocrats": "Os democratas", "midtermsHubPartyRepublicans": "Os republicanos", - "midtermsHubForecastsRealtime": "As previsões são atualizadas em tempo real.", "midtermsHubChamberSenate": "Senado", "midtermsHubChamberHouse": "Câmara dos Representantes", "midtermsHubChamberGovernor": "Governador", @@ -2244,10 +2243,11 @@ "midtermsHubDemPct": "{pct}% Democrata", "midtermsHubRepPct": "{pct}% Republicano", "midtermsHubNoForecast": "Sem previsão", - "midtermsHubFooterDisclaimer": "Previsões pela comunidade Metaculus. Não afiliada a nenhum partido político.", + "midtermsHubFooterDisclaimer": "Não afiliado a nenhum partido político.", "midtermsHubHeroTitleLine1": "Eleições de Meio", "midtermsHubHeroTitleLine2": "de Mandato dos EUA 2026", - "midtermsHubHeroSubtitle": "Previsões em tempo real da comunidade Metaculus sobre as eleições de meio de mandato dos EUA de 2026.", + "midtermsHubHeroSubtitle": "Previsões da comunidade Metaculus sobre as disputas contestadas e as políticas que moldarão.", + "midtermsHubHeroSubtitleMobile": "Previsões em tempo real da comunidade Metaculus.", "midtermsHubThingsToWatchSubtitle": "Indicadores que vale a pena acompanhar à medida que o ciclo avança.", "midtermsHubConsequencesSubtitle": "Como os resultados políticos previstos mudam dependendo de qual partido controla o Congresso.", "midtermsHubUpdatedRealtime": "Atualizado em tempo real", diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json index 4e31d9a226..0f17a21b4b 100644 --- a/front_end/messages/zh-TW.json +++ b/front_end/messages/zh-TW.json @@ -2206,7 +2206,6 @@ "midtermsHubChamberTooltipDisclaimer": "點擊查看預測問題", "midtermsHubPartyDemocrats": "民主黨", "midtermsHubPartyRepublicans": "共和黨", - "midtermsHubForecastsRealtime": "預測即時更新。", "midtermsHubChamberSenate": "參議院", "midtermsHubChamberHouse": "眾議院", "midtermsHubChamberGovernor": "州長", @@ -2243,10 +2242,11 @@ "midtermsHubDemPct": "{pct}% 民主黨", "midtermsHubRepPct": "{pct}% 共和黨", "midtermsHubNoForecast": "無預測", - "midtermsHubFooterDisclaimer": "由 Metaculus 社群進行的預測。不隸屬於任何政黨。", + "midtermsHubFooterDisclaimer": "不隸屬於任何政黨。", "midtermsHubHeroTitleLine1": "2026 年美國", "midtermsHubHeroTitleLine2": "期中選舉", - "midtermsHubHeroSubtitle": "Metaculus 社群對 2026 年美國期中選舉的即時預測。", + "midtermsHubHeroSubtitle": "Metaculus 社群對競爭激烈的選舉以及由此形塑的政策的預測。", + "midtermsHubHeroSubtitleMobile": "Metaculus 社群的即時預測。", "midtermsHubThingsToWatchSubtitle": "選舉週期推進過程中值得關注的指標。", "midtermsHubConsequencesSubtitle": "預測的政策結果如何根據掌握國會的政黨而變化。", "midtermsHubUpdatedRealtime": "即時更新", diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json index bac0272d6a..c57f943a80 100644 --- a/front_end/messages/zh.json +++ b/front_end/messages/zh.json @@ -2211,7 +2211,6 @@ "midtermsHubChamberTooltipDisclaimer": "点击查看预测问题", "midtermsHubPartyDemocrats": "民主党", "midtermsHubPartyRepublicans": "共和党", - "midtermsHubForecastsRealtime": "预测实时更新。", "midtermsHubChamberSenate": "参议院", "midtermsHubChamberHouse": "众议院", "midtermsHubChamberGovernor": "州长", @@ -2248,10 +2247,11 @@ "midtermsHubDemPct": "{pct}% 民主党", "midtermsHubRepPct": "{pct}% 共和党", "midtermsHubNoForecast": "无预测", - "midtermsHubFooterDisclaimer": "由 Metaculus 社区进行的预测。不隶属于任何政党。", + "midtermsHubFooterDisclaimer": "不隶属于任何政党。", "midtermsHubHeroTitleLine1": "2026 年美国", "midtermsHubHeroTitleLine2": "中期选举", - "midtermsHubHeroSubtitle": "Metaculus 社区对 2026 年美国中期选举的实时预测。", + "midtermsHubHeroSubtitle": "Metaculus 社区对竞争激烈的选举以及由此塑造的政策的预测。", + "midtermsHubHeroSubtitleMobile": "Metaculus 社区的实时预测。", "midtermsHubThingsToWatchSubtitle": "选举周期推进过程中值得关注的指标。", "midtermsHubConsequencesSubtitle": "预测的政策结果如何根据控制国会的政党而变化。", "midtermsHubUpdatedRealtime": "实时更新", diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx index ccd1bcdb93..df9030c114 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_control_card.tsx @@ -6,10 +6,27 @@ import cn from "@/utils/core/cn"; import ChamberRowTooltip from "./chamber_row_tooltip"; import { MIDTERMS_COLORS } from "../constants"; -import CvBar from "./cv_bar"; +import CvBar, { ThemedColor } from "./cv_bar"; import { ChamberData } from "../helpers/fetch_dashboard_data"; import { getMultipleChoiceOptionProbability } from "../helpers/post_utils"; +const DEM_FILL: ThemedColor = { + light: MIDTERMS_COLORS.demPrimary, + dark: MIDTERMS_COLORS.demPrimaryDark, +}; +const DEM_BORDER: ThemedColor = { + light: MIDTERMS_COLORS.demBorder, + dark: MIDTERMS_COLORS.demBorderDark, +}; +const REP_FILL: ThemedColor = { + light: MIDTERMS_COLORS.repPrimary, + dark: MIDTERMS_COLORS.repPrimaryDark, +}; +const REP_BORDER: ThemedColor = { + light: MIDTERMS_COLORS.repBorder, + dark: MIDTERMS_COLORS.repBorderDark, +}; + const CURRENT_SENATE = { dem: 47, rep: 53 }; const CURRENT_HOUSE = { dem: 215, rep: 220 }; const SENATE_TOTAL = 100; @@ -207,16 +224,8 @@ function ChamberRow({ className="grid w-full items-center gap-1" style={{ gridTemplateColumns: `${demShare}fr ${repShare}fr` }} > - - + +
)}
diff --git a/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx b/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx index d7c027a84a..4fab6cc1bb 100644 --- a/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx +++ b/front_end/src/app/(main)/midterms-2026/components/chamber_row_tooltip.tsx @@ -1,5 +1,6 @@ "use client"; +import { useTranslations } from "next-intl"; import { FC, MouseEvent as RMouseEvent, @@ -45,6 +46,7 @@ const getHoverNoneSnapshot = () => window.matchMedia("(hover: none)").matches; const getHoverNoneServerSnapshot = () => false; const ChamberRowTooltip: FC = ({ body, disclaimer, href, children }) => { + const t = useTranslations(); const [open, setOpen] = useState(false); const isTouch = useSyncExternalStore( subscribeHoverMQ, @@ -106,7 +108,7 @@ const ChamberRowTooltip: FC = ({ body, disclaimer, href, children }) => { {open && isTouch && (