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, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "metaforecast",
"version": "2.0.0", "version": "2.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -17,11 +16,13 @@
"@prisma/client": "^3.11.1", "@prisma/client": "^3.11.1",
"@tailwindcss/forms": "^0.4.0", "@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.1", "@tailwindcss/typography": "^0.5.1",
"@types/chroma-js": "^2.1.3",
"@types/dom-to-image": "^2.6.4", "@types/dom-to-image": "^2.6.4",
"@types/jsdom": "^16.2.14", "@types/jsdom": "^16.2.14",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",
"@types/react-copy-to-clipboard": "^5.0.2", "@types/react-copy-to-clipboard": "^5.0.2",
"@types/textversionjs": "^1.1.1",
"airtable": "^0.11.1", "airtable": "^0.11.1",
"algoliasearch": "^4.10.3", "algoliasearch": "^4.10.3",
"autoprefixer": "^10.1.0", "autoprefixer": "^10.1.0",
@ -3170,6 +3171,11 @@
"@types/responselike": "*" "@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": { "node_modules/@types/cross-spawn": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz",
@ -3354,6 +3360,11 @@
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
"license": "MIT" "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": { "node_modules/@types/tough-cookie": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz",
@ -42405,6 +42416,11 @@
"@types/responselike": "*" "@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": { "@types/cross-spawn": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz", "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", "resolved": "https://registry.npmjs.org/@types%2fscheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" "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": { "@types/tough-cookie": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", "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", "@prisma/client": "^3.11.1",
"@tailwindcss/forms": "^0.4.0", "@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.1", "@tailwindcss/typography": "^0.5.1",
"@types/chroma-js": "^2.1.3",
"@types/dom-to-image": "^2.6.4", "@types/dom-to-image": "^2.6.4",
"@types/jsdom": "^16.2.14", "@types/jsdom": "^16.2.14",
"@types/nprogress": "^0.2.0", "@types/nprogress": "^0.2.0",
"@types/react": "^17.0.39", "@types/react": "^17.0.39",
"@types/react-copy-to-clipboard": "^5.0.2", "@types/react-copy-to-clipboard": "^5.0.2",
"@types/textversionjs": "^1.1.1",
"airtable": "^0.11.1", "airtable": "^0.11.1",
"algoliasearch": "^4.10.3", "algoliasearch": "^4.10.3",
"autoprefixer": "^10.1.0", "autoprefixer": "^10.1.0",

View File

@ -42,7 +42,7 @@ async function fetchPredictions() {
const agent = new https.Agent({ const agent = new https.Agent({
rejectUnauthorized: false, rejectUnauthorized: false,
}); });
let response = await axios({ const response = await axios({
url: endpoint, url: endpoint,
method: "GET", method: "GET",
headers: { headers: {
@ -115,18 +115,19 @@ async function processPredictions(data) {
if (rules == undefined) { if (rules == undefined) {
// console.log(prediction.description) // console.log(prediction.description)
} }
let title = rules.split("? ")[0] + "?"; let title = rules.split("? ")[0] + "?";
let description = rules.split("? ")[1].trim(); let description = rules.split("? ")[1].trim();
if (title.includes("of the named")) { if (title.includes("of the named")) {
title = prediction.marketName + ": " + title; title = prediction.marketName + ": " + title;
} }
let result = { const result = {
id: id, id,
title: title, title,
url: `https://www.betfair.com/exchange/plus/politics/market/${prediction.marketId}`, url: `https://www.betfair.com/exchange/plus/politics/market/${prediction.marketId}`,
platform: platformName, platform: platformName,
description: description, description,
options: options, options,
qualityindicators: { qualityindicators: {
stars: calculateStars(platformName, { stars: calculateStars(platformName, {
volume: prediction.totalMatched, volume: prediction.totalMatched,

View File

@ -9,10 +9,7 @@ import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
const platformName = "goodjudgment"; const platformName = "goodjudgment";
let endpoint = "https://goodjudgment.io/superforecasts/"; const endpoint = "https://goodjudgment.io/superforecasts/";
String.prototype.replaceAll = function replaceAll(search, replace) {
return this.split(search).join(replace);
};
/* Body */ /* Body */
export const goodjudgment: Platform = { export const goodjudgment: Platform = {

View File

@ -9,10 +9,7 @@ import { FetchedQuestion, Platform } from "./";
/* Definitions */ /* Definitions */
const platformName = "infer"; const platformName = "infer";
let htmlEndPoint = "https://www.infer-pub.com/questions"; const htmlEndPoint = "https://www.infer-pub.com/questions";
String.prototype.replaceAll = function replaceAll(search, replace) {
return this.split(search).join(replace);
};
const DEBUG_MODE: "on" | "off" = "off"; // "off" const DEBUG_MODE: "on" | "off" = "off"; // "off"
const SLEEP_TIME_RANDOM = 7000; // miliseconds const SLEEP_TIME_RANDOM = 7000; // miliseconds
const SLEEP_TIME_EXTRA = 2000; 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 average = (array: number[]) =>
let stars = "★★☆☆☆"; array.reduce((a, b) => a + b, 0) / array.length;
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;
function calculateStarsAstralCodexTen(data) { function calculateStarsAstralCodexTen(data) {
let nuno = (data) => 3; let nuno = (data) => 3;

View File

@ -1,26 +1,16 @@
/* Imports */
import textVersion from "textversionjs"; import textVersion from "textversionjs";
/* Definitions */ export default function toMarkdown(htmlText: string) {
String.prototype.replaceAll = function replaceAll(search, replace) { let html2 = htmlText.replaceAll(`='`, `="`).replaceAll(`'>`, `">`);
return this.split(search).join(replace); return textVersion(html2, {
}; linkProcess: (href, linkText) => {
let newHref = href
var styleConfig = { ? href.replace(/\(/g, "%28").replace(/\)/g, "%29")
linkProcess: function (href, linkText) { : "";
let newHref = href ? href.replace(/\(/g, "%28").replace(/\)/g, "%29") : ""; // Deal correctly in markdown with links that contain parenthesis
// Deal corretly in markdown with links that contain parenthesis
return `[${linkText}](${newHref})`; return `[${linkText}](${newHref})`;
}, },
}; });
/* Support functions */
/* Body */
export default function toMarkdown(htmlText) {
let html2 = htmlText.replaceAll(`='`, `="`).replaceAll(`'>`, `">`);
return textVersion(html2, styleConfig);
} }
// toMarkdown() // toMarkdown()

View File

@ -36,6 +36,7 @@ const DashboardObj = builder.objectRef<Dashboard>("Dashboard").implement({
builder.queryField("dashboard", (t) => builder.queryField("dashboard", (t) =>
t.field({ t.field({
type: DashboardObj, type: DashboardObj,
nullable: true,
description: "Look up a single dashboard by its id", description: "Look up a single dashboard by its id",
args: { args: {
id: t.arg({ type: "ID", required: true }), id: t.arg({ type: "ID", required: true }),

View File

@ -140,6 +140,7 @@ builder.queryField("questions", (t) =>
builder.queryField("question", (t) => builder.queryField("question", (t) =>
t.field({ t.field({
type: QuestionObj, type: QuestionObj,
nullable: true,
description: "Look up a single question by its id", description: "Look up a single question by its id",
args: { args: {
id: t.arg({ type: "ID", required: true }), id: t.arg({ type: "ID", required: true }),
@ -149,7 +150,6 @@ builder.queryField("question", (t) =>
const [platform, id] = [parts[0], parts.slice(1).join("-")]; const [platform, id] = [parts[0], parts.slice(1).join("-")];
if (platform === "guesstimate") { if (platform === "guesstimate") {
const q = await guesstimate.fetchQuestion(Number(id)); const q = await guesstimate.fetchQuestion(Number(id));
console.log(q);
return q; return q;
} }
return await prisma.question.findUnique({ return await prisma.question.findUnique({

View File

@ -32,19 +32,19 @@ builder.queryField("searchQuestions", (t) =>
// defs // defs
const query = input.query === undefined ? "" : input.query; const query = input.query === undefined ? "" : input.query;
if (query === "") return []; if (query === "") return [];
const forecastsThreshold = input.forecastsThreshold; const { forecastsThreshold, starsThreshold } = input;
const starsThreshold = input.starsThreshold;
const platformsIncludeGuesstimate = const platformsIncludeGuesstimate =
input.forecastingPlatforms?.includes("guesstimate") && input.forecastingPlatforms?.includes("guesstimate") &&
starsThreshold <= 1; (!starsThreshold || starsThreshold <= 1);
// preparation // preparation
const unawaitedAlgoliaResponse = searchWithAlgolia({ const unawaitedAlgoliaResponse = searchWithAlgolia({
queryString: query, queryString: query,
hitsPerPage: input.limit + 50, hitsPerPage: input.limit ?? 50,
starsThreshold, starsThreshold: starsThreshold ?? undefined,
filterByPlatforms: input.forecastingPlatforms, filterByPlatforms: input.forecastingPlatforms ?? undefined,
forecastsThreshold, forecastsThreshold: forecastsThreshold ?? undefined,
}); });
let results: AlgoliaQuestion[] = []; let results: AlgoliaQuestion[] = [];

View File

@ -1,5 +1,5 @@
import { GetServerSideProps, NextPage } from "next"; import { GetServerSideProps, NextPage } from "next";
import Error from "next/error"; import NextError from "next/error";
import { import {
DashboardByIdDocument, DashboardFragment DashboardByIdDocument, DashboardFragment
@ -19,9 +19,13 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
const dashboardId = context.query.id as string; const dashboardId = context.query.id as string;
const numCols = Number(context.query.numCols); const numCols = Number(context.query.numCols);
const dashboard = ( const response = await client
await client.query(DashboardByIdDocument, { id: dashboardId }).toPromise() .query(DashboardByIdDocument, { id: dashboardId })
).data?.result; .toPromise();
if (!response.data) {
throw new Error(`GraphQL query failed: ${response.error}`);
}
const dashboard = response.data.result;
if (!dashboard) { if (!dashboard) {
context.res.statusCode = 404; 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 // reduntant: page component doesn't do graphql requests, but it's still nice/more consistent to have data in cache
urqlState: ssrCache.extractData(), urqlState: ssrCache.extractData(),
dashboard, dashboard,
numCols: !numCols ? null : numCols < 5 ? numCols : 4, numCols: !numCols ? undefined : numCols < 5 ? numCols : 4,
}, },
}; };
}; };
const EmbedDashboardPage: NextPage<Props> = ({ dashboard, numCols }) => { const EmbedDashboardPage: NextPage<Props> = ({ dashboard, numCols }) => {
if (!dashboard) { if (!dashboard) {
return <Error statusCode={404} />; return <NextError statusCode={404} />;
} }
return ( return (

View File

@ -45,8 +45,11 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
const defaultNumDisplay = 21; const defaultNumDisplay = 21;
const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay; const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay;
const defaultResults = (await client.query(FrontpageDocument).toPromise()) const response = await client.query(FrontpageDocument).toPromise();
.data.result; if (!response.data) {
throw new Error(`GraphQL query failed: ${response.error}`);
}
const defaultResults = response.data.result;
if ( if (
!!initialQueryParameters && !!initialQueryParameters &&
@ -58,7 +61,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
.query(SearchDocument, { .query(SearchDocument, {
input: { input: {
...initialQueryParameters, ...initialQueryParameters,
limit: initialNumDisplay, limit: initialNumDisplay + 50,
}, },
}) })
.toPromise(); .toPromise();

View File

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

View File

@ -2,7 +2,7 @@ import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import React, { ErrorInfo } from "react"; import React, { ErrorInfo } from "react";
import { Logo2 } from "../icons/index"; import { Logo2 } from "../icons";
interface MenuItem { interface MenuItem {
page: string; 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 selectValue = value.map((v) => id2option[v]).filter((v) => v);
const onSelectChange = (newValue: Option[]) => { const onSelectChange = (newValue: readonly Option[]) => {
onChange(newValue.map((o) => o.value)); onChange(newValue.map((o) => o.value));
}; };

View File

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

View File

@ -1,6 +1,4 @@
import * as React from "react"; export const Favicon: React.FC<React.SVGAttributes<SVGElement>> = (props) => {
function SvgFavicon(props) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/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" /> <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> </svg>
); );
} };
export default SvgFavicon;

View File

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

View File

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

View File

@ -1,3 +1,3 @@
export { default as Favicon } from "./Favicon"; export { Favicon } from "./Favicon";
export { default as Logo } from "./Logo"; export { Logo } from "./Logo";
export { default as Logo2 } from "./Logo2"; export { Logo2 } from "./Logo2";

View File

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

View File

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

View File

@ -2,11 +2,12 @@ import dynamic from "next/dynamic";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { QuestionWithHistoryFragment } from "../../../fragments.generated"; 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 { InnerChartPlaceholder } from "./InnerChartPlaceholder";
import { Legend } from "./Legend"; import { Legend } from "./Legend";
import { buildChartData, chartColors } from "./utils"; import { buildChartData, chartColors } from "./utils";
const InnerChart = dynamic( const InnerChart = dynamic<InnerChartProps>(
() => import("./InnerChart").then((mod) => mod.InnerChart), () => import("./InnerChart").then((mod) => mod.InnerChart),
{ ssr: false, loading: () => <InnerChartPlaceholder /> } { ssr: false, loading: () => <InnerChartPlaceholder /> }
); );

View File

@ -1,6 +1,8 @@
import { addDays, startOfDay, startOfToday, startOfTomorrow } from "date-fns"; import { addDays, startOfDay, startOfToday, startOfTomorrow } from "date-fns";
import { QuestionWithHistoryFragment } from "../../../fragments.generated"; import { QuestionWithHistoryFragment } from "../../../fragments.generated";
import { isQuestionBinary } from "../../../utils";
import { isFullQuestionOption } from "../../utils";
export type ChartSeries = { x: Date; y: number; name: string }[]; export type ChartSeries = { x: Date; y: number; name: string }[];
@ -33,6 +35,7 @@ export const buildChartData = (
question: QuestionWithHistoryFragment question: QuestionWithHistoryFragment
): ChartData => { ): ChartData => {
let seriesNames = question.options let seriesNames = question.options
.filter(isFullQuestionOption)
.sort((a, b) => { .sort((a, b) => {
if (a.probability > b.probability) { if (a.probability > b.probability) {
return -1; return -1;
@ -44,9 +47,7 @@ export const buildChartData = (
.map((o) => o.name) .map((o) => o.name)
.slice(0, MAX_LINES); .slice(0, MAX_LINES);
const isBinary = const isBinary = isQuestionBinary(question);
(seriesNames[0] === "Yes" && seriesNames[1] === "No") ||
(seriesNames[0] === "No" && seriesNames[1] === "Yes");
if (isBinary) { if (isBinary) {
seriesNames = ["Yes"]; seriesNames = ["Yes"];
} }
@ -69,6 +70,9 @@ export const buildChartData = (
const date = new Date(item.timestamp * 1000); const date = new Date(item.timestamp * 1000);
for (const option of item.options) { for (const option of item.options) {
if (option.name == null || option.probability == null) {
continue;
}
const idx = nameToIndex[option.name]; const idx = nameToIndex[option.name];
if (idx === undefined) { if (idx === undefined) {
continue; continue;

View File

@ -45,18 +45,18 @@ export const IndicatorsTable: React.FC<Props> = ({ question }) => (
) : null} ) : null}
{Object.keys(question.qualityIndicators) {Object.keys(question.qualityIndicators)
.filter( .filter(
(indicator) => (indicator): indicator is UsedIndicatorName =>
question.qualityIndicators[indicator] != null && (question.qualityIndicators as any)[indicator] != null &&
!!qualityIndicatorLabels[indicator] indicator in qualityIndicatorLabels
) )
.map((indicator: UsedIndicatorName) => { .map((indicator) => {
return ( return (
<TableRow <TableRow
title={qualityIndicatorLabels[indicator]} title={qualityIndicatorLabels[indicator]}
key={indicator} key={indicator}
> >
{formatIndicatorValue( {formatIndicatorValue(
question.qualityIndicators[indicator], Number(question.qualityIndicators[indicator]), // must be non-null due to former check
indicator, indicator,
question.platform.id question.platform.id
)} )}

View File

@ -30,13 +30,17 @@ export const qualityIndicatorLabels: { [k in UsedIndicatorName]: string } = {
openInterest: "Interest", openInterest: "Interest",
}; };
const formatNumber = (num) => { const isUsedIndicatorName = (name: string): name is UsedIndicatorName => {
if (Number(num) < 1000) { return name in qualityIndicatorLabels;
return Number(num).toFixed(0); };
const formatNumber = (num: number) => {
if (num < 1000) {
return num.toFixed(0);
} else if (num < 10000) { } else if (num < 10000) {
return (Number(num) / 1000).toFixed(1) + "k"; return (num / 1000).toFixed(1) + "k";
} else { } 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 = ( export const formatIndicatorValue = (
value: any, value: number,
indicator: UsedIndicatorName, indicator: UsedIndicatorName,
platform: string platform: string
): string => { ): string => {
@ -119,21 +123,26 @@ const QualityIndicatorsList: React.FC<{
return ( return (
<div className="text-sm"> <div className="text-sm">
<FirstQualityIndicator question={question} /> <FirstQualityIndicator question={question} />
{Object.entries(question.qualityIndicators).map((entry, i) => { {Object.entries(question.qualityIndicators).map(
const indicatorLabel = qualityIndicatorLabels[entry[0]]; ([indicator, value], i) => {
if (!indicatorLabel || entry[1] === null) return; if (!isUsedIndicatorName(indicator)) return;
const indicator = entry[0] as UsedIndicatorName; // guaranteed by the previous line const indicatorLabel = qualityIndicatorLabels[indicator];
const value = entry[1]; if (!indicatorLabel || value === null) return;
return ( return (
<div key={indicator}> <div key={indicator}>
<span>{indicatorLabel}:</span>&nbsp; <span>{indicatorLabel}:</span>&nbsp;
<span className="font-bold"> <span className="font-bold">
{formatIndicatorValue(value, indicator, question.platform.id)} {formatIndicatorValue(
Number(value),
indicator,
question.platform.id
)}
</span> </span>
</div> </div>
); );
})} }
)}
</div> </div>
); );
}; };

View File

@ -13,62 +13,25 @@ const truncateText = (length: number, text: string): string => {
if (!text) { if (!text) {
return ""; return "";
} }
if (!!text && text.length <= length) { if (text.length <= length) {
return text; return text;
} }
const breakpoints = " .!?"; const breakpoints = " .!?";
let lastLetter = null; let lastLetter: string | undefined = undefined;
let lastIndex = null; let lastIndex: number | undefined = undefined;
for (let index = length; index > 0; index--) { for (let index = length; index > 0; index--) {
let letter = text[index]; const letter = text[index];
if (breakpoints.includes(letter)) { if (breakpoints.includes(letter)) {
lastLetter = letter; lastLetter = letter;
lastIndex = index; lastIndex = index;
break; break;
} }
} }
let truncatedText = !!text.slice let truncatedText =
? text.slice(0, lastIndex) + (lastLetter != "." ? "..." : "..") text.slice(0, lastIndex) + (lastLetter != "." ? "..." : "..");
: "";
return truncatedText; 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 // Auxiliary components
const DisplayMarkdown: React.FC<{ description: string }> = ({ const DisplayMarkdown: React.FC<{ description: string }> = ({
@ -153,14 +116,14 @@ export const QuestionCard: React.FC<Props> = ({
</div> </div>
{isBinary ? ( {isBinary ? (
<div className="flex justify-between"> <div className="flex justify-between">
<QuestionOptions options={options} /> <QuestionOptions question={question} />
<div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}> <div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}>
<LastUpdated timestamp={lastUpdated} /> <LastUpdated timestamp={lastUpdated} />
</div> </div>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
<QuestionOptions options={options} /> <QuestionOptions question={question} />
<div className={`hidden ${showTimeStamp ? "sm:block" : ""} ml-6`}> <div className={`hidden ${showTimeStamp ? "sm:block" : ""} ml-6`}>
<LastUpdated timestamp={lastUpdated} /> <LastUpdated timestamp={lastUpdated} />
</div> </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 { QuestionFragment } from "../../fragments.generated";
import { formatProbability } from "../utils"; import { isQuestionBinary } from "../../utils";
import { formatProbability, FullQuestionOption, isFullQuestionOption } from "../utils";
type Option = QuestionFragment["options"][0];
const textColor = (probability: number) => { const textColor = (probability: number) => {
if (probability < 0.03) { 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 ( return (
<div className="flex items-center"> <div className="flex items-center">
<div <div
@ -106,15 +105,19 @@ const OptionRow: React.FC<{ option: Option }> = ({ option }) => {
); );
}; };
export const QuestionOptions: React.FC<{ options: Option[] }> = ({ export const QuestionOptions: React.FC<{ question: QuestionFragment }> = ({
options, question,
}) => { }) => {
const isBinary = const isBinary = isQuestionBinary(question);
options.length === 2 &&
(options[0].name === "Yes" || options[0].name === "No");
if (isBinary) { 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 ( return (
<div className="space-x-2"> <div className="space-x-2">
<span <span
@ -134,8 +137,11 @@ export const QuestionOptions: React.FC<{ options: Option[] }> = ({
</div> </div>
); );
} else { } else {
const optionsSorted = options.sort((a, b) => b.probability - a.probability); const optionsSorted = question.options
const optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options. .filter(isFullQuestionOption)
.sort((a, b) => b.probability - a.probability);
const optionsMax5 = optionsSorted.slice(0, 5); // display max 5 options.
return ( return (
<div className="space-y-2"> <div className="space-y-2">

View File

@ -8,3 +8,20 @@ export const formatProbability = (probability: number) => {
: percentage.toFixed(0) + "%"; : percentage.toFixed(0) + "%";
return percentageCapped; 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: { variables: {
input: { input: {
...queryParameters, ...queryParameters,
limit: numDisplay, limit: numDisplay + 50,
}, },
}, },
pause: !isFirstRender, pause: !isFirstRender,

View File

@ -1,3 +1,5 @@
import { QuestionFragment } from "./fragments.generated";
export const getBasePath = () => { export const getBasePath = () => {
if (process.env.NEXT_PUBLIC_VERCEL_URL) { if (process.env.NEXT_PUBLIC_VERCEL_URL) {
return `https://${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) //console.log(textString)
return 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 { interface SearchOpts {
queryString: string; queryString: string;
hitsPerPage?: number; hitsPerPage?: number;
starsThreshold: number; starsThreshold?: number;
filterByPlatforms: string[]; filterByPlatforms?: string[];
forecastsThreshold: number; forecastsThreshold?: number;
} }
const buildFilter = ({ const buildFilter = ({
@ -33,7 +33,7 @@ const buildFilter = ({
? filterByPlatforms.map((platform) => `platform:"${platform}"`).join(" OR ") ? filterByPlatforms.map((platform) => `platform:"${platform}"`).join(" OR ")
: null; : null;
const numForecastsFilter = const numForecastsFilter =
forecastsThreshold > 0 forecastsThreshold && forecastsThreshold > 0
? `qualityindicators.numforecasts >= ${forecastsThreshold}` ? `qualityindicators.numforecasts >= ${forecastsThreshold}`
: null; : null;
const finalFilter = [starsFilter, platformsFilter, numForecastsFilter] const finalFilter = [starsFilter, platformsFilter, numForecastsFilter]