-
Notifications
You must be signed in to change notification settings - Fork 27
Homepage Improvements #4123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Homepage Improvements #4123
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: fd "homepage_forecasts.tsx" --type fRepository: Metaculus/metaculus Length of output: 128 🏁 Script executed: cat -n front_end/src/app/(main)/(home)/components/homepage_forecasts.tsxRepository: 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 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 |
||
| }; | ||
|
|
@@ -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 /> | ||
|
|
@@ -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> | ||
| ); | ||
| }; | ||
|
|
||
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Metaculus/metaculus
Length of output: 2333
🏁 Script executed:
Repository: Metaculus/metaculus
Length of output: 1592
🏁 Script executed:
Repository: Metaculus/metaculus
Length of output: 1709
Correct
yearsOfPredictioncasing 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
📝 Committable suggestion
🤖 Prompt for AI Agents