Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions front_end/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1847,10 +1847,10 @@
"hostPrivateInstances": "Hostujte soukromé instance",
"hostPrivateInstancesDescription": "Objevte poznatky z vaší organizace",
"whatsMetaculus": "Co je Metaculus?",
"metaculusDescription": "Metaculus je online platforma pro předpovídání a agregační nástroj, který pracuje na zlepšení lidského uvažování a koordinace v tématech globálního významu.",
"metaculusDescription": "Metaculus je online platforma pro předpovídání zaměřená na témata globálního významu.",
"openQuestions": "Otevřené otázky",
"forecastsSubmitted": "Odeslaných předpovědí",
"yearsOfPrediction": "Let předpovídání",
"yearsOfPrediction": "let předpovědí",
"featuredIn": "Zmíněno v",
"popular": "Populární",
"exploreAll": "Prozkoumat vše",
Expand Down
8 changes: 4 additions & 4 deletions front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"exploreAll": "Explore all",
"thousandsOfOpenQuestions": "20,000+ open questions",
"whatsMetaculus": "What's Metaculus?",
"metaculusDescription": "Metaculus is an online forecasting platform and aggregation engine working to improve human reasoning and coordination on topics of global importance.",
"openQuestions": "Open questions",
"forecastsSubmitted": "Forecasts submitted",
"yearsOfPrediction": "Years of prediction",
"metaculusDescription": "Metaculus is an online forecasting platform focusing on topics of global importance.",
"openQuestions": "open questions",
"forecastsSubmitted": "forecasts submitted",
"yearsOfPrediction": "years of predictions",
"featuredIn": "Featured in",
"hero1TopTitle": "Metaculus Platform",
"heroIndividualsTitle": "Make decisions based on trusted community forecasts",
Expand Down
4 changes: 2 additions & 2 deletions front_end/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1847,10 +1847,10 @@
"hostPrivateInstances": "Aloja instancias privadas",
"hostPrivateInstancesDescription": "Descubre información desde dentro de tu organización",
"whatsMetaculus": "¿Qué es Metaculus?",
"metaculusDescription": "Metaculus es una plataforma de pronósticos en línea y motor de agregación que trabaja para mejorar el razonamiento humano y la coordinación en temas de importancia global.",
"metaculusDescription": "Metaculus es una plataforma de pronósticos en línea enfocada en temas de importancia global.",
"openQuestions": "Preguntas abiertas",
"forecastsSubmitted": "Pronósticos enviados",
"yearsOfPrediction": "Años de predicción",
"yearsOfPrediction": "años de predicciones",
Comment on lines +1850 to +1853
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

head -n 1860 front_end/messages/es.json | tail -n 30 | cat -n

Repository: Metaculus/metaculus

Length of output: 2333


🏁 Script executed:

sed -n '1840,1860p' front_end/messages/es.json | cat -n

Repository: Metaculus/metaculus

Length of output: 1592


🏁 Script executed:

rg -n "forecastsSubmitted|openQuestions|yearsOfPrediction|Preguntas|Pronósticos" front_end/messages/es.json | head -20

Repository: Metaculus/metaculus

Length of output: 1709


Correct yearsOfPrediction casing and phrasing for consistency with adjacent stat labels.

Neighboring stat labels ("Preguntas abiertas", "Pronósticos enviados") start with capitalized nouns, but "años de predicciones" begins with lowercase. Additionally, the singular form "Años de predicción" is more idiomatic in Spanish for UI metric labels than the plural.

Suggested adjustment
-  "yearsOfPrediction": "años de predicciones",
+  "yearsOfPrediction": "Años de predicción",
📝 Committable suggestion

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

Suggested change
"metaculusDescription": "Metaculus es una plataforma de pronósticos en línea enfocada en temas de importancia global.",
"openQuestions": "Preguntas abiertas",
"forecastsSubmitted": "Pronósticos enviados",
"yearsOfPrediction": "Años de predicción",
"yearsOfPrediction": "años de predicciones",
"metaculusDescription": "Metaculus es una plataforma de pronósticos en línea enfocada en temas de importancia global.",
"openQuestions": "Preguntas abiertas",
"forecastsSubmitted": "Pronósticos enviados",
"yearsOfPrediction": "Años de predicción",
🤖 Prompt for AI Agents
In `@front_end/messages/es.json` around lines 1850 - 1853, Update the translation
value for the key yearsOfPrediction to match the capitalization and idiomatic
phrasing of neighboring stat labels: change the string to "Años de predicción"
(capitalized first letter and singular "predicción") so it reads consistently
with "Preguntas abiertas" and "Pronósticos enviados" in the es.json file.

"featuredIn": "Destacado en",
"popular": "Popular",
"exploreAll": "Explorar todo",
Expand Down
4 changes: 2 additions & 2 deletions front_end/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -1845,10 +1845,10 @@
"hostPrivateInstances": "Hospede instâncias privadas",
"hostPrivateInstancesDescription": "Descubra insights de dentro da sua organização",
"whatsMetaculus": "O que é Metaculus?",
"metaculusDescription": "Metaculus é uma plataforma de previsões online e motor de agregação que trabalha para melhorar o raciocínio humano e a coordenação em temas de importância global.",
"metaculusDescription": "Metaculus é uma plataforma de previsões online focada em temas de importância global.",
"openQuestions": "Perguntas abertas",
"forecastsSubmitted": "Previsões enviadas",
"yearsOfPrediction": "Anos de previsão",
"yearsOfPrediction": "anos de previsões",
"featuredIn": "Destaque em",
"popular": "Popular",
"exploreAll": "Explorar tudo",
Expand Down
4 changes: 2 additions & 2 deletions front_end/messages/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -1844,10 +1844,10 @@
"hostPrivateInstances": "託管私有實例",
"hostPrivateInstancesDescription": "從您的組織內部發掘見解",
"whatsMetaculus": "什麼是 Metaculus?",
"metaculusDescription": "Metaculus 是一個線上預測平台和聚合引擎,致力於改善人類在全球重要議題上的推理和協調能力。",
"metaculusDescription": "Metaculus 是一個專注於全球重要議題的線上預測平台。",
"openQuestions": "開放問題",
"forecastsSubmitted": "已提交預測",
"yearsOfPrediction": "預測年數",
"yearsOfPrediction": "年預測歷史",
"featuredIn": "媒體報導",
"popular": "熱門",
"exploreAll": "探索全部",
Expand Down
4 changes: 2 additions & 2 deletions front_end/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1849,10 +1849,10 @@
"hostPrivateInstances": "托管私有实例",
"hostPrivateInstancesDescription": "从您的组织内部发掘见解",
"whatsMetaculus": "什么是 Metaculus?",
"metaculusDescription": "Metaculus 是一个在线预测平台和聚合引擎,致力于改善人类在全球重要议题上的推理和协调能力。",
"metaculusDescription": "Metaculus 是一个专注于全球重要议题的在线预测平台。",
"openQuestions": "开放问题",
"forecastsSubmitted": "已提交预测",
"yearsOfPrediction": "预测年数",
"yearsOfPrediction": "年预测历史",
"featuredIn": "媒体报道",
"popular": "热门",
"exploreAll": "探索全部",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { PostsParams } from "@/services/api/posts/posts.shared";
import { PostForecastType } from "@/types/post";
import { QuestionType } from "@/types/question";

export type TabId = "popular" | "news" | "new";
export type TabId = "news" | "popular" | "new";

export const TABS: { id: TabId; label: string }[] = [
{ id: "popular", label: "Popular" },
{ id: "news", label: "In the news" },
{ id: "popular", label: "Popular" },
{ id: "new", label: "New" },
];

Expand Down
89 changes: 44 additions & 45 deletions front_end/src/app/(main)/(home)/components/homepage_forecasts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,54 @@ import Link from "next/link";
import { useTranslations } from "next-intl";
import { FC, useState, useTransition } from "react";

import PostCard from "@/components/post_card";
import { Tabs, TabsList, TabsTab } from "@/components/ui/tabs";
import { useBreakpoint } from "@/hooks/tailwind";
import ClientPostsApi from "@/services/api/posts/posts.client";
import { PostWithForecasts } from "@/types/post";
import cn from "@/utils/core/cn";

import { ExploreImagesGrid } from "./ExploreImagesGrid";
import { FILTERS, TABS, TabId } from "./homepage_filters";
import HomepagePostCard from "./homepage_post_card";

type Props = {
initialPopularPosts: PostWithForecasts[];
initialPosts: PostWithForecasts[];
className?: string;
};

const HomePageForecasts: FC<Props> = ({ initialPopularPosts, className }) => {
const HomePageForecasts: FC<Props> = ({ initialPosts, className }) => {
const t = useTranslations();
const [activeTab, setActiveTab] = useState<TabId>("popular");
const [posts, setPosts] = useState<PostWithForecasts[]>(initialPopularPosts);
const [activeTab, setActiveTab] = useState<TabId>("news");
const [posts, setPosts] = useState<PostWithForecasts[]>(initialPosts);
const [isPending, startTransition] = useTransition();
const [cachedPosts, setCachedPosts] = useState<
Partial<Record<TabId, PostWithForecasts[]>>
>({
popular: initialPopularPosts,
news: initialPosts,
});

const tabLabels: Record<TabId, string> = {
popular: t("popular"),
news: t("inTheNews"),
popular: t("popular"),
new: t("new"),
};

const handleTabChange = (tabId: TabId) => {
if (tabId === activeTab) return;
const handleTabChange = (tabId: string) => {
const id = tabId as TabId;
if (id === activeTab) return;

setActiveTab(tabId);
setActiveTab(id);

if (cachedPosts[tabId]) {
setPosts(cachedPosts[tabId] ?? []);
if (cachedPosts[id]) {
setPosts(cachedPosts[id] ?? []);
return;
}

startTransition(async () => {
const response = await ClientPostsApi.getPostsWithCPForHomepage(
FILTERS[tabId]
FILTERS[id]
);
const newPosts = response.results;
setCachedPosts((prev) => ({ ...prev, [tabId]: newPosts }));
setCachedPosts((prev) => ({ ...prev, [id]: newPosts }));
setPosts(newPosts);
});
Comment on lines +38 to 56
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "homepage_forecasts.tsx" --type f

Repository: Metaculus/metaculus

Length of output: 128


🏁 Script executed:

cat -n front_end/src/app/(main)/(home)/components/homepage_forecasts.tsx

Repository: Metaculus/metaculus

Length of output: 222


🏁 Script executed:

cat -n "front_end/src/app/(main)/(home)/components/homepage_forecasts.tsx"

Repository: Metaculus/metaculus

Length of output: 4658


Guard against stale async responses overriding the active tab.

If a user switches tabs quickly, a slower request can finish later and overwrite posts for the wrong tab. When the response for "popular" tab arrives after the user has switched to "new" tab, line 55 will still call setPosts(newPosts) with the stale data, causing the displayed content to jump back to the previous tab.

Add a ref to track the active tab at response completion time and validate the response still matches the current tab before updating state:

Proposed fix
-import { FC, useState, useTransition } from "react";
+import { FC, useEffect, useRef, useState, useTransition } from "react";

   const [activeTab, setActiveTab] = useState<TabId>("news");
+  const activeTabRef = useRef<TabId>("news");
+
+  useEffect(() => {
+    activeTabRef.current = activeTab;
+  }, [activeTab]);

   const handleTabChange = (tabId: string) => {
     const id = tabId as TabId;
     if (id === activeTab) return;
@@
     startTransition(async () => {
       const response = await ClientPostsApi.getPostsWithCPForHomepage(
         FILTERS[id]
       );
       const newPosts = response.results;
       setCachedPosts((prev) => ({ ...prev, [id]: newPosts }));
-      setPosts(newPosts);
+      if (activeTabRef.current !== id) return;
+      setPosts(newPosts);
     });
   };
🤖 Prompt for AI Agents
In `@front_end/src/app/`(main)/(home)/components/homepage_forecasts.tsx around
lines 38 - 56, handleTabChange can apply stale async responses to the wrong tab:
capture the requested tab id at request start and validate it against a
persisted ref that always reflects the current active tab before mutating state.
Add a useRef (e.g., activeTabRef) updated whenever setActiveTab is called (or on
effect when activeTab changes), then inside the startTransition async block
compare activeTabRef.current === id before calling setCachedPosts and setPosts
(and skip updating if it no longer matches); keep the existing cachedPosts
fast-return logic but ensure the async branch performs this check to avoid
overwriting the active tab's posts with stale responses.

};
Expand All @@ -60,37 +61,39 @@ const HomePageForecasts: FC<Props> = ({ initialPopularPosts, className }) => {

return (
<section className={cn("flex flex-col gap-3", className)}>
<h2 className="m-0 text-xl font-bold leading-7 text-gray-1000 dark:text-gray-1000-dark">
{t("forecasts")}
</h2>

<div className="flex gap-2">
{TABS.map((tab) => (
<button
key={tab.id}
onClick={() => handleTabChange(tab.id)}
className={cn(
"rounded-full px-3.5 py-2.5 text-sm font-semibold leading-none transition-colors",
activeTab === tab.id
? "bg-gray-300 text-gray-800 dark:bg-gray-300-dark dark:text-gray-800-dark"
: "border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-200 dark:border-gray-300-dark dark:text-gray-700-dark dark:hover:bg-gray-200-dark"
)}
>
{tabLabels[tab.id]}
</button>
))}
</div>
<Tabs
defaultValue="news"
value={activeTab}
onChange={handleTabChange}
className="bg-transparent dark:bg-transparent"
>
<TabsList contained className="justify-start gap-1 lg:gap-3">
{TABS.map((tab) => (
<TabsTab
key={tab.id}
value={tab.id}
className="px-2 text-sm no-underline sm:px-2 sm:text-sm lg:px-5 lg:text-lg"
dynamicClassName={(isActive) =>
!isActive
? "hover:bg-blue-400 dark:hover:bg-blue-400-dark text-blue-800 dark:text-blue-800-dark"
: ""
}
scrollOnSelect={false}
>
{tabLabels[tab.id]}
</TabsTab>
))}
</TabsList>
</Tabs>

<div
className={cn(
"mt-3 grid grid-cols-1 gap-4 transition-opacity md:auto-rows-fr md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
"mt-1 columns-1 gap-4 transition-opacity md:mt-3 md:columns-2 lg:columns-3 xl:columns-4",
isPending && "opacity-50"
)}
>
{visiblePosts.map((post) => (
<div key={post.id} className="[&>*>div]:h-full [&>*]:h-full">
<PostCard post={post} minimalistic={true} />
</div>
<HomepagePostCard key={post.id} post={post} className="mb-4" />
))}

<ExploreAllCard />
Expand All @@ -104,21 +107,17 @@ const ExploreAllCard: FC = () => {
return (
<Link
href="/questions/"
className="flex flex-col rounded border border-blue-400 bg-gray-0 p-5 pb-0 no-underline dark:border-blue-400-dark dark:bg-gray-0-dark"
className="mb-4 flex break-inside-avoid flex-col rounded border border-blue-400 bg-gray-0 p-5 no-underline dark:border-blue-400-dark dark:bg-gray-0-dark"
>
<div>
<div className="m-0 flex justify-between text-base font-semibold text-gray-900 dark:text-gray-900-dark">
<span className="">{t("exploreAll")}</span>
<span>→</span>
</div>
<p className="mt-2 text-sm text-gray-700 dark:text-gray-700-dark">
<p className="mb-0 mt-2 text-sm text-gray-700 dark:text-gray-700-dark">
{t("thousandsOfOpenQuestions")}
</p>
</div>

<div className="relative mt-10 hidden flex-1 overflow-hidden md:block">
<ExploreImagesGrid className="absolute bottom-0 left-0 h-full w-auto" />
</div>
</Link>
);
};
Expand Down
72 changes: 72 additions & 0 deletions front_end/src/app/(main)/(home)/components/homepage_post_card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import Link from "next/link";
import { FC } from "react";

import ForecastersCounter from "@/app/(main)/questions/components/forecaster_counter";
import ConsumerQuestionTile from "@/components/consumer_post_card/consumer_question_tile";
import GroupForecastCard from "@/components/consumer_post_card/group_forecast_card";
import CommentStatus from "@/components/post_card/basic_post_card/comment_status";
import PostCardErrorBoundary from "@/components/post_card/error_boundary";
import HideCPProvider from "@/contexts/cp_context";
import { PostWithForecasts } from "@/types/post";
import cn from "@/utils/core/cn";
import { getPostLink } from "@/utils/navigation";
import {
isGroupOfQuestionsPost,
isMultipleChoicePost,
isQuestionPost,
} from "@/utils/questions/helpers";

type Props = {
post: PostWithForecasts;
className?: string;
};

const HomepagePostCard: FC<Props> = ({ post, className }) => {
const { title } = post;

return (
<PostCardErrorBoundary>
<div
className={cn(
"flex w-full break-inside-avoid items-start justify-start rounded border border-blue-400 bg-gray-0 px-4 py-5 no-underline @container dark:border-blue-400-dark dark:bg-gray-0-dark",
className
)}
>
<div className="relative z-0 flex w-full flex-col items-center gap-2.5 overflow-hidden">
<div className="flex items-center justify-between">
<CommentStatus
totalCount={post.comment_count ?? 0}
unreadCount={post.unread_comment_count ?? 0}
url={getPostLink(post)}
variant="gray"
className="z-[101]"
/>
<ForecastersCounter forecasters={post.nr_forecasters} />
</div>
<div className="flex w-full flex-col items-center gap-5 overflow-hidden no-underline @container">
<h4 className="m-0 max-w-xl text-center text-sm font-medium md:text-base">
{title}
</h4>
<HideCPProvider post={post}>
{isQuestionPost(post) && !isMultipleChoicePost(post) && (
<ConsumerQuestionTile question={post.question} />
)}

{(isGroupOfQuestionsPost(post) || isMultipleChoicePost(post)) && (
<GroupForecastCard post={post} />
)}
</HideCPProvider>
</div>
<Link
href={getPostLink(post)}
className="absolute top-0 z-100 h-full w-full @container"
/>
</div>
</div>
</PostCardErrorBoundary>
);
};

export default HomepagePostCard;
Loading