-
Notifications
You must be signed in to change notification settings - Fork 22
Add the new home page #3906
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?
Add the new home page #3906
Conversation
3fe5c13 to
251dc07
Compare
| "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" |
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.
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
| 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" |
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.
are these not fontawesome icons?
| 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> | ||
| ); | ||
| }; |
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.
I believe a map here would be more readable and extensible
| 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> | |
| ); | |
| }; |
| <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" | ||
| > |
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.
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.
| <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> |
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.
Maybe we should add some fallbacks here?
| const sidebarItems = await serverMiscApi.getSidebarItems(); | ||
| const homepagePosts = await ServerPostsApi.getPostsForHomepage(); | ||
| const categories = await ServerProjectsApi.getHomepageCategories(); |
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.
We can parallelize the independent requests
| 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), | |
| ]); |
This is part of the new homepage design
251dc07 to
e9e75f2
Compare
| ) | ||
|
|
||
| data = [ | ||
| {**CategorySerializer(obj).data, "posts": obj.top_n_post_titles} |
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.
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 |
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.
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; |
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.
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 }) => ( |
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.
Let's move each svg image into the separate component file for convenience. See front_end/src/app/(main)/about/components/MetacLogo.tsx
| <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> |
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.
I see the same block in the LanguagePreferences. Let's move this to the separate component
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.
- 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>
| const locale = useLocale(); | ||
| const currentLanguage = | ||
| APP_LANGUAGES.find((l) => l.locale === locale) ?? | ||
| APP_LANGUAGES[APP_LANGUAGES.length - 1]; | ||
|
|
||
| if (!currentLanguage) { | ||
| return null; | ||
| } |
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.
This does not respect user db setting.
Let's do:
const selectedLanguage = user.language || currentLanguage;
see
metaculus/front_end/src/app/(main)/accounts/settings/(general)/components/language_preferences.tsx
Line 38 in 9535e4b
| const selectedLanguage = user.language || currentLocale; |
| const initialPopularPosts = await ServerPostsApi.getPostsWithCP( | ||
| FILTERS.popular | ||
| ); |
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.
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 },
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.
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?
| } catch (error) { | ||
| console.error(error); | ||
| } |
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.
logError(error) to capture via sentry
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.
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 }) => ( |
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.
re: images, let's move into the separate images folder component files
| const tournaments = await ServerProjectsApi.getTournaments({ | ||
| show_on_homepage: true, | ||
| }); | ||
| const allTournaments = (await ServerProjectsApi.getTournaments()).filter( | ||
| (t) => t.is_ongoing | ||
| ); |
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.
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
| <div className="p-3 pb-0"> | ||
| {notebook.image_url && false ? ( |
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.
this is always false
| @@ -0,0 +1,557 @@ | |||
| "use client"; | |||
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.
Do we really need "use client" here?
| ); | ||
| }; | ||
|
|
||
| const ExploreImagesGrid: FC<{ className?: string }> = ({ className }) => { |
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.
Move image to the separate file component
| @@ -0,0 +1,170 @@ | |||
| "use client"; | |||
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.
Check if this is really needed
hlbmtc
left a comment
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.
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`; |
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.
If we do POST_CATEGORIES_FILTER let's replace for_main_feed with POST_FOR_MAIN_FEED
| ); | ||
| }; | ||
|
|
||
| const ExploreImagesGrid: FC<{ className?: string }> = ({ className }) => { |
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.
separate file
| tournament.slug | ||
| ? `/tournament/${tournament.slug}` | ||
| : `/tournament/${tournament.id}` | ||
| } |
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.
Let's use getProjectLink
| </svg> | ||
| ); | ||
|
|
||
| const NasdaqLogo: FC<{ className?: string }> = ({ className }) => ( |
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.
separate file
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