refactor: more typescript
This commit is contained in:
parent
439a9045da
commit
f6e2e8cfa1
23
package-lock.json
generated
23
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -29,16 +29,19 @@ export const getServerSideProps: GetServerSideProps<Props> = 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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 /> }
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
<span className="font-bold">
|
||||
{formatIndicatorValue(value, indicator, question.platform.id)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<div key={indicator}>
|
||||
<span>{indicatorLabel}:</span>
|
||||
<span className="font-bold">
|
||||
{formatIndicatorValue(
|
||||
Number(value),
|
||||
indicator,
|
||||
question.platform.id
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
};
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -61,7 +61,7 @@ export const SearchScreen: React.FC<Props> = ({
|
|||
variables: {
|
||||
input: {
|
||||
...queryParameters,
|
||||
limit: numDisplay,
|
||||
limit: numDisplay + 50,
|
||||
},
|
||||
},
|
||||
pause: !isFirstRender,
|
||||
|
|
|
@ -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"))
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue
Block a user