Skip to content

Conversation

@elisescu
Copy link
Contributor

@elisescu elisescu commented Dec 11, 2025

Figma Link here: https://www.figma.com/design/9N8DIIhfRt6ADzpZrRCBxS/Homepage?node-id=5267-13700&m=dev

Sadly, it's a very large PR, but it is separated into smaller commits which are easier to be reviewed independently

Comment on lines 1736 to +1781
"copyQuestionLinkToAccount": "Copy",
"none": "none"
"none": "none",
"metaculusFutureEval": "Metaculus FutureEval",
"futureEvalDescription": "FutureEval measures AI's ability to predict future outcomes. It is guaranteed leak proof.",
"futureEvalTagline": "We use forecasting as a way to evaluate reasoning against reality.",
"modelLeaderboard": "Model leaderboard",
"modelLeaderboardDescription": "We run all major models with a simple prompt on most open Metaculus forecasting questions, and collect their forecasts.",
"botsVsHumans": "Bots vs Humans",
"botsVsHumansDescription": "We run regular tournaments, open to all builders who compete based on their bot's accuracy against both each other and top human forecasters.",
"quarterlyBiweeklyTournaments": "Quarterly and Bi-weekly tournaments",
"tournamentsDescriptionPart1": "Join 100+ teams and individual bot builders competing for $50,000 prize pool.",
"theFall2025": "The Fall 2025",
"tournamentsDescriptionPart2": "tournament is live or join",
"miniBench": "MiniBench",
"leaderboardDataNotAvailable": "Leaderboard data not currently available, please check back soon!",
"viewLess": "View less",
"explore": "Explore"
Copy link
Contributor

Choose a reason for hiding this comment

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

usually we place translations in en.json file before the last item there and then generate translations in all the other files, this way we minimize the number of conflicts with other PRs

Comment on lines +12 to +39
const ListStarIcon = () => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-gray-500 dark:text-gray-500-dark"
>
<g clipPath="url(#clip0_5369_16385)">
<path
d="M6.74824 18.7623H18.747C19.2438 18.7623 19.6477 18.3579 19.6477 17.8616C19.6477 17.3507 19.2456 16.9531 18.747 16.9531H6.74824C6.24188 16.9531 5.85352 17.3558 5.85352 17.8616C5.85352 18.3528 6.24529 18.7623 6.74824 18.7623Z"
fill="#91999E"
/>
<path
d="M1.18575 19.5244L1.98107 18.9387L2.75133 19.5244C3.07348 19.7713 3.42996 19.5063 3.31025 19.1467L3.00233 18.1925L3.78209 17.6069C4.06629 17.3886 3.95094 16.9833 3.5918 16.9833H2.61967L2.29448 15.9562C2.19545 15.6426 1.76061 15.6366 1.66157 15.9562L1.3364 16.9833H0.354772C-0.00607591 16.9833 -0.129204 17.3886 0.162775 17.6069L0.952017 18.1942L0.645806 19.1467C0.526093 19.5063 0.871383 19.7539 1.18575 19.5244Z"
fill="#91999E"
/>
<path
d="M6.74824 12.9107H18.747C19.2438 12.9107 19.6477 12.4985 19.6477 12.0022C19.6477 11.499 19.2456 11.1016 18.747 11.1016H6.74824C6.24188 11.1016 5.85352 11.5042 5.85352 12.0022C5.85352 12.4951 6.24529 12.9107 6.74824 12.9107Z"
fill="#91999E"
/>
<path
d="M1.18575 13.6543L1.98107 13.0687L2.75133 13.6543C3.07348 13.9012 3.42996 13.6361 3.31025 13.2767L3.00233 12.3224L3.78209 11.7368C4.06629 11.5185 3.95094 11.1132 3.5918 11.1132H2.61967L2.29448 10.086C2.19545 9.77254 1.76061 9.77426 1.66157 10.086L1.3364 11.1132H0.354772C-0.00607591 11.1132 -0.129204 11.5185 0.162775 11.7368L0.952017 12.3241L0.645806 13.2767C0.526093 13.6361 0.871383 13.8838 1.18575 13.6543Z"
fill="#91999E"
/>
<path
d="M6.74824 7.05129H18.747C19.2438 7.05129 19.6477 6.64696 19.6477 6.15065C19.6477 5.64138 19.2456 5.24219 18.747 5.24219H6.74824C6.24188 5.24219 5.85352 5.64481 5.85352 6.15065C5.85352 6.64352 6.24529 7.05129 6.74824 7.05129Z"
Copy link
Contributor

Choose a reason for hiding this comment

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

are these not fontawesome icons?

Comment on lines +21 to +32
const MedalRow = ({ rank }: { rank: number }) => {
const medalType: MedalType =
rank === 1 ? "gold" : rank === 2 ? "silver" : "bronze";

return rank <= 3 ? (
<MedalIcon type={medalType} className="size-8" />
) : (
<span className="text-sm font-normal text-gray-1000 dark:text-gray-1000-dark">
{rank}
</span>
);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe a map here would be more readable and extensible

Suggested change
const MedalRow = ({ rank }: { rank: number }) => {
const medalType: MedalType =
rank === 1 ? "gold" : rank === 2 ? "silver" : "bronze";
return rank <= 3 ? (
<MedalIcon type={medalType} className="size-8" />
) : (
<span className="text-sm font-normal text-gray-1000 dark:text-gray-1000-dark">
{rank}
</span>
);
};
const MEDALS: Record<number, MedalType> = {
1: "gold",
2: "silver",
3: "bronze",
};
const MedalRow: FC<{ rank: number }> = ({ rank }) => {
const medalType = MEDALS[rank];
return medalType ? (
<MedalIcon type={medalType} className="size-8" />
) : (
<span className="text-sm font-normal text-gray-1000 dark:text-gray-1000-dark">
{rank}
</span>
);
};

Comment on lines +93 to +103
<HeroCTACard
href={individualsHref}
topTitle={t("forIndividuals")}
imageSrc="/images/pie-chart.png"
imageAlt="Pie chart"
title={t("heroIndividualsTitle")}
buttonText={t("exploreQuestions")}
bgColorClasses="bg-blue-300 dark:bg-blue-300-dark"
textColorClasses="text-blue-800 dark:text-blue-800-dark"
buttonClassName="border-blue-500 bg-gray-0 text-blue-700 hover:border-blue-600 hover:bg-blue-100 dark:border-blue-500-dark dark:bg-gray-0-dark dark:text-blue-700-dark dark:hover:border-blue-600-dark dark:hover:bg-blue-100-dark"
>
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it’s better to add a variant prop here and let the card control its own styling, instead of passing all these classes down. For example, if we want to reuse the same visual elsewhere, we’ll have to duplicate the class list again – and any future changes would be harder to maintain.

Comment on lines +62 to +66
<Suspense>
<div className="mt-8 w-full border-y border-gray-300 bg-gray-100 py-20 dark:border-gray-300-dark dark:bg-gray-100-dark md:mt-16 ">
<TournamentsSection className={contentWidthClassNames} />
</div>
</Suspense>
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should add some fallbacks here?

Comment on lines +30 to +32
const sidebarItems = await serverMiscApi.getSidebarItems();
const homepagePosts = await ServerPostsApi.getPostsForHomepage();
const categories = await ServerProjectsApi.getHomepageCategories();
Copy link
Contributor

Choose a reason for hiding this comment

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

We can parallelize the independent requests

Suggested change
const sidebarItems = await serverMiscApi.getSidebarItems();
const homepagePosts = await ServerPostsApi.getPostsForHomepage();
const categories = await ServerProjectsApi.getHomepageCategories();
const [sidebarItems, homepagePosts, categories, initialPopularPosts] =
await Promise.all([
serverMiscApi.getSidebarItems(),
ServerPostsApi.getPostsForHomepage(),
ServerProjectsApi.getHomepageCategories(),
ServerPostsApi.getPostsWithCP(FILTERS.popular),
]);

)

data = [
{**CategorySerializer(obj).data, "posts": obj.top_n_post_titles}
Copy link
Contributor

Choose a reason for hiding this comment

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

Usually we put post objects under the posts key, so let’s keep that convention. I’d suggest changing this to post_titles, or alternatively structuring it as posts: [{ title: ... }].

# includes extra columns in the SELECT. ArraySubquery wraps the query in PostgreSQL's ARRAY()
# which requires exactly one column. Querying the through table with a direct FK lookup
# generates a clean single-column SELECT.
ThroughModel = Post.projects.through
Copy link
Contributor

Choose a reason for hiding this comment

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

But what about default_project? We don't duplicate default project of the post into ThroughModel table

locale: "original", // Check the translations documentation why this is the case
},
];
const languageMenuItems = APP_LANGUAGES;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's replace languageMenuItems with just APP_LANGUAGES in the entire file

import cn from "@/utils/core/cn";
import { logError } from "@/utils/core/errors";

const MetaculusTextLogo: FC<{ className?: string }> = ({ className }) => (
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move each svg image into the separate component file for convenience. See front_end/src/app/(main)/about/components/MetacLogo.tsx

Comment on lines +171 to +174
<span className="flex items-center text-base font-bold">
<span className="text-blue-800">a</span>
<span className="text-gray-400">/</span>
<span className="text-salmon-600"></span>
Copy link
Contributor

Choose a reason for hiding this comment

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

I see the same block in the LanguagePreferences. Let's move this to the separate component

Copy link
Contributor

Choose a reason for hiding this comment

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

  • the original one has dark mode settings:
 <div className="pointer-events-none absolute inset-y-0 left-3 z-10 flex items-center text-lg font-bold">
          <span className="text-blue-800 dark:text-blue-800-dark">a</span>
          <span className="text-gray-400 dark:text-gray-400-dark">/</span>
          <span className="text-salmon-600 dark:text-salmon-600-dark">文</span>
        </div>

Comment on lines +152 to +159
const locale = useLocale();
const currentLanguage =
APP_LANGUAGES.find((l) => l.locale === locale) ??
APP_LANGUAGES[APP_LANGUAGES.length - 1];

if (!currentLanguage) {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not respect user db setting.

Let's do:

const selectedLanguage = user.language || currentLanguage;

see

const selectedLanguage = user.language || currentLocale;

Comment on lines +42 to +44
const initialPopularPosts = await ServerPostsApi.getPostsWithCP(
FILTERS.popular
);
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's create a separate version of this function and cache it on the frontend side.

E.g

await ServerPostsApi.getPostsWithCPForHomepage();

next: { revalidate: 30 * 60 },

Copy link
Contributor

@hlbmtc hlbmtc Dec 12, 2025

Choose a reason for hiding this comment

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

Important! I see that the homepage isn’t very dynamic or personalized, but it does make a lot of backend requests and is used frequently. Maybe we should consider caching it entirely, say for 15 minutes?

Comment on lines +284 to +286
} catch (error) {
console.error(error);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

logError(error) to capture via sentry

Copy link
Contributor

@hlbmtc hlbmtc Dec 12, 2025

Choose a reason for hiding this comment

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

I think we need to move this function into actions

import ClientMiscApi from "@/services/api/misc/misc.client";
import cn from "@/utils/core/cn";

const AeiLogo: FC<{ className?: string }> = ({ className }) => (
Copy link
Contributor

Choose a reason for hiding this comment

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

re: images, let's move into the separate images folder component files

Comment on lines +15 to +20
const tournaments = await ServerProjectsApi.getTournaments({
show_on_homepage: true,
});
const allTournaments = (await ServerProjectsApi.getTournaments()).filter(
(t) => t.is_ongoing
);
Copy link
Contributor

@hlbmtc hlbmtc Dec 12, 2025

Choose a reason for hiding this comment

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

await ServerProjectsApi.getTournaments() already returns all tournaments including the ones with show_on_homepage: true, so let's minimize it to one request only and filter on the FE side

Comment on lines +99 to +100
<div className="p-3 pb-0">
{notebook.image_url && false ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

this is always false

@@ -0,0 +1,557 @@
"use client";
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really need "use client" here?

);
};

const ExploreImagesGrid: FC<{ className?: string }> = ({ className }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Move image to the separate file component

@@ -0,0 +1,170 @@
"use client";
Copy link
Contributor

Choose a reason for hiding this comment

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

Check if this is really needed

Copy link
Contributor

@hlbmtc hlbmtc left a comment

Choose a reason for hiding this comment

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

Could you please check how SSR-friendly the homepage is? Since it’s a landing page that’s frequently indexed by search engines, it probably makes sense to make it as SEO-friendly as possible

};

const CategoryCard: FC<CategoryCardProps> = ({ category }) => {
const categoryUrl = `/questions/?${POST_CATEGORIES_FILTER}=${category.slug}&for_main_feed=false`;
Copy link
Contributor

Choose a reason for hiding this comment

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

If we do POST_CATEGORIES_FILTER let's replace for_main_feed with POST_FOR_MAIN_FEED

);
};

const ExploreImagesGrid: FC<{ className?: string }> = ({ className }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

separate file

Comment on lines +47 to +50
tournament.slug
? `/tournament/${tournament.slug}`
: `/tournament/${tournament.id}`
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's use getProjectLink

</svg>
);

const NasdaqLogo: FC<{ className?: string }> = ({ className }) => (
Copy link
Contributor

Choose a reason for hiding this comment

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

separate file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants