From f6e2e8cfa1d8e3f2201a97beaa22311266868886 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Mon, 9 May 2022 23:27:51 +0400 Subject: [PATCH] refactor: more typescript --- package-lock.json | 23 +- package.json | 2 + src/backend/platforms/betfair.ts | 13 +- src/backend/platforms/goodjudgment.ts | 5 +- src/backend/platforms/infer.ts | 5 +- src/backend/utils/roughSize.ts | 25 -- src/backend/utils/stars.ts | 30 +-- src/backend/utils/toMarkdown.ts | 30 +-- src/graphql/schema/dashboards.ts | 1 + src/graphql/schema/questions.ts | 2 +- src/graphql/schema/search.ts | 14 +- src/pages/dashboards/embed/[id].tsx | 16 +- src/pages/index.tsx | 9 +- src/pages/secretEmbed.tsx | 23 +- src/web/common/Layout.tsx | 2 +- src/web/common/MultiSelectPlatform.tsx | 2 +- src/web/display/DashboardCreator.tsx | 4 +- src/web/icons/Favicon.tsx | 8 +- src/web/icons/Logo.tsx | 8 +- src/web/icons/Logo2.tsx | 8 +- src/web/icons/index.ts | 6 +- .../components/HistoryChart/InnerChart.tsx | 8 +- .../components/HistoryChart/Legend.tsx | 8 +- .../components/HistoryChart/index.tsx | 3 +- .../components/HistoryChart/utils.ts | 10 +- .../questions/components/IndicatorsTable.tsx | 10 +- .../QuestionCard/QuestionFooter.tsx | 49 ++-- .../components/QuestionCard/index.tsx | 53 +--- .../components/QuestionIndicators.tsx | 227 ------------------ .../questions/components/QuestionOptions.tsx | 30 ++- src/web/questions/utils.ts | 17 ++ src/web/search/components/SearchScreen.tsx | 2 +- src/web/utils.ts | 11 + src/web/worker/searchWithAlgolia.ts | 8 +- 34 files changed, 209 insertions(+), 463 deletions(-) delete mode 100644 src/backend/utils/roughSize.ts delete mode 100644 src/web/questions/components/QuestionIndicators.tsx diff --git a/package-lock.json b/package-lock.json index 1ae8f10..4560da0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "metaforecast", "version": "2.0.0", "license": "MIT", "dependencies": { @@ -17,11 +16,13 @@ "@prisma/client": "^3.11.1", "@tailwindcss/forms": "^0.4.0", "@tailwindcss/typography": "^0.5.1", + "@types/chroma-js": "^2.1.3", "@types/dom-to-image": "^2.6.4", "@types/jsdom": "^16.2.14", "@types/nprogress": "^0.2.0", "@types/react": "^17.0.39", "@types/react-copy-to-clipboard": "^5.0.2", + "@types/textversionjs": "^1.1.1", "airtable": "^0.11.1", "algoliasearch": "^4.10.3", "autoprefixer": "^10.1.0", @@ -3170,6 +3171,11 @@ "@types/responselike": "*" } }, + "node_modules/@types/chroma-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", + "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==" + }, "node_modules/@types/cross-spawn": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", @@ -3354,6 +3360,11 @@ "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", "license": "MIT" }, + "node_modules/@types/textversionjs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/textversionjs/-/textversionjs-1.1.1.tgz", + "integrity": "sha512-xXa08oZ76+J2rS36guKd6zJFAbkRtUnuDb0R6OFtY93VTuXdi94k0ycsIBrsq/Av8DmiQVfTYE3k7KCEUVYsag==" + }, "node_modules/@types/tough-cookie": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", @@ -42405,6 +42416,11 @@ "@types/responselike": "*" } }, + "@types/chroma-js": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.1.3.tgz", + "integrity": "sha512-1xGPhoSGY1CPmXLCBcjVZSQinFjL26vlR8ZqprsBWiFyED4JacJJ9zHhh5aaUXqbY9B37mKQ73nlydVAXmr1+g==" + }, "@types/cross-spawn": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", @@ -42577,6 +42593,11 @@ "resolved": "https://registry.npmjs.org/@types%2fscheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "@types/textversionjs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/textversionjs/-/textversionjs-1.1.1.tgz", + "integrity": "sha512-xXa08oZ76+J2rS36guKd6zJFAbkRtUnuDb0R6OFtY93VTuXdi94k0ycsIBrsq/Av8DmiQVfTYE3k7KCEUVYsag==" + }, "@types/tough-cookie": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", diff --git a/package.json b/package.json index 07dbc88..2eecce6 100644 --- a/package.json +++ b/package.json @@ -35,11 +35,13 @@ "@prisma/client": "^3.11.1", "@tailwindcss/forms": "^0.4.0", "@tailwindcss/typography": "^0.5.1", + "@types/chroma-js": "^2.1.3", "@types/dom-to-image": "^2.6.4", "@types/jsdom": "^16.2.14", "@types/nprogress": "^0.2.0", "@types/react": "^17.0.39", "@types/react-copy-to-clipboard": "^5.0.2", + "@types/textversionjs": "^1.1.1", "airtable": "^0.11.1", "algoliasearch": "^4.10.3", "autoprefixer": "^10.1.0", diff --git a/src/backend/platforms/betfair.ts b/src/backend/platforms/betfair.ts index 6e71ebe..2fec2cf 100644 --- a/src/backend/platforms/betfair.ts +++ b/src/backend/platforms/betfair.ts @@ -42,7 +42,7 @@ async function fetchPredictions() { const agent = new https.Agent({ rejectUnauthorized: false, }); - let response = await axios({ + const response = await axios({ url: endpoint, method: "GET", headers: { @@ -115,18 +115,19 @@ async function processPredictions(data) { if (rules == undefined) { // console.log(prediction.description) } + let title = rules.split("? ")[0] + "?"; let description = rules.split("? ")[1].trim(); if (title.includes("of the named")) { title = prediction.marketName + ": " + title; } - let result = { - id: id, - title: title, + const result = { + id, + title, url: `https://www.betfair.com/exchange/plus/politics/market/${prediction.marketId}`, platform: platformName, - description: description, - options: options, + description, + options, qualityindicators: { stars: calculateStars(platformName, { volume: prediction.totalMatched, diff --git a/src/backend/platforms/goodjudgment.ts b/src/backend/platforms/goodjudgment.ts index 499e8f0..3d191ea 100644 --- a/src/backend/platforms/goodjudgment.ts +++ b/src/backend/platforms/goodjudgment.ts @@ -9,10 +9,7 @@ import { FetchedQuestion, Platform } from "./"; /* Definitions */ const platformName = "goodjudgment"; -let endpoint = "https://goodjudgment.io/superforecasts/"; -String.prototype.replaceAll = function replaceAll(search, replace) { - return this.split(search).join(replace); -}; +const endpoint = "https://goodjudgment.io/superforecasts/"; /* Body */ export const goodjudgment: Platform = { diff --git a/src/backend/platforms/infer.ts b/src/backend/platforms/infer.ts index cf62fd0..d6133c6 100644 --- a/src/backend/platforms/infer.ts +++ b/src/backend/platforms/infer.ts @@ -9,10 +9,7 @@ import { FetchedQuestion, Platform } from "./"; /* Definitions */ const platformName = "infer"; -let htmlEndPoint = "https://www.infer-pub.com/questions"; -String.prototype.replaceAll = function replaceAll(search, replace) { - return this.split(search).join(replace); -}; +const htmlEndPoint = "https://www.infer-pub.com/questions"; const DEBUG_MODE: "on" | "off" = "off"; // "off" const SLEEP_TIME_RANDOM = 7000; // miliseconds const SLEEP_TIME_EXTRA = 2000; diff --git a/src/backend/utils/roughSize.ts b/src/backend/utils/roughSize.ts deleted file mode 100644 index 75fff92..0000000 --- a/src/backend/utils/roughSize.ts +++ /dev/null @@ -1,25 +0,0 @@ -export function roughSizeOfObject(object) { - var objectList = []; - var stack = [object]; - var bytes = 0; - - while (stack.length) { - var value = stack.pop(); - if (typeof value === "boolean") { - bytes += 4; - } else if (typeof value === "string") { - bytes += value.length * 2; - } else if (typeof value === "number") { - bytes += 8; - } else if (typeof value === "object" && objectList.indexOf(value) === -1) { - objectList.push(value); - - for (var i in value) { - stack.push(value[i]); - } - } - } - let megaBytes = bytes / 1024 ** 2; - let megaBytesRounded = Math.round(megaBytes * 10) / 10; - return megaBytesRounded; -} diff --git a/src/backend/utils/stars.ts b/src/backend/utils/stars.ts index c0a7dae..5150394 100644 --- a/src/backend/utils/stars.ts +++ b/src/backend/utils/stars.ts @@ -1,31 +1,5 @@ -export function getStarSymbols(numstars) { - let stars = "★★☆☆☆"; - switch (numstars) { - case 0: - stars = "☆☆☆☆☆"; - break; - case 1: - stars = "★☆☆☆☆"; - break; - case 2: - stars = "★★☆☆☆"; - break; - case 3: - stars = "★★★☆☆"; - break; - case 4: - stars = "★★★★☆"; - break; - case 5: - stars = "★★★★★"; - break; - default: - stars = "★★☆☆☆"; - } - return stars; -} - -let average = (array) => array.reduce((a, b) => a + b, 0) / array.length; +let average = (array: number[]) => + array.reduce((a, b) => a + b, 0) / array.length; function calculateStarsAstralCodexTen(data) { let nuno = (data) => 3; diff --git a/src/backend/utils/toMarkdown.ts b/src/backend/utils/toMarkdown.ts index 31b59ae..b227af6 100644 --- a/src/backend/utils/toMarkdown.ts +++ b/src/backend/utils/toMarkdown.ts @@ -1,26 +1,16 @@ -/* Imports */ import textVersion from "textversionjs"; -/* Definitions */ -String.prototype.replaceAll = function replaceAll(search, replace) { - return this.split(search).join(replace); -}; - -var styleConfig = { - linkProcess: function (href, linkText) { - let newHref = href ? href.replace(/\(/g, "%28").replace(/\)/g, "%29") : ""; - // Deal corretly in markdown with links that contain parenthesis - return `[${linkText}](${newHref})`; - }, -}; - -/* Support functions */ - -/* Body */ - -export default function toMarkdown(htmlText) { +export default function toMarkdown(htmlText: string) { let html2 = htmlText.replaceAll(`='`, `="`).replaceAll(`'>`, `">`); - return textVersion(html2, styleConfig); + return textVersion(html2, { + linkProcess: (href, linkText) => { + let newHref = href + ? href.replace(/\(/g, "%28").replace(/\)/g, "%29") + : ""; + // Deal correctly in markdown with links that contain parenthesis + return `[${linkText}](${newHref})`; + }, + }); } // toMarkdown() diff --git a/src/graphql/schema/dashboards.ts b/src/graphql/schema/dashboards.ts index f9837a6..8138fc2 100644 --- a/src/graphql/schema/dashboards.ts +++ b/src/graphql/schema/dashboards.ts @@ -36,6 +36,7 @@ const DashboardObj = builder.objectRef("Dashboard").implement({ builder.queryField("dashboard", (t) => t.field({ type: DashboardObj, + nullable: true, description: "Look up a single dashboard by its id", args: { id: t.arg({ type: "ID", required: true }), diff --git a/src/graphql/schema/questions.ts b/src/graphql/schema/questions.ts index f3ec7e2..fb28bed 100644 --- a/src/graphql/schema/questions.ts +++ b/src/graphql/schema/questions.ts @@ -140,6 +140,7 @@ builder.queryField("questions", (t) => builder.queryField("question", (t) => t.field({ type: QuestionObj, + nullable: true, description: "Look up a single question by its id", args: { id: t.arg({ type: "ID", required: true }), @@ -149,7 +150,6 @@ builder.queryField("question", (t) => const [platform, id] = [parts[0], parts.slice(1).join("-")]; if (platform === "guesstimate") { const q = await guesstimate.fetchQuestion(Number(id)); - console.log(q); return q; } return await prisma.question.findUnique({ diff --git a/src/graphql/schema/search.ts b/src/graphql/schema/search.ts index 4676b45..6039e21 100644 --- a/src/graphql/schema/search.ts +++ b/src/graphql/schema/search.ts @@ -32,19 +32,19 @@ builder.queryField("searchQuestions", (t) => // defs const query = input.query === undefined ? "" : input.query; if (query === "") return []; - const forecastsThreshold = input.forecastsThreshold; - const starsThreshold = input.starsThreshold; + const { forecastsThreshold, starsThreshold } = input; + const platformsIncludeGuesstimate = input.forecastingPlatforms?.includes("guesstimate") && - starsThreshold <= 1; + (!starsThreshold || starsThreshold <= 1); // preparation const unawaitedAlgoliaResponse = searchWithAlgolia({ queryString: query, - hitsPerPage: input.limit + 50, - starsThreshold, - filterByPlatforms: input.forecastingPlatforms, - forecastsThreshold, + hitsPerPage: input.limit ?? 50, + starsThreshold: starsThreshold ?? undefined, + filterByPlatforms: input.forecastingPlatforms ?? undefined, + forecastsThreshold: forecastsThreshold ?? undefined, }); let results: AlgoliaQuestion[] = []; diff --git a/src/pages/dashboards/embed/[id].tsx b/src/pages/dashboards/embed/[id].tsx index 09bbe79..d91e0ad 100644 --- a/src/pages/dashboards/embed/[id].tsx +++ b/src/pages/dashboards/embed/[id].tsx @@ -1,5 +1,5 @@ import { GetServerSideProps, NextPage } from "next"; -import Error from "next/error"; +import NextError from "next/error"; import { DashboardByIdDocument, DashboardFragment @@ -19,9 +19,13 @@ export const getServerSideProps: GetServerSideProps = async ( const dashboardId = context.query.id as string; const numCols = Number(context.query.numCols); - const dashboard = ( - await client.query(DashboardByIdDocument, { id: dashboardId }).toPromise() - ).data?.result; + const response = await client + .query(DashboardByIdDocument, { id: dashboardId }) + .toPromise(); + if (!response.data) { + throw new Error(`GraphQL query failed: ${response.error}`); + } + const dashboard = response.data.result; if (!dashboard) { context.res.statusCode = 404; @@ -32,14 +36,14 @@ export const getServerSideProps: GetServerSideProps = async ( // reduntant: page component doesn't do graphql requests, but it's still nice/more consistent to have data in cache urqlState: ssrCache.extractData(), dashboard, - numCols: !numCols ? null : numCols < 5 ? numCols : 4, + numCols: !numCols ? undefined : numCols < 5 ? numCols : 4, }, }; }; const EmbedDashboardPage: NextPage = ({ dashboard, numCols }) => { if (!dashboard) { - return ; + return ; } return ( diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b8cb2ae..1ea9ad9 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -45,8 +45,11 @@ export const getServerSideProps: GetServerSideProps = async ( const defaultNumDisplay = 21; const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay; - const defaultResults = (await client.query(FrontpageDocument).toPromise()) - .data.result; + const response = await client.query(FrontpageDocument).toPromise(); + if (!response.data) { + throw new Error(`GraphQL query failed: ${response.error}`); + } + const defaultResults = response.data.result; if ( !!initialQueryParameters && @@ -58,7 +61,7 @@ export const getServerSideProps: GetServerSideProps = async ( .query(SearchDocument, { input: { ...initialQueryParameters, - limit: initialNumDisplay, + limit: initialNumDisplay + 50, }, }) .toPromise(); diff --git a/src/pages/secretEmbed.tsx b/src/pages/secretEmbed.tsx index fd7f222..7420734 100644 --- a/src/pages/secretEmbed.tsx +++ b/src/pages/secretEmbed.tsx @@ -29,16 +29,19 @@ export const getServerSideProps: GetServerSideProps = async ( let results: QuestionFragment[] = []; if (initialQueryParameters.query !== "") { - results = ( - await client - .query(SearchDocument, { - input: { - ...initialQueryParameters, - limit: 1, - }, - }) - .toPromise() - ).data.result; + const response = await client + .query(SearchDocument, { + input: { + ...initialQueryParameters, + limit: 1, + }, + }) + .toPromise(); + if (response.data?.result) { + results = response.data.result; + } else { + throw new Error("GraphQL request failed"); + } } return { diff --git a/src/web/common/Layout.tsx b/src/web/common/Layout.tsx index 4d1e84f..084daac 100644 --- a/src/web/common/Layout.tsx +++ b/src/web/common/Layout.tsx @@ -2,7 +2,7 @@ import Head from "next/head"; import Link from "next/link"; import React, { ErrorInfo } from "react"; -import { Logo2 } from "../icons/index"; +import { Logo2 } from "../icons"; interface MenuItem { page: string; diff --git a/src/web/common/MultiSelectPlatform.tsx b/src/web/common/MultiSelectPlatform.tsx index cc4436e..a458faf 100644 --- a/src/web/common/MultiSelectPlatform.tsx +++ b/src/web/common/MultiSelectPlatform.tsx @@ -87,7 +87,7 @@ export const MultiSelectPlatform: React.FC = ({ const selectValue = value.map((v) => id2option[v]).filter((v) => v); - const onSelectChange = (newValue: Option[]) => { + const onSelectChange = (newValue: readonly Option[]) => { onChange(newValue.map((o) => o.value)); }; diff --git a/src/web/display/DashboardCreator.tsx b/src/web/display/DashboardCreator.tsx index 48ddde5..7014caf 100644 --- a/src/web/display/DashboardCreator.tsx +++ b/src/web/display/DashboardCreator.tsx @@ -37,7 +37,9 @@ export const DashboardCreator: React.FC = ({ handleSubmit }) => { } } catch (error) { setActing(false); - const substituteText = `Error: ${error.message} + const substituteText = `Error: ${ + error instanceof Error ? error.message : "Unknown" + } Try something like: ${exampleInput} diff --git a/src/web/icons/Favicon.tsx b/src/web/icons/Favicon.tsx index 9bee8ac..412723f 100644 --- a/src/web/icons/Favicon.tsx +++ b/src/web/icons/Favicon.tsx @@ -1,6 +1,4 @@ -import * as React from "react"; - -function SvgFavicon(props) { +export const Favicon: React.FC> = (props) => { return ( ); -} - -export default SvgFavicon; +}; diff --git a/src/web/icons/Logo.tsx b/src/web/icons/Logo.tsx index 0633163..9fe8fa9 100644 --- a/src/web/icons/Logo.tsx +++ b/src/web/icons/Logo.tsx @@ -1,6 +1,4 @@ -import * as React from "react"; - -function SvgLogo(props) { +export const Logo: React.FC> = (props) => { return ( ); -} - -export default SvgLogo; +}; diff --git a/src/web/icons/Logo2.tsx b/src/web/icons/Logo2.tsx index 43709c6..dd2ce86 100644 --- a/src/web/icons/Logo2.tsx +++ b/src/web/icons/Logo2.tsx @@ -1,6 +1,4 @@ -import * as React from "react"; - -function SvgLogo2(props) { +export const Logo2: React.FC> = (props) => { return ( ); -} - -export default SvgLogo2; +}; diff --git a/src/web/icons/index.ts b/src/web/icons/index.ts index 7fb9e03..843cc09 100644 --- a/src/web/icons/index.ts +++ b/src/web/icons/index.ts @@ -1,3 +1,3 @@ -export { default as Favicon } from "./Favicon"; -export { default as Logo } from "./Logo"; -export { default as Logo2 } from "./Logo2"; +export { Favicon } from "./Favicon"; +export { Logo } from "./Logo"; +export { Logo2 } from "./Logo2"; diff --git a/src/web/questions/components/HistoryChart/InnerChart.tsx b/src/web/questions/components/HistoryChart/InnerChart.tsx index 826c46d..c25edb2 100644 --- a/src/web/questions/components/HistoryChart/InnerChart.tsx +++ b/src/web/questions/components/HistoryChart/InnerChart.tsx @@ -34,10 +34,12 @@ const getVictoryGroup = ({ ); }; -export const InnerChart: React.FC<{ +export type Props = { data: ChartData; highlight: number | undefined; -}> = ({ +}; + +export const InnerChart: React.FC = ({ data: { maxProbability, seriesList, minDate, maxDate }, highlight, }) => { @@ -120,7 +122,7 @@ export const InnerChart: React.FC<{ void }> = ({ onHighlight, }) => { const { x, y, reference, floating, strategy } = useFloating({ - // placement: "right", middleware: [shift()], }); const [showTooltip, setShowTooltip] = useState(false); - const textRef = useRef(); + const textRef = useRef(null); const onHover = () => { - if (textRef.current.scrollWidth > textRef.current.clientWidth) { + if ( + textRef.current && + textRef.current.scrollWidth > textRef.current.clientWidth + ) { setShowTooltip(true); } onHighlight(); diff --git a/src/web/questions/components/HistoryChart/index.tsx b/src/web/questions/components/HistoryChart/index.tsx index 0884adc..7d512dd 100644 --- a/src/web/questions/components/HistoryChart/index.tsx +++ b/src/web/questions/components/HistoryChart/index.tsx @@ -2,11 +2,12 @@ import dynamic from "next/dynamic"; import React, { useMemo, useState } from "react"; import { QuestionWithHistoryFragment } from "../../../fragments.generated"; +import { Props as InnerChartProps } from "./InnerChart"; // hopefully doesn't import code, just types - need to check import { InnerChartPlaceholder } from "./InnerChartPlaceholder"; import { Legend } from "./Legend"; import { buildChartData, chartColors } from "./utils"; -const InnerChart = dynamic( +const InnerChart = dynamic( () => import("./InnerChart").then((mod) => mod.InnerChart), { ssr: false, loading: () => } ); diff --git a/src/web/questions/components/HistoryChart/utils.ts b/src/web/questions/components/HistoryChart/utils.ts index 886bdfb..3072809 100644 --- a/src/web/questions/components/HistoryChart/utils.ts +++ b/src/web/questions/components/HistoryChart/utils.ts @@ -1,6 +1,8 @@ import { addDays, startOfDay, startOfToday, startOfTomorrow } from "date-fns"; import { QuestionWithHistoryFragment } from "../../../fragments.generated"; +import { isQuestionBinary } from "../../../utils"; +import { isFullQuestionOption } from "../../utils"; export type ChartSeries = { x: Date; y: number; name: string }[]; @@ -33,6 +35,7 @@ export const buildChartData = ( question: QuestionWithHistoryFragment ): ChartData => { let seriesNames = question.options + .filter(isFullQuestionOption) .sort((a, b) => { if (a.probability > b.probability) { return -1; @@ -44,9 +47,7 @@ export const buildChartData = ( .map((o) => o.name) .slice(0, MAX_LINES); - const isBinary = - (seriesNames[0] === "Yes" && seriesNames[1] === "No") || - (seriesNames[0] === "No" && seriesNames[1] === "Yes"); + const isBinary = isQuestionBinary(question); if (isBinary) { seriesNames = ["Yes"]; } @@ -69,6 +70,9 @@ export const buildChartData = ( const date = new Date(item.timestamp * 1000); for (const option of item.options) { + if (option.name == null || option.probability == null) { + continue; + } const idx = nameToIndex[option.name]; if (idx === undefined) { continue; diff --git a/src/web/questions/components/IndicatorsTable.tsx b/src/web/questions/components/IndicatorsTable.tsx index 6fce458..81099a0 100644 --- a/src/web/questions/components/IndicatorsTable.tsx +++ b/src/web/questions/components/IndicatorsTable.tsx @@ -45,18 +45,18 @@ export const IndicatorsTable: React.FC = ({ question }) => ( ) : null} {Object.keys(question.qualityIndicators) .filter( - (indicator) => - question.qualityIndicators[indicator] != null && - !!qualityIndicatorLabels[indicator] + (indicator): indicator is UsedIndicatorName => + (question.qualityIndicators as any)[indicator] != null && + indicator in qualityIndicatorLabels ) - .map((indicator: UsedIndicatorName) => { + .map((indicator) => { return ( {formatIndicatorValue( - question.qualityIndicators[indicator], + Number(question.qualityIndicators[indicator]), // must be non-null due to former check indicator, question.platform.id )} diff --git a/src/web/questions/components/QuestionCard/QuestionFooter.tsx b/src/web/questions/components/QuestionCard/QuestionFooter.tsx index 1b5a233..77d3168 100644 --- a/src/web/questions/components/QuestionCard/QuestionFooter.tsx +++ b/src/web/questions/components/QuestionCard/QuestionFooter.tsx @@ -30,13 +30,17 @@ export const qualityIndicatorLabels: { [k in UsedIndicatorName]: string } = { openInterest: "Interest", }; -const formatNumber = (num) => { - if (Number(num) < 1000) { - return Number(num).toFixed(0); +const isUsedIndicatorName = (name: string): name is UsedIndicatorName => { + return name in qualityIndicatorLabels; +}; + +const formatNumber = (num: number) => { + if (num < 1000) { + return num.toFixed(0); } else if (num < 10000) { - return (Number(num) / 1000).toFixed(1) + "k"; + return (num / 1000).toFixed(1) + "k"; } else { - return (Number(num) / 1000).toFixed(0) + "k"; + return (num / 1000).toFixed(0) + "k"; } }; @@ -100,7 +104,7 @@ const FirstQualityIndicator: React.FC<{ }; export const formatIndicatorValue = ( - value: any, + value: number, indicator: UsedIndicatorName, platform: string ): string => { @@ -119,21 +123,26 @@ const QualityIndicatorsList: React.FC<{ return (
- {Object.entries(question.qualityIndicators).map((entry, i) => { - const indicatorLabel = qualityIndicatorLabels[entry[0]]; - if (!indicatorLabel || entry[1] === null) return; - const indicator = entry[0] as UsedIndicatorName; // guaranteed by the previous line - const value = entry[1]; + {Object.entries(question.qualityIndicators).map( + ([indicator, value], i) => { + if (!isUsedIndicatorName(indicator)) return; + const indicatorLabel = qualityIndicatorLabels[indicator]; + if (!indicatorLabel || value === null) return; - return ( -
- {indicatorLabel}:  - - {formatIndicatorValue(value, indicator, question.platform.id)} - -
- ); - })} + return ( +
+ {indicatorLabel}:  + + {formatIndicatorValue( + Number(value), + indicator, + question.platform.id + )} + +
+ ); + } + )}
); }; diff --git a/src/web/questions/components/QuestionCard/index.tsx b/src/web/questions/components/QuestionCard/index.tsx index f1d9eef..0cba25b 100644 --- a/src/web/questions/components/QuestionCard/index.tsx +++ b/src/web/questions/components/QuestionCard/index.tsx @@ -13,62 +13,25 @@ const truncateText = (length: number, text: string): string => { if (!text) { return ""; } - if (!!text && text.length <= length) { + if (text.length <= length) { return text; } const breakpoints = " .!?"; - let lastLetter = null; - let lastIndex = null; + let lastLetter: string | undefined = undefined; + let lastIndex: number | undefined = undefined; for (let index = length; index > 0; index--) { - let letter = text[index]; + const letter = text[index]; if (breakpoints.includes(letter)) { lastLetter = letter; lastIndex = index; break; } } - let truncatedText = !!text.slice - ? text.slice(0, lastIndex) + (lastLetter != "." ? "..." : "..") - : ""; + let truncatedText = + text.slice(0, lastIndex) + (lastLetter != "." ? "..." : ".."); return truncatedText; }; -// replaceAll polyfill -function escapeRegExp(string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string -} - -function replaceAll( - originalString: string, - pattern: string | RegExp, - substitute -) { - return originalString.replace( - new RegExp(escapeRegExp(pattern), "g"), - substitute - ); -} - -if (!String.prototype.replaceAll) { - String.prototype.replaceAll = function ( - pattern: string | RegExp, - substitute - ) { - let originalString = this; - - // If a regex pattern - if ( - Object.prototype.toString.call(pattern).toLowerCase() === - "[object regexp]" - ) { - return originalString.replace(pattern, substitute); - } - - // If a string - return replaceAll(originalString, pattern, substitute); - }; -} - // Auxiliary components const DisplayMarkdown: React.FC<{ description: string }> = ({ @@ -153,14 +116,14 @@ export const QuestionCard: React.FC = ({ {isBinary ? (
- +
) : (
- +
diff --git a/src/web/questions/components/QuestionIndicators.tsx b/src/web/questions/components/QuestionIndicators.tsx deleted file mode 100644 index ecd37da..0000000 --- a/src/web/questions/components/QuestionIndicators.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { QuestionFragment } from "../../fragments.generated"; - -type QualityIndicator = QuestionFragment["qualityIndicators"]; -type IndicatorName = keyof QualityIndicator; - -// this duplication can probably be simplified with typescript magic, but this is good enough for now -type UsedIndicatorName = - | "volume" - | "numForecasters" - | "spread" - | "sharesVolume" - | "liquidity" - | "tradeVolume" - | "openInterest"; - -const qualityIndicatorLabels: { [k in UsedIndicatorName]: string } = { - // numForecasts: null, - // stars: null, - // yesBid: "Yes bid", - // yesAsk: "Yes ask", - volume: "Volume", - numForecasters: "Forecasters", - spread: "Spread", - sharesVolume: "Shares vol.", - liquidity: "Liquidity", - tradeVolume: "Volume", - openInterest: "Interest", -}; - -const formatNumber = (num) => { - if (Number(num) < 1000) { - return Number(num).toFixed(0); - } else if (num < 10000) { - return (Number(num) / 1000).toFixed(1) + "k"; - } else { - return (Number(num) / 1000).toFixed(0) + "k"; - } -}; - -/* Display functions*/ - -const getPercentageSymbolIfNeeded = ({ - indicator, - platform, -}: { - indicator: UsedIndicatorName; - platform: string; -}) => { - let indicatorsWhichNeedPercentageSymbol: IndicatorName[] = ["spread"]; - if (indicatorsWhichNeedPercentageSymbol.includes(indicator)) { - return "%"; - } else { - return ""; - } -}; - -const getCurrencySymbolIfNeeded = ({ - indicator, - platform, -}: { - indicator: UsedIndicatorName; - platform: string; -}) => { - const indicatorsWhichNeedCurrencySymbol: IndicatorName[] = [ - "volume", - "tradeVolume", - "openInterest", - "liquidity", - ]; - let dollarPlatforms = ["predictit", "kalshi", "polymarket"]; - if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) { - if (dollarPlatforms.includes(platform)) { - return "$"; - } else { - return "£"; - } - } else { - return ""; - } -}; - -const FirstQualityIndicator: React.FC<{ - question: QuestionFragment; -}> = ({ question }) => { - if (question.qualityIndicators.numForecasts) { - return ( -
- Forecasts:  - - {Number(question.qualityIndicators.numForecasts).toFixed(0)} - -
- ); - } else { - return null; - } -}; - -const QualityIndicatorsList: React.FC<{ - question: QuestionFragment; -}> = ({ question }) => { - return ( -
- - {Object.entries(question.qualityIndicators).map((entry, i) => { - const indicatorLabel = qualityIndicatorLabels[entry[0]]; - if (!indicatorLabel || entry[1] === null) return; - const indicator = entry[0] as UsedIndicatorName; // guaranteed by the previous line - const value = entry[1]; - - return ( -
- {indicatorLabel}:  - - {`${getCurrencySymbolIfNeeded({ - indicator, - platform: question.platform.id, - })}${formatNumber(value)}${getPercentageSymbolIfNeeded({ - indicator, - platform: question.platform.id, - })}`} - -
- ); - })} -
- ); -}; - -// Database-like functions -export function getstars(numstars: number) { - let stars = "★★☆☆☆"; - switch (numstars) { - case 0: - stars = "☆☆☆☆☆"; - break; - case 1: - stars = "★☆☆☆☆"; - break; - case 2: - stars = "★★☆☆☆"; - break; - case 3: - stars = "★★★☆☆"; - break; - case 4: - stars = "★★★★☆"; - break; - case 5: - stars = "★★★★★"; - break; - default: - stars = "★★☆☆☆"; - } - return stars; -} - -function getStarsColor(numstars: number) { - let color = "text-yellow-400"; - switch (numstars) { - case 0: - color = "text-red-400"; - break; - case 1: - color = "text-red-400"; - break; - case 2: - color = "text-orange-400"; - break; - case 3: - color = "text-yellow-400"; - break; - case 4: - color = "text-green-400"; - break; - case 5: - color = "text-blue-400"; - break; - default: - color = "text-yellow-400"; - } - return color; -} - -interface Props { - question: QuestionFragment; - expandFooterToFullWidth: boolean; -} - -export const QuestionFooter: React.FC = ({ - question, - expandFooterToFullWidth, -}) => { - return ( -
-
- {getstars(question.qualityIndicators.stars)} -
-
- {question.platform.label - .replace("Good Judgment Open", "GJOpen") - .replace(/ /g, "\u00a0")} -
-
- -
-
- ); -}; diff --git a/src/web/questions/components/QuestionOptions.tsx b/src/web/questions/components/QuestionOptions.tsx index 833e121..c227f10 100644 --- a/src/web/questions/components/QuestionOptions.tsx +++ b/src/web/questions/components/QuestionOptions.tsx @@ -1,7 +1,6 @@ import { QuestionFragment } from "../../fragments.generated"; -import { formatProbability } from "../utils"; - -type Option = QuestionFragment["options"][0]; +import { isQuestionBinary } from "../../utils"; +import { formatProbability, FullQuestionOption, isFullQuestionOption } from "../utils"; const textColor = (probability: number) => { if (probability < 0.03) { @@ -89,7 +88,7 @@ const chooseColor = (probability: number) => { } }; -const OptionRow: React.FC<{ option: Option }> = ({ option }) => { +const OptionRow: React.FC<{ option: FullQuestionOption }> = ({ option }) => { return (
= ({ option }) => { ); }; -export const QuestionOptions: React.FC<{ options: Option[] }> = ({ - options, +export const QuestionOptions: React.FC<{ question: QuestionFragment }> = ({ + question, }) => { - const isBinary = - options.length === 2 && - (options[0].name === "Yes" || options[0].name === "No"); + const isBinary = isQuestionBinary(question); if (isBinary) { - const yesOption = options.find((o) => o.name === "Yes"); + const yesOption = question.options.find((o) => o.name === "Yes"); + if (!yesOption) { + return null; // shouldn't happen + } + if (!isFullQuestionOption(yesOption)) { + return null; // missing data + } return (
= ({
); } else { - const optionsSorted = options.sort((a, b) => b.probability - a.probability); - const optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options. + const optionsSorted = question.options + .filter(isFullQuestionOption) + .sort((a, b) => b.probability - a.probability); + + const optionsMax5 = optionsSorted.slice(0, 5); // display max 5 options. return (
diff --git a/src/web/questions/utils.ts b/src/web/questions/utils.ts index f7f4f1c..36fa46b 100644 --- a/src/web/questions/utils.ts +++ b/src/web/questions/utils.ts @@ -8,3 +8,20 @@ export const formatProbability = (probability: number) => { : percentage.toFixed(0) + "%"; return percentageCapped; }; + +import { QuestionFragment } from "../fragments.generated"; + +export type QuestionOption = QuestionFragment["options"][0]; +export type FullQuestionOption = Exclude< + QuestionOption, + "name" | "probability" +> & { + name: NonNullable; + probability: NonNullable; +}; + +export const isFullQuestionOption = ( + option: QuestionOption +): option is FullQuestionOption => { + return option.name != null && option.probability != null; +}; diff --git a/src/web/search/components/SearchScreen.tsx b/src/web/search/components/SearchScreen.tsx index 31d09c7..56200ff 100644 --- a/src/web/search/components/SearchScreen.tsx +++ b/src/web/search/components/SearchScreen.tsx @@ -61,7 +61,7 @@ export const SearchScreen: React.FC = ({ variables: { input: { ...queryParameters, - limit: numDisplay, + limit: numDisplay + 50, }, }, pause: !isFirstRender, diff --git a/src/web/utils.ts b/src/web/utils.ts index f4c5c81..70f8945 100644 --- a/src/web/utils.ts +++ b/src/web/utils.ts @@ -1,3 +1,5 @@ +import { QuestionFragment } from "./fragments.generated"; + export const getBasePath = () => { if (process.env.NEXT_PUBLIC_VERCEL_URL) { return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`; @@ -29,3 +31,12 @@ export const cleanText = (text: string): string => { //console.log(textString) return textString; }; + +export const isQuestionBinary = (question: QuestionFragment): boolean => { + const { options } = question; + return ( + options.length === 2 && + ((options[0].name === "Yes" && options[1].name === "No") || + (options[0].name === "No" && options[1].name === "Yes")) + ); +}; diff --git a/src/web/worker/searchWithAlgolia.ts b/src/web/worker/searchWithAlgolia.ts index 7b53b14..8f0d4ca 100644 --- a/src/web/worker/searchWithAlgolia.ts +++ b/src/web/worker/searchWithAlgolia.ts @@ -13,9 +13,9 @@ const index = client.initIndex("metaforecast"); interface SearchOpts { queryString: string; hitsPerPage?: number; - starsThreshold: number; - filterByPlatforms: string[]; - forecastsThreshold: number; + starsThreshold?: number; + filterByPlatforms?: string[]; + forecastsThreshold?: number; } const buildFilter = ({ @@ -33,7 +33,7 @@ const buildFilter = ({ ? filterByPlatforms.map((platform) => `platform:"${platform}"`).join(" OR ") : null; const numForecastsFilter = - forecastsThreshold > 0 + forecastsThreshold && forecastsThreshold > 0 ? `qualityindicators.numforecasts >= ${forecastsThreshold}` : null; const finalFilter = [starsFilter, platformsFilter, numForecastsFilter]