refactor: more typescript

This commit is contained in:
Vyacheslav Matyukhin 2022-05-09 23:27:51 +04:00
parent 439a9045da
commit f6e2e8cfa1
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
34 changed files with 209 additions and 463 deletions

23
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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,

View File

@ -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 = {

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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
export default function toMarkdown(htmlText: string) {
let html2 = htmlText.replaceAll(`='`, `="`).replaceAll(`'>`, `">`);
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})`;
},
};
/* Support functions */
/* Body */
export default function toMarkdown(htmlText) {
let html2 = htmlText.replaceAll(`='`, `="`).replaceAll(`'>`, `">`);
return textVersion(html2, styleConfig);
});
}
// toMarkdown()

View File

@ -36,6 +36,7 @@ const DashboardObj = builder.objectRef<Dashboard>("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 }),

View File

@ -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({

View File

@ -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[] = [];

View File

@ -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<Props> = 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<Props> = 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<Props> = ({ dashboard, numCols }) => {
if (!dashboard) {
return <Error statusCode={404} />;
return <NextError statusCode={404} />;
}
return (

View File

@ -45,8 +45,11 @@ export const getServerSideProps: GetServerSideProps<Props> = 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<Props> = async (
.query(SearchDocument, {
input: {
...initialQueryParameters,
limit: initialNumDisplay,
limit: initialNumDisplay + 50,
},
})
.toPromise();

View File

@ -29,16 +29,19 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
let results: QuestionFragment[] = [];
if (initialQueryParameters.query !== "") {
results = (
await client
const response = await client
.query(SearchDocument, {
input: {
...initialQueryParameters,
limit: 1,
},
})
.toPromise()
).data.result;
.toPromise();
if (response.data?.result) {
results = response.data.result;
} else {
throw new Error("GraphQL request failed");
}
}
return {

View File

@ -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;

View File

@ -87,7 +87,7 @@ export const MultiSelectPlatform: React.FC<Props> = ({
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));
};

View File

@ -37,7 +37,9 @@ export const DashboardCreator: React.FC<Props> = ({ handleSubmit }) => {
}
} catch (error) {
setActing(false);
const substituteText = `Error: ${error.message}
const substituteText = `Error: ${
error instanceof Error ? error.message : "Unknown"
}
Try something like:
${exampleInput}

View File

@ -1,6 +1,4 @@
import * as React from "react";
function SvgFavicon(props) {
export const Favicon: React.FC<React.SVGAttributes<SVGElement>> = (props) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
@ -11,6 +9,4 @@ function SvgFavicon(props) {
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm198.4-588.1a32 32 0 00-24.5.5L414.9 415 296.4 686c-3.6 8.2-3.6 17.5 0 25.7 3.4 7.8 9.7 13.9 17.7 17 3.8 1.5 7.7 2.2 11.7 2.2 4.4 0 8.7-.9 12.8-2.7l271-118.6 118.5-271a32.06 32.06 0 00-17.7-42.7zM576.8 534.4l26.2 26.2-42.4 42.4-26.2-26.2L380 644.4 447.5 490 422 464.4l42.4-42.4 25.5 25.5L644.4 380l-67.6 154.4zM464.4 422L422 464.4l25.5 25.6 86.9 86.8 26.2 26.2 42.4-42.4-26.2-26.2-86.8-86.9z" />
</svg>
);
}
export default SvgFavicon;
};

View File

@ -1,6 +1,4 @@
import * as React from "react";
function SvgLogo(props) {
export const Logo: React.FC<React.SVGAttributes<SVGElement>> = (props) => {
return (
<svg
width={1333.333}
@ -76,6 +74,4 @@ function SvgLogo(props) {
/>
</svg>
);
}
export default SvgLogo;
};

View File

@ -1,6 +1,4 @@
import * as React from "react";
function SvgLogo2(props) {
export const Logo2: React.FC<React.SVGAttributes<SVGElement>> = (props) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
@ -69,6 +67,4 @@ function SvgLogo2(props) {
/>
</svg>
);
}
export default SvgLogo2;
};

View File

@ -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";

View File

@ -34,10 +34,12 @@ const getVictoryGroup = ({
);
};
export const InnerChart: React.FC<{
export type Props = {
data: ChartData;
highlight: number | undefined;
}> = ({
};
export const InnerChart: React.FC<Props> = ({
data: { maxProbability, seriesList, minDate, maxDate },
highlight,
}) => {
@ -120,7 +122,7 @@ export const InnerChart: React.FC<{
<VictoryAxis
tickCount={Math.min(7, differenceInDays(maxDate, minDate) + 1)}
style={{
grid: { stroke: null, strokeWidth: 0.5 },
grid: { strokeWidth: 0.5 },
}}
tickLabelComponent={
<VictoryLabel

View File

@ -12,15 +12,17 @@ const LegendItem: React.FC<{ item: Item; onHighlight: () => void }> = ({
onHighlight,
}) => {
const { x, y, reference, floating, strategy } = useFloating({
// placement: "right",
middleware: [shift()],
});
const [showTooltip, setShowTooltip] = useState(false);
const textRef = useRef<HTMLDivElement>();
const textRef = useRef<HTMLDivElement>(null);
const onHover = () => {
if (textRef.current.scrollWidth > textRef.current.clientWidth) {
if (
textRef.current &&
textRef.current.scrollWidth > textRef.current.clientWidth
) {
setShowTooltip(true);
}
onHighlight();

View File

@ -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<InnerChartProps>(
() => import("./InnerChart").then((mod) => mod.InnerChart),
{ ssr: false, loading: () => <InnerChartPlaceholder /> }
);

View File

@ -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;

View File

@ -45,18 +45,18 @@ export const IndicatorsTable: React.FC<Props> = ({ 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 (
<TableRow
title={qualityIndicatorLabels[indicator]}
key={indicator}
>
{formatIndicatorValue(
question.qualityIndicators[indicator],
Number(question.qualityIndicators[indicator]), // must be non-null due to former check
indicator,
question.platform.id
)}

View File

@ -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 (
<div className="text-sm">
<FirstQualityIndicator question={question} />
{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 (
<div key={indicator}>
<span>{indicatorLabel}:</span>&nbsp;
<span className="font-bold">
{formatIndicatorValue(value, indicator, question.platform.id)}
{formatIndicatorValue(
Number(value),
indicator,
question.platform.id
)}
</span>
</div>
);
})}
}
)}
</div>
);
};

View File

@ -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<Props> = ({
</div>
{isBinary ? (
<div className="flex justify-between">
<QuestionOptions options={options} />
<QuestionOptions question={question} />
<div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}>
<LastUpdated timestamp={lastUpdated} />
</div>
</div>
) : (
<div className="space-y-2">
<QuestionOptions options={options} />
<QuestionOptions question={question} />
<div className={`hidden ${showTimeStamp ? "sm:block" : ""} ml-6`}>
<LastUpdated timestamp={lastUpdated} />
</div>

View File

@ -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 (
<div className="flex">
<span>Forecasts:</span>&nbsp;
<span className="font-bold">
{Number(question.qualityIndicators.numForecasts).toFixed(0)}
</span>
</div>
);
} else {
return null;
}
};
const QualityIndicatorsList: React.FC<{
question: QuestionFragment;
}> = ({ question }) => {
return (
<div className="text-sm">
<FirstQualityIndicator question={question} />
{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 (
<div key={indicator}>
<span>{indicatorLabel}:</span>&nbsp;
<span className="font-bold">
{`${getCurrencySymbolIfNeeded({
indicator,
platform: question.platform.id,
})}${formatNumber(value)}${getPercentageSymbolIfNeeded({
indicator,
platform: question.platform.id,
})}`}
</span>
</div>
);
})}
</div>
);
};
// 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<Props> = ({
question,
expandFooterToFullWidth,
}) => {
return (
<div
className={`grid grid-cols-3 ${
expandFooterToFullWidth ? "justify-between" : ""
} text-gray-500 mb-2 mt-1`}
>
<div
className={`self-center col-span-1 ${getStarsColor(
question.qualityIndicators.stars
)}`}
>
{getstars(question.qualityIndicators.stars)}
</div>
<div
className={`${
expandFooterToFullWidth ? "place-self-center" : "self-center"
} col-span-1 font-bold`}
>
{question.platform.label
.replace("Good Judgment Open", "GJOpen")
.replace(/ /g, "\u00a0")}
</div>
<div
className={`${
expandFooterToFullWidth
? "justify-self-end mr-4"
: "justify-self-center"
} col-span-1`}
>
<QualityIndicatorsList question={question} />
</div>
</div>
);
};

View File

@ -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 (
<div className="flex items-center">
<div
@ -106,15 +105,19 @@ const OptionRow: React.FC<{ option: Option }> = ({ 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 (
<div className="space-x-2">
<span
@ -134,8 +137,11 @@ export const QuestionOptions: React.FC<{ options: Option[] }> = ({
</div>
);
} 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 (
<div className="space-y-2">

View File

@ -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<QuestionOption["name"]>;
probability: NonNullable<QuestionOption["probability"]>;
};
export const isFullQuestionOption = (
option: QuestionOption
): option is FullQuestionOption => {
return option.name != null && option.probability != null;
};

View File

@ -61,7 +61,7 @@ export const SearchScreen: React.FC<Props> = ({
variables: {
input: {
...queryParameters,
limit: numDisplay,
limit: numDisplay + 50,
},
},
pause: !isFirstRender,

View File

@ -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"))
);
};

View File

@ -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]