Skip to content

Commit

Permalink
Index project type (#2285)
Browse files Browse the repository at this point in the history
* Index project type frontend changes

* Index project type backend changes

* - Added `ProjectIndexQuestion` model
- Added admin layer
- Implemented `serialize_project_index_weights`

* Index project changes FE

* Adjusted link generation

* Small admin fix

* Conflicts resolution

* - Added `/index/:slug` page url
- Adjusted `getProjectLink` util

* Translations
  • Loading branch information
hlbmtc authored Feb 28, 2025
1 parent 126802d commit ec06f90
Show file tree
Hide file tree
Showing 27 changed files with 456 additions and 323 deletions.
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",
"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

0 comments on commit ec06f90

Please sign in to comment.