Skip to content
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

Index project type #2285

Merged
merged 14 commits into from
Feb 28, 2025
Merged
4 changes: 3 additions & 1 deletion front_end/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1089,5 +1089,7 @@
"pickQuestion": "Vybrat otázku",
"showApiToken": "Zobrazit API token",
"copyApiToken": "Kopírovat API token",
"copiedApiTokenMessage": "API token byl zkopírován do vaší schránky"
"copiedApiTokenMessage": "API token byl zkopírován do vaší schránky",
"Indexes": "Indexy",
"Index": "Index"
}
2 changes: 2 additions & 0 deletions front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,8 @@
"tournamentsHero3": "<scores>Learn</scores> how tournaments are scored.",
"tournamentsHero4": "<email>Reach out</email> to discuss launching a tournament on what’s important to you.",
"ActiveTournaments": "Active Tournaments",
"Indexes": "Indexes",
"Index": "Index",
Comment on lines +512 to +513
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 add translation for other locales as well

"Tournament": "Tournament",
"QuestionSeries": "Question Series",
"Archive": "Archive",
Expand Down
4 changes: 3 additions & 1 deletion front_end/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1089,5 +1089,7 @@
"pickQuestion": "Elegir pregunta",
"showApiToken": "Mostrar token de API",
"copyApiToken": "Copiar token de API",
"copiedApiTokenMessage": "El token de API se ha copiado a su portapapeles"
"copiedApiTokenMessage": "El token de API se ha copiado a su portapapeles",
"Indexes": "Índices",
"Index": "Índice"
}
4 changes: 3 additions & 1 deletion front_end/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -1087,5 +1087,7 @@
"pickQuestion": "Escolher Pergunta",
"showApiToken": "Mostrar token da API",
"copyApiToken": "Copiar token da API",
"copiedApiTokenMessage": "Token da API foi copiado para sua área de transferência"
"copiedApiTokenMessage": "Token da API foi copiado para sua área de transferência",
"Indexes": "Índices",
"Index": "Índice"
}
4 changes: 3 additions & 1 deletion front_end/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1091,5 +1091,7 @@
"pickQuestion": "选择问题",
"showApiToken": "显示 API 令牌",
"copyApiToken": "复制 API 令牌",
"copiedApiTokenMessage": "API 令牌已复制到您的剪贴板"
"copiedApiTokenMessage": "API 令牌已复制到您的剪贴板",
"Indexes": "索引",
"Index": "索引"
}
8 changes: 8 additions & 0 deletions front_end/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ const nextConfig = {
},
];
},
async rewrites() {
return [
{
source: "/index/:slug",
destination: "/tournament/:slug",
},
];
},
eslint: {
ignoreDuringBuilds: true,
},
Expand Down
39 changes: 26 additions & 13 deletions front_end/src/app/(main)/(tournaments)/tournament/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import invariant from "ts-invariant";

import ProjectContributions from "@/app/(main)/(leaderboards)/contributions/components/project_contributions";
import ProjectLeaderboard from "@/app/(main)/(leaderboards)/leaderboard/components/project_leaderboard";
import IndexSection from "@/app/(main)/(tournaments)/tournament/components/index";
import TournamentSubscribeButton from "@/app/(main)/(tournaments)/tournament/components/tournament_subscribe_button";
import HtmlContent from "@/components/html_content";
import TournamentFilters from "@/components/tournament_filters";
Expand Down Expand Up @@ -66,11 +67,17 @@ export default async function TournamentSlug({ params }: Props) {
const t = await getTranslations();
const locale = await getLocale();
const isQuestionSeries = tournament.type === TournamentType.QuestionSeries;
const title = isQuestionSeries ? t("QuestionSeries") : t("Tournament");
const title = isQuestionSeries
? t("QuestionSeries")
: tournament.type === TournamentType.Index
? t("Index")
: t("Tournament");
const questionsTitle = isQuestionSeries
? t("SeriesContents")
: t("questions");

const indexWeights = tournament.index_weights ?? [];

return (
<main className="mx-auto mb-16 mt-4 min-h-min w-full max-w-[780px] flex-auto px-0">
<div className="bg-gray-0 dark:bg-gray-0-dark">
Expand Down Expand Up @@ -141,19 +148,25 @@ export default async function TournamentSlug({ params }: Props) {
</div>
<HtmlContent content={tournament.description} />

<div className="mt-3 flex flex-col gap-3">
<ProjectLeaderboard
projectId={tournament.id}
userId={currentUser?.id}
isQuestionSeries={isQuestionSeries}
/>
{currentUser && (
<ProjectContributions
project={tournament}
userId={currentUser.id}
{indexWeights.length > 0 && (
<IndexSection indexWeights={indexWeights} />
)}

{tournament.score_type && (
<div className="mt-3 flex flex-col gap-3">
<ProjectLeaderboard
projectId={tournament.id}
userId={currentUser?.id}
isQuestionSeries={isQuestionSeries}
/>
)}
</div>
{currentUser && (
<ProjectContributions
project={tournament}
userId={currentUser.id}
/>
)}
</div>
)}
</div>

<section className="mx-2 border-t border-t-[#e5e7eb] px-1 py-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { fromUnixTime, subWeeks } from "date-fns";
import { useTranslations } from "next-intl";
import React, { FC } from "react";

import WeeklyMovement from "@/components/weekly_movement";
import { ProjectIndexWeights } from "@/types/projects";
import { QuestionType } from "@/types/question";
import { scaleInternalLocation } from "@/utils/charts";

import IndexQuestionsTable from "./index_questions_table";

import "./styles.css";

type Props = {
indexWeights: ProjectIndexWeights[];
};

const IndexSection: FC<Props> = ({ indexWeights }) => {
const t = useTranslations();

const { index: indexValue, indexWeekAgo } = calculateIndex(indexWeights);
const indexWeeklyMovement = Number((indexValue - indexWeekAgo).toFixed(1));

return (
<IndexQuestionsTable
indexWeights={indexWeights}
HeadingSection={
<div className="flex flex-col items-center border-b border-gray-300 bg-blue-100 px-4 py-4 text-center leading-4 dark:border-gray-300-dark dark:bg-blue-100-dark">
<p className="m-0 mb-2 text-3xl capitalize leading-9">
{t.rich("indexScore", {
value: Number(indexValue.toFixed(1)),
bold: (chunks) => <b>{chunks}</b>,
})}
</p>
<WeeklyMovement
weeklyMovement={indexWeeklyMovement}
message={t("weeklyMovementChange", {
value:
indexWeeklyMovement === 0
? t("noChange")
: Math.abs(indexWeeklyMovement),
})}
className="text-base"
iconClassName="text-base"
/>
</div>
}
/>
);
};

function calculateIndex(posts: ProjectIndexWeights[]): {
index: number;
indexWeekAgo: number;
} {
const weightSum = posts.reduce((acc, post) => acc + post.weight, 0);
if (weightSum === 0) {
return { index: 0, indexWeekAgo: 0 };
}
const dateNow = new Date();
const weekAgoDate = subWeeks(dateNow, 1);

const { scoreSum, weeklyScoreSum } = posts.reduce(
(acc, obj) => {
const question =
obj.post.question ||
obj.post.group_of_questions?.questions?.find(
(q) => obj.question_id === q.id
);

if (!question) {
return acc;
}

const latestAggregation = question.aggregations.recency_weighted.latest;
const historyAggregation = question.aggregations.recency_weighted.history;
if (!latestAggregation) {
return acc;
}

let postValue = 0;
let postValueWeekAgo = 0;
const cp = latestAggregation.centers?.at(-1);
const weekAgoCP =
historyAggregation.find(
(el) =>
el.end_time &&
fromUnixTime(el.end_time) >=
fromUnixTime(weekAgoDate.getTime() / 1000)
)?.centers?.[0] ?? null;

switch (question.type) {
case QuestionType.Binary: {
if (!cp) {
break;
}

const median = scaleInternalLocation(cp, {
range_min: 0,
range_max: 100,
zero_point: null,
});
postValue = 2 * median - 1;

const medianWeekAgo = scaleInternalLocation(weekAgoCP ?? cp, {
range_min: 0,
range_max: 100,
zero_point: null,
});
postValueWeekAgo = 2 * medianWeekAgo - 1;
break;
}
case QuestionType.Numeric: {
const scaling = question.scaling;
const min = scaling.range_min;
const max = scaling.range_max;
if (!min || !max) {
break;
}

if (!cp) {
break;
}
const median = scaleInternalLocation(cp, scaling);
postValue = (2 * median - max - min) / (max - min);

const medianWeekAgo = scaleInternalLocation(weekAgoCP ?? cp, scaling);
postValueWeekAgo = (2 * medianWeekAgo - max - min) / (max - min);
break;
}
}

return {
scoreSum: acc.scoreSum + obj.weight * postValue,
weeklyScoreSum: acc.weeklyScoreSum + obj.weight * postValueWeekAgo,
};
},
{ scoreSum: 0, weeklyScoreSum: 0 }
);

return {
index: scoreSum / weightSum,
indexWeekAgo: weeklyScoreSum / weightSum,
};
}

export default IndexSection;
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
TableRow,
} from "@/components/ui/table";
import useScreenSize from "@/hooks/use_screen_size";
import { PostWithForecasts, PostWithForecastsAndWeight } from "@/types/post";
import { PostWithForecasts } from "@/types/post";
import { ProjectIndexWeights } from "@/types/projects";
import { getDisplayValue } from "@/utils/charts";
import cn from "@/utils/cn";
import { getPostLink } from "@/utils/navigation";
Expand All @@ -39,19 +40,20 @@ type TableItem = {
weight: number;
communityPrediction: IndexCommunityPrediction;
post: PostWithForecasts;
questionId: number;
};

const columnHelper = createColumnHelper<TableItem>();

type Props = {
indexQuestions: PostWithForecastsAndWeight[];
indexWeights: ProjectIndexWeights[];
HeadingSection?: ReactNode;
};

const IndexQuestionsTable: FC<Props> = ({ indexQuestions, HeadingSection }) => {
const IndexQuestionsTable: FC<Props> = ({ indexWeights, HeadingSection }) => {
const t = useTranslations();

const data = useMemo(() => getTableData(indexQuestions), [indexQuestions]);
const data = useMemo(() => getTableData(indexWeights), [indexWeights]);
const questionsCount = data.length;

const { width } = useScreenSize();
Expand All @@ -73,7 +75,10 @@ const IndexQuestionsTable: FC<Props> = ({ indexQuestions, HeadingSection }) => {
cell: (info) => (
<>
<Link
href={getPostLink(info.row.original.post)}
href={getPostLink(
info.row.original.post,
info.row.original.questionId
)}
className="absolute inset-0"
/>
{info.getValue()}
Expand Down Expand Up @@ -183,41 +188,48 @@ const IndexQuestionsTable: FC<Props> = ({ indexQuestions, HeadingSection }) => {
);
};

function getTableData(questions: PostWithForecastsAndWeight[]): TableItem[] {
function getTableData(questions: ProjectIndexWeights[]): TableItem[] {
const data: TableItem[] = [];
for (const post of questions) {
if (!post.question) {
for (const obj of questions) {
const question =
obj.post.question ||
obj.post.group_of_questions?.questions?.find(
(q) => obj.question_id === q.id
);

if (!question) {
continue;
}

const cpRawValue =
post.question.aggregations.recency_weighted.latest?.centers?.[0] ?? null;
question.aggregations.recency_weighted.latest?.centers?.[0] ?? null;
const cpDisplayValue = getDisplayValue({
value: cpRawValue,
questionType: post.question.type,
scaling: post.question.scaling,
questionType: question.type,
scaling: question.scaling,
});

data.push({
title: post.title,
weight: post.weight,
title: question.title,
weight: obj.weight,
communityPrediction: {
rawValue: cpRawValue,
displayValue: cpDisplayValue,
},
post,
post: obj.post,
questionId: obj.question_id,
});
}

return data;
}

const MobileQuestionCell: FC<CellContext<TableItem, string>> = ({ row }) => {
const { title, weight, communityPrediction, post } = row.original;
const { title, weight, communityPrediction, post, questionId } = row.original;

return (
<div className="flex flex-col gap-2">
<Link href={getPostLink(post)} className="absolute inset-0" />
<Link href={getPostLink(post, questionId)} className="absolute inset-0" />

<span className="text-sm font-medium leading-5">{title}</span>
<CommunityPrediction post={post} {...communityPrediction} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
TournamentsSortBy,
TournamentType,
} from "@/types/projects";
import { getProjectLink } from "@/utils/navigation";

import {
TOURNAMENTS_SEARCH,
Expand Down Expand Up @@ -66,11 +67,7 @@ const TournamentsList: FC<Props> = ({
{filteredItems.slice(0, displayItemsCount).map((item) => (
<TournamentCard
key={item.id}
href={
item.slug
? `/tournament/${item.slug}`
: `/tournament/${item.id}`
}
href={getProjectLink(item)}
headerImageSrc={item.header_image}
name={item.name}
questionsCount={item.questions_count}
Expand Down
Loading