refactor: frontend code, especially DisplayForecast(s)

This commit is contained in:
Vyacheslav Matyukhin 2022-04-06 01:50:50 +03:00
parent 5e7596e6e7
commit ce9bb0eedf
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
22 changed files with 855 additions and 879 deletions

View File

@ -2,9 +2,9 @@ import React from "react";
import ReactMarkdown from "react-markdown";
import gfm from "remark-gfm";
import Layout from "../web/display/layout";
import { Layout } from "../web/display/Layout";
let readmeMarkdownText = `# About
const readmeMarkdownText = `# About
This webpage is a search engine for probabilities. Given a query, it searches for relevant questions in various prediction markets and forecasting platforms. For example, try searching for "China", "North Korea", "Semiconductors", "COVID", "Trump", or "X-risk". In addition to search, we also provide various [tools](http://localhost:3000/tools).

View File

@ -2,7 +2,7 @@ import { NextPage } from "next";
import React from "react";
import { displayForecastsWrapperForCapture } from "../web/display/displayForecastsWrappers";
import Layout from "../web/display/layout";
import { Layout } from "../web/display/Layout";
import { Props } from "../web/search/anySearchPage";
import CommonDisplay from "../web/search/CommonDisplay";

View File

@ -6,9 +6,9 @@ import { useState } from "react";
import { DashboardItem } from "../backend/dashboards";
import { getPlatformsConfig, PlatformConfig } from "../backend/platforms";
import { DashboardCreator } from "../web/display/dashboardCreator";
import displayForecasts from "../web/display/displayForecasts";
import Layout from "../web/display/layout";
import { DashboardCreator } from "../web/display/DashboardCreator";
import { DisplayForecasts } from "../web/display/DisplayForecasts";
import { Layout } from "../web/display/Layout";
import { addLabelsToForecasts, FrontendForecast } from "../web/platforms";
import { getDashboardForecastsByDashboardId } from "../web/worker/getDashboardForecasts";
@ -70,12 +70,12 @@ const DashboardsPage: NextPage<Props> = ({
);
const [dashboardItem, setDashboardItem] = useState(initialDashboardItem);
let handleSubmit = async (data) => {
const handleSubmit = async (data) => {
console.log(data);
// Send to server to create
// Get back the id
let response = await axios({
url: `/api/create-dashboard-from-ids`,
url: "/api/create-dashboard-from-ids",
method: "POST",
headers: { "Content-Type": "application/json" },
data: JSON.stringify(data),
@ -107,7 +107,8 @@ const DashboardsPage: NextPage<Props> = ({
}
};
let isGraubardEasterEgg = (name) => (name == "Clay Graubard" ? true : false);
let isGraubardEasterEgg = (name: string) =>
name == "Clay Graubard" ? true : false;
return (
<Layout page="dashboard">
@ -162,11 +163,11 @@ const DashboardsPage: NextPage<Props> = ({
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
{displayForecasts({
results: dashboardForecasts,
numDisplay: dashboardForecasts.length,
showIdToggle: false,
})}
<DisplayForecasts
results={dashboardForecasts}
numDisplay={dashboardForecasts.length}
showIdToggle={false}
/>
</div>
{/* */}
<h3 className="flex items-center col-start-2 col-end-2 w-full justify-center mt-8 mb-4">

View File

@ -2,7 +2,7 @@ import { NextPage } from "next";
import React from "react";
import { displayForecastsWrapperForSearch } from "../web/display/displayForecastsWrappers";
import Layout from "../web/display/layout";
import { Layout } from "../web/display/Layout";
import { Props } from "../web/search/anySearchPage";
import CommonDisplay from "../web/search/CommonDisplay";

View File

@ -1,6 +1,7 @@
import { NextPage } from "next";
import React, { useEffect } from "react";
function Recursion() {
const Recursion: NextPage = () => {
useEffect(() => {
if (typeof window !== "undefined") {
window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
@ -12,6 +13,6 @@ function Recursion() {
<h2>You have now reached the fourth level of recursion!!</h2>
</div>
);
}
};
export default Recursion;

View File

@ -5,7 +5,7 @@ import { useRouter } from "next/router"; // https://nextjs.org/docs/api-referenc
import { useState } from "react";
import { getPlatformsConfig } from "../backend/platforms";
import displayForecasts from "../web/display/displayForecasts";
import { DisplayForecasts } from "../web/display/DisplayForecasts";
import { addLabelsToForecasts } from "../web/platforms";
import { getDashboardForecastsByDashboardId } from "../web/worker/getDashboardForecasts";
@ -88,11 +88,11 @@ export default function Home({
numCols || 3
} gap-4 mb-6`}
>
{displayForecasts({
results: dashboardForecasts,
numDisplay: dashboardForecasts.length,
showIdToggle: false,
})}
<DisplayForecasts
results={dashboardForecasts}
numDisplay={dashboardForecasts.length}
showIdToggle={false}
/>
</div>
</div>
</div>

View File

@ -4,7 +4,7 @@ import { GetServerSideProps, NextPage } from "next";
import React from "react";
import { platforms } from "../backend/platforms";
import { DisplayForecast } from "../web/display/displayForecasts";
import { DisplayForecast } from "../web/display/DisplayForecast";
import { FrontendForecast } from "../web/platforms";
import searchAccordingToQueryData from "../web/worker/searchAccordingToQueryData";

View File

@ -1,85 +1,76 @@
import Link from "next/link";
import React from "react";
import Layout from "../web/display/layout";
import { Card } from "../web/display/Card";
import { Layout } from "../web/display/Layout";
type AnyTool = {
title: string;
description: string;
img?: string;
};
type InnerTool = AnyTool & { innerLink: string };
type ExternalTool = AnyTool & { externalLink: string };
type UpcomingTool = AnyTool;
type Tool = InnerTool | ExternalTool | UpcomingTool;
/* Display one tool */
function displayTool({
sameWebpage,
title,
description,
link,
url,
img,
i,
}: any) {
switch (sameWebpage) {
case true:
const ToolCard: React.FC<Tool> = (tool) => {
const inner = (
<Card>
<div className="grid content-start gap-3">
<div className="text-gray-800 text-lg font-medium">{tool.title}</div>
<div className="text-gray-500">{tool.description}</div>
{tool.img && <img src={tool.img} className="text-gray-500" />}
</div>
</Card>
);
if ("innerLink" in tool) {
return (
<Link href={link} passHref key={`tool-${i}`}>
<div className="hover:bg-gray-100 hover:no-underline cursor-pointer flex flex-col px-4 py-3 bg-white rounded-md shadow place-content-stretch flex-grow no-underline b-6">
<div className="flex-grow items-stretch">
<div className={`text-gray-800 text-lg mb-2 font-medium `}>
{title}
</div>
<div className={`text-gray-500 mb-3 `}>{description}</div>
{}
<img src={img} className={`text-gray-500 mb-2`} />
</div>
</div>
<Link href={tool.innerLink} passHref>
<a className="textinherit no-underline">{inner}</a>
</Link>
);
break;
default:
} else if ("externalLink" in tool) {
return (
<a
href={url}
key={`tool-${i}`}
className="hover:bg-gray-100 hover:no-underline cursor-pointer flex flex-col px-4 py-3 bg-white rounded-md shadow place-content-stretch flex-grow no-underline b-6"
>
<div className="flex-grow items-stretch">
<div className={`text-gray-800 text-lg mb-2 font-medium `}>
{title}
</div>
<div className={`text-gray-500 mb-3 `}>{description}</div>
{}
<img src={img} className={`text-gray-500 mb-2`} />
</div>
<a href={tool.externalLink} className="textinherit no-underline">
{inner}
</a>
);
break;
}
} else {
return inner;
}
};
export default function Tools({ lastUpdated }) {
let tools = [
let tools: Tool[] = [
{
title: "Search",
description: "Find forecasting questions on many platforms",
link: "/",
sameWebpage: true,
description: "Find forecasting questions on many platforms.",
innerLink: "/",
img: "https://i.imgur.com/Q94gVqG.png",
},
{
title: "[Beta] Present",
description: "Present forecasts in dashboards.",
sameWebpage: true,
link: "/dashboards",
innerLink: "/dashboards",
img: "https://i.imgur.com/x8qkuHQ.png",
},
{
title: "Capture",
description:
"Capture forecasts save them to Imgur. Useful for posting them somewhere else as images. Currently rate limited by Imgur, so if you get a .gif of a fox falling flat on his face, that's why.",
link: "/capture",
sameWebpage: true,
innerLink: "/capture",
img: "https://i.imgur.com/EXkFBzz.png",
},
{
title: "Summon",
description:
"Summon metaforecast on Twitter by mentioning @metaforecast, or on Discord by using Fletcher and !metaforecast, followed by search terms",
url: "https://twitter.com/metaforecast",
"Summon metaforecast on Twitter by mentioning @metaforecast, or on Discord by using Fletcher and !metaforecast, followed by search terms.",
externalLink: "https://twitter.com/metaforecast",
img: "https://i.imgur.com/BQ4Zzjw.png",
},
{
@ -87,7 +78,6 @@ export default function Tools({ lastUpdated }) {
description:
"Interact with metaforecast's API and fetch forecasts for your application. Currently possible but documentation is poor, get in touch.",
},
{
title: "[Upcoming] Record",
description: "Save your forecasts or bets.",
@ -95,8 +85,10 @@ export default function Tools({ lastUpdated }) {
];
return (
<Layout page="tools">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4 mb-8">
{tools.map((tool, i) => displayTool({ ...tool, i }))}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4 mb-8 place-content-stretch">
{tools.map((tool, i) => (
<ToolCard {...tool} key={`tool-${i}`} />
))}
</div>
</Layout>
);

15
src/web/display/Card.tsx Normal file
View File

@ -0,0 +1,15 @@
const CardTitle: React.FC = ({ children }) => (
<div className="text-gray-800 text-lg font-medium">{children}</div>
);
type CardType = React.FC & {
Title: typeof CardTitle;
};
export const Card: CardType = ({ children }) => (
<div className="h-full px-4 py-3 bg-white hover:bg-gray-100 rounded-md shadow">
{children}
</div>
);
Card.Title = CardTitle;

View File

@ -0,0 +1,299 @@
const formatQualityIndicator = (indicator) => {
let result;
switch (indicator) {
case "numforecasts":
result = null;
break;
case "stars":
result = null;
break;
case "volume":
result = "Volume";
break;
case "numforecasters":
result = "Forecasters";
break;
case "yes_bid":
result = null; // "Yes bid"
break;
case "yes_ask":
result = null; // "Yes ask"
break;
case "spread":
result = "Spread";
break;
case "shares_volume":
result = "Shares vol.";
break;
case "open_interest":
result = "Interest";
break;
case "resolution_data":
result = null;
break;
case "liquidity":
result = "Liquidity";
break;
case "tradevolume":
result = "Volume";
break;
}
return result;
};
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";
}
};
const formatQualityIndicators = (qualityIndicators: any) => {
let newQualityIndicators = {};
for (let key in qualityIndicators) {
let newKey = formatQualityIndicator(key);
if (newKey) {
newQualityIndicators[newKey] = qualityIndicators[key];
}
}
return newQualityIndicators;
};
/* Display functions*/
const getPercentageSymbolIfNeeded = ({ indicator, platform }) => {
let indicatorsWhichNeedPercentageSymbol = ["Spread"];
if (indicatorsWhichNeedPercentageSymbol.includes(indicator)) {
return "%";
} else {
return "";
}
};
const getCurrencySymbolIfNeeded = ({
indicator,
platform,
}: {
indicator: any;
platform: string;
}) => {
let indicatorsWhichNeedCurrencySymbol = ["Volume", "Interest", "Liquidity"];
let dollarPlatforms = ["predictit", "kalshi", "polymarket"];
if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) {
if (dollarPlatforms.includes(platform)) {
return "$";
} else {
return "£";
}
} else {
return "";
}
};
const showFirstQualityIndicator = ({
numforecasts,
timestamp,
showTimeStamp,
qualityindicators,
}) => {
if (!!numforecasts) {
return (
<div className="flex col-span-1 row-span-1">
{/*<span>{` ${numforecasts == 1 ? "Forecast" : "Forecasts:"}`}</span>&nbsp;*/}
<span>{"Forecasts:"}</span>&nbsp;
<span className="font-bold">{Number(numforecasts).toFixed(0)}</span>
</div>
);
} else if (showTimeStamp) {
return (
<span className="hidden sm:flex items-center justify-center text-gray-600 mt-2">
<svg className="ml-4 mr-1 mt-1" height="10" width="16">
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
</svg>
{`Last updated: ${
timestamp && !!timestamp.slice ? timestamp.slice(0, 10) : "unknown"
}`}
</span>
);
} else {
return null;
}
};
const displayQualityIndicators: React.FC<{
numforecasts: number;
timestamp: number;
showTimeStamp: boolean;
qualityindicators: any;
platform: string; // id string - e.g. "goodjudgment", not "Good Judgment"
}> = ({
numforecasts,
timestamp,
showTimeStamp,
qualityindicators,
platform,
}) => {
// grid grid-cols-1
return (
<div className="text-sm">
{showFirstQualityIndicator({
numforecasts,
timestamp,
showTimeStamp,
qualityindicators,
})}
{Object.entries(formatQualityIndicators(qualityindicators)).map(
(entry, i) => {
return (
<div className="col-span-1 row-span-1">
<span>{`${entry[0]}:`}</span>&nbsp;
<span className="font-bold">
{`${getCurrencySymbolIfNeeded({
indicator: entry[0],
platform,
})}${formatNumber(entry[1])}${getPercentageSymbolIfNeeded({
indicator: entry[0],
platform,
})}`}
</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 {
stars: any;
platform: string;
platformLabel: string;
numforecasts: any;
qualityindicators: any;
timestamp: any;
showTimeStamp: boolean;
expandFooterToFullWidth: boolean;
}
export const ForecastFooter: React.FC<Props> = ({
stars,
platform,
platformLabel,
numforecasts,
qualityindicators,
timestamp,
showTimeStamp,
expandFooterToFullWidth,
}) => {
// I experimented with justify-evenly, justify-around, etc., here: https://tailwindcss.com/docs/justify-content
// I came to the conclusion that as long as the description isn't justified too, aligning the footer symmetrically doesn't make sense
// because the contrast is jarring.
let debuggingWithBackground = false;
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(stars)} ${
debuggingWithBackground ? "bg-red-200" : ""
}`}
>
{getstars(stars)}
</div>
<div
className={`${
expandFooterToFullWidth ? "place-self-center" : "self-center"
} col-span-1 font-bold ${debuggingWithBackground ? "bg-red-100" : ""}`}
>
{platformLabel
.replace("Good Judgment Open", "GJOpen")
.replace(/ /g, "\u00a0")}
</div>
<div
className={`${
expandFooterToFullWidth
? "justify-self-end mr-4"
: "justify-self-center"
} col-span-1 ${debuggingWithBackground ? "bg-red-100" : ""}`}
>
{displayQualityIndicators({
numforecasts,
timestamp,
showTimeStamp,
qualityindicators,
platform,
})}
</div>
</div>
);
};

View File

@ -0,0 +1,390 @@
import { FaRegClipboard } from "react-icons/fa";
import ReactMarkdown from "react-markdown";
import { FrontendForecast } from "../../platforms";
import { Card } from "../Card";
import { ForecastFooter } from "./ForecastFooter";
const truncateText = (length: number, text: string): string => {
if (!text) {
return "";
}
if (!!text && text.length <= length) {
return text;
}
let breakpoints = " .!?";
let lastLetter = null;
let lastIndex = null;
for (let index = length; index > 0; index--) {
let letter = text[index];
if (breakpoints.includes(letter)) {
lastLetter = letter;
lastIndex = index;
break;
}
}
let truncatedText = !!text.slice
? text.slice(0, lastIndex) + (lastLetter != "." ? "..." : "..")
: "";
return truncatedText;
};
const formatProbability = (probability: number) => {
let percentage = probability * 100;
let percentageCapped =
percentage < 1
? "< 1%"
: percentage > 99
? "> 99%"
: percentage.toFixed(0) + "%";
return percentageCapped;
};
// 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);
};
}
const cleanText = (text: string): string => {
// Note: should no longer be necessary
let textString = !!text ? text : "";
textString = textString
.replaceAll("] (", "](")
.replaceAll(") )", "))")
.replaceAll("( [", "([")
.replaceAll(") ,", "),")
.replaceAll("==", "") // Denotes a title in markdown
.replaceAll("Background\n", "")
.replaceAll("Context\n", "")
.replaceAll("--- \n", "- ")
.replaceAll(/\[(.*?)\]\(.*?\)/g, "$1");
textString = textString.slice(0, 1) == "=" ? textString.slice(1) : textString;
//console.log(textString)
return textString;
};
const primaryForecastColor = (probability: number) => {
if (probability < 0.03) {
return "bg-red-600";
} else if (probability < 0.1) {
return "bg-red-600 opacity-80";
} else if (probability < 0.2) {
return "bg-red-600 opacity-70";
} else if (probability < 0.3) {
return "bg-red-600 opacity-60";
} else if (probability < 0.4) {
return "bg-red-600 opacity-50";
} else if (probability < 0.5) {
return "bg-gray-500";
} else if (probability < 0.6) {
return "bg-gray-500";
} else if (probability < 0.7) {
return "bg-green-600 opacity-50";
} else if (probability < 0.8) {
return "bg-green-600 opacity-60";
} else if (probability < 0.9) {
return "bg-green-600 opacity-70";
} else if (probability < 0.97) {
return "bg-green-600 opacity-80";
} else {
return "bg-green-600";
}
};
const textColor = (probability: number) => {
if (probability < 0.03) {
return "text-red-600";
} else if (probability < 0.1) {
return "text-red-600 opacity-80";
} else if (probability < 0.2) {
return "text-red-600 opacity-80";
} else if (probability < 0.3) {
return "text-red-600 opacity-70";
} else if (probability < 0.4) {
return "text-red-600 opacity-70";
} else if (probability < 0.5) {
return "text-gray-500";
} else if (probability < 0.6) {
return "text-gray-500";
} else if (probability < 0.7) {
return "text-green-600 opacity-70";
} else if (probability < 0.8) {
return "text-green-600 opacity-70";
} else if (probability < 0.9) {
return "text-green-600 opacity-80";
} else if (probability < 0.97) {
return "text-green-600 opacity-80";
} else {
return "text-green-600";
}
};
const primaryEstimateAsText = (probability: number) => {
if (probability < 0.03) {
return "Exceptionally unlikely";
} else if (probability < 0.1) {
return "Very unlikely";
} else if (probability < 0.4) {
return "Unlikely";
} else if (probability < 0.6) {
return "About Even";
} else if (probability < 0.9) {
return "Likely";
} else if (probability < 0.97) {
return "Very likely";
} else {
return "Virtually certain";
}
};
// Logical checks
const checkIfDisplayTimeStampAtBottom = (qualityIndicators: {
[k: string]: any;
}) => {
let indicators = Object.keys(qualityIndicators);
if (indicators.length == 1 && indicators[0] == "stars") {
return true;
} else {
return false;
}
};
// Auxiliary components
const DisplayMarkdown: React.FC<{ description: string }> = ({
description,
}) => {
let formatted = truncateText(250, cleanText(description));
// overflow-hidden overflow-ellipsis h-24
return formatted === "" ? null : (
<div className="overflow-clip">
<ReactMarkdown linkTarget="_blank" className="font-normal">
{formatted}
</ReactMarkdown>
</div>
);
};
const OptionRow: React.FC<{ option: any }> = ({ option }) => {
const chooseColor = (probability: number) => {
if (probability < 0.1) {
return "bg-blue-50 text-blue-500";
} else if (probability < 0.3) {
return "bg-blue-100 text-blue-600";
} else if (probability < 0.7) {
return "bg-blue-200 text-blue-700";
} else {
return "bg-blue-300 text-blue-800";
}
};
return (
<div className="flex items-center">
<div
className={`${chooseColor(
option.probability
)} w-14 flex-none rounded-md py-0.5 text-sm text-center`}
>
{formatProbability(option.probability)}
</div>
<div className="text-gray-700 pl-3 leading-snug text-sm">
{option.name}
</div>
</div>
);
};
const ForecastOptions: React.FC<{ options: any[] }> = ({ options }) => {
const optionsSorted = options.sort((a, b) => b.probability - a.probability);
const optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options.
return (
<div className="space-y-2">
{optionsMax5.map((option, i) => (
<OptionRow option={option} key={i} />
))}
</div>
);
};
const CopyText: React.FC<{ text: string; displayText: string }> = ({
text,
displayText,
}) => (
<div
className="flex items-center justify-center p-4 space-x-3 border rounded border-blue-400 hover:border-transparent bg-transparent hover:bg-blue-300 text-sm font-medium text-blue-400 hover:text-white"
onClick={(e) => {
e.preventDefault();
navigator.clipboard.writeText(text);
}}
>
<span>{displayText}</span>
<FaRegClipboard />
</div>
);
const LastUpdated: React.FC<{ timestamp: string }> = ({ timestamp }) => (
<div className="flex items-center">
<svg className="mt-1" height="10" width="16">
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
</svg>
<span className="text-gray-600">
Last updated: {timestamp ? timestamp.slice(0, 10) : "unknown"}
</span>
</div>
);
// Main component
interface Props {
forecast: FrontendForecast;
showTimeStamp: boolean;
expandFooterToFullWidth: boolean;
showIdToggle?: boolean;
}
export const DisplayForecast: React.FC<Props> = ({
forecast: {
id,
title,
url,
platform,
platformLabel,
description,
options,
qualityindicators,
timestamp,
visualization,
},
showTimeStamp,
expandFooterToFullWidth,
showIdToggle,
}) => {
const displayTimestampAtBottom =
checkIfDisplayTimeStampAtBottom(qualityindicators);
const yesNoOptions =
options.length === 2 &&
(options[0].name === "Yes" || options[0].name === "No");
return (
<a className="textinherit no-underline" href={url} target="_blank">
<Card>
<div className="h-full flex flex-col space-y-4">
<div className="flex-grow space-y-4">
{showIdToggle ? (
<div className="mx-10">
<CopyText text={id} displayText={`[${id}]`} />
</div>
) : null}
<Card.Title>{title}</Card.Title>
{yesNoOptions && (
<div className="flex justify-between">
<div className="space-x-2">
<span
className={`${primaryForecastColor(
options[0].probability
)} text-white w-16 rounded-md px-1.5 py-0.5 font-bold`}
>
{formatProbability(options[0].probability)}
</span>
<span
className={`${textColor(
options[0].probability
)} text-gray-500 inline-block`}
>
{primaryEstimateAsText(options[0].probability)}
</span>
</div>
<div
className={`hidden ${
showTimeStamp && !displayTimestampAtBottom ? "sm:block" : ""
}`}
>
<LastUpdated timestamp={timestamp} />
</div>
</div>
)}
{!yesNoOptions && (
<div className="space-y-2">
<ForecastOptions options={options} />
<div
className={`hidden ${
showTimeStamp && !displayTimestampAtBottom ? "sm:block" : ""
} ml-6`}
>
<LastUpdated timestamp={timestamp} />
</div>
</div>
)}
{platform !== "guesstimate" && options.length < 3 && (
<div className="text-gray-500">
<DisplayMarkdown description={description} />
</div>
)}
{platform === "guesstimate" && (
<img
className="rounded-sm"
src={visualization}
alt="Guesstimate Screenshot"
/>
)}
</div>
<div
className={`sm:hidden ${
!showTimeStamp ? "hidden" : ""
} self-center`}
>
{/* This one is exclusively for mobile*/}
<LastUpdated timestamp={timestamp} />
</div>
<div className="w-full">
<ForecastFooter
stars={qualityindicators.stars}
platform={platform}
platformLabel={platformLabel || platform} // author || platformLabel,
numforecasts={qualityindicators.numforecasts}
qualityindicators={qualityindicators}
timestamp={timestamp}
showTimeStamp={showTimeStamp && displayTimestampAtBottom}
expandFooterToFullWidth={expandFooterToFullWidth}
/>
</div>
</div>
</Card>
</a>
);
};

View File

@ -1,6 +1,16 @@
import React from "react";
export default function Form({ value, onChange, placeholder }) {
interface Props {
value: string;
onChange: (v: string) => void;
placeholder: string;
}
export const QueryForm: React.FC<Props> = ({
value,
onChange,
placeholder,
}) => {
const handleInputChange = (event) => {
event.preventDefault();
onChange(event.target.value); // In this case, the query, e.g. "COVID.19"
@ -21,4 +31,4 @@ export default function Form({ value, onChange, placeholder }) {
/>
</form>
);
}
};

View File

@ -76,9 +76,19 @@ function Track({ source, target, getTrackProps }) {
);
}
interface Props {
value: number;
onChange: (event: any) => void;
displayFunction: (value: number) => string;
}
/* Body */
// Two functions, essentially identical.
export function SliderElement({ onChange, value, displayFunction }) {
export const SliderElement: React.FC<Props> = ({
onChange,
value,
displayFunction,
}) => {
return (
<Slider
rootStyle={
@ -122,4 +132,4 @@ export function SliderElement({ onChange, value, displayFunction }) {
</Tracks>
</Slider>
);
}
};

View File

@ -5,11 +5,11 @@ interface Props {
value: number;
}
const ButtonsForStars: React.FC<Props> = ({ onChange, value }) => {
export const ButtonsForStars: React.FC<Props> = ({ onChange, value }) => {
const onChangeInner = (buttonPressed: number) => {
onChange(buttonPressed);
};
let setStyle = (buttonNumber: number) =>
const setStyle = (buttonNumber: number) =>
`flex row-span-1 col-start-${buttonNumber + 1} col-end-${
buttonNumber + 2
} items-center justify-center text-center${
@ -37,5 +37,3 @@ const ButtonsForStars: React.FC<Props> = ({ onChange, value }) => {
</div>
);
};
export default ButtonsForStars;

View File

@ -1,46 +1,45 @@
import React, { useState } from "react";
let exampleInput = `{
const exampleInput = `{
"title": "Random example",
"description": "Just a random description of a random example",
"ids": [ "metaculus-372", "goodjudgmentopen-2244", "metaculus-7550", "kalshi-09d060ee-b184-4167-b86b-d773e56b4162", "wildeford-5d1a04e1a8", "metaculus-2817" ],
"creator": "Peter Parker"
}`;
export function DashboardCreator({ handleSubmit }) {
let [value, setValue] = useState(exampleInput);
interface Props {
handleSubmit: (data: any) => Promise<void>;
}
export const DashboardCreator: React.FC<Props> = ({ handleSubmit }) => {
const [value, setValue] = useState(exampleInput);
const [displayingDoneMessage, setDisplayingDoneMessage] = useState(false);
const [displayingDoneMessageTimer, setDisplayingDoneMessageTimer] =
useState(null);
let handleChange = (event) => {
const handleChange = (event) => {
setValue(event.target.value);
};
let handleSubmitInner = (event) => {
const handleSubmitInner = (event) => {
clearTimeout(displayingDoneMessageTimer);
event.preventDefault();
//console.log(event)
console.log("value@handleSubmitInner@DashboardCreator");
//console.log(typeof(value));
console.log(value);
try {
let newData = JSON.parse(value);
//console.log(typeof(newData))
//console.log(newData)
if (!newData || !newData.ids || newData.ids.length == 0) {
throw Error("Not enough objects");
} else {
handleSubmit(newData);
setDisplayingDoneMessage(true);
let timer = setTimeout(() => setDisplayingDoneMessage(false), 3000);
const timer = setTimeout(() => setDisplayingDoneMessage(false), 3000);
setDisplayingDoneMessageTimer(timer);
}
} catch (error) {
setDisplayingDoneMessage(false);
//alert(error)
//console.log(error)
let substituteText = `Error: ${error.message}
const substituteText = `Error: ${error.message}
Try something like:
${exampleInput}
@ -49,6 +48,7 @@ Your old input was: ${value}`;
setValue(substituteText);
}
};
return (
<form onSubmit={handleSubmitInner} className="block place-centers">
<textarea
@ -83,4 +83,4 @@ Your old input was: ${value}`;
</div>
</form>
);
}
};

View File

@ -1,746 +1,7 @@
/* Imports */
import React from "react";
import { FaRegClipboard } from "react-icons/fa";
import ReactMarkdown from "react-markdown";
import { FrontendForecast } from "../platforms";
/* Definitions */
/* Support functions */
// Short utils
let truncateTextSimple = (length, text) => {
text.length > length
? !!text.slice
? text.slice(0, length) + "..."
: ""
: text;
};
let truncateText = (length, text) => {
if (!text) {
return "";
}
if (!!text && text.length <= length) {
return text;
}
let breakpoints = " .!?";
let lastLetter = null;
let lastIndex = null;
for (let index = length; index > 0; index--) {
let letter = text[index];
if (breakpoints.includes(letter)) {
lastLetter = letter;
lastIndex = index;
break;
}
}
let truncatedText = !!text.slice
? text.slice(0, lastIndex) + (lastLetter != "." ? "..." : "..")
: "";
return truncatedText;
};
let formatProbability = (probability) => {
let percentage = probability * 100;
let percentageCapped =
percentage < 1
? "< 1%"
: percentage > 99
? "> 99%"
: percentage.toFixed(0) + "%";
return percentageCapped;
};
let 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";
}
};
let formatQualityIndicators = (qualityIndicators) => {
let newQualityIndicators = {};
for (let key in qualityIndicators) {
let newKey = formatQualityIndicator(key);
if (newKey) {
newQualityIndicators[newKey] = qualityIndicators[key];
}
}
return newQualityIndicators;
};
// replaceAll polyfill
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
function replaceAll(originalString, pattern, substitute) {
return originalString.replace(
new RegExp(escapeRegExp(pattern), "g"),
substitute
);
}
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (pattern, 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);
};
}
let cleanText = (text) => {
// Note: should no longer be necessary
let textString = !!text ? text : "";
textString = textString
.replaceAll("] (", "](")
.replaceAll(") )", "))")
.replaceAll("( [", "([")
.replaceAll(") ,", "),")
.replaceAll("==", "") // Denotes a title in markdown
.replaceAll("Background\n", "")
.replaceAll("Context\n", "")
.replaceAll("--- \n", "- ")
.replaceAll(/\[(.*?)\]\(.*?\)/g, "$1");
textString = textString.slice(0, 1) == "=" ? textString.slice(1) : textString;
//console.log(textString)
return textString;
};
/* Faulty regex implementation
let cleanText = (text) => {
// Note: should no longer be necessary
let textString = !!text ? "" : text;
textString = textString
.replace(/\]\] \(/g, "](")
.replace(/\) \)/g, "))")
.replace(/\( \[/g, "([")
.replace(/\) ,/g, "),")
.replace(/==/g, "") // Denotes a title in markdown
.replace(/Background\n/g, "")
.replace(/Context\n/g, "")
.replace(/--- \n/g, "- ")
.replace(/\[(.*?)\]\(.*?\)/g, "$1");
textString = textString.slice(0, 1) == "=" && !!text.slice ? textString.slice(1) : textString;
//console.log(textString)
return textString;
};
*/
// Database-like functions
export function getstars(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;
}
function getStarsColor(numstars) {
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;
}
let primaryForecastColor = (probability) => {
if (probability < 0.03) {
return "bg-red-600";
} else if (probability < 0.1) {
return "bg-red-600 opacity-80";
} else if (probability < 0.2) {
return "bg-red-600 opacity-70";
} else if (probability < 0.3) {
return "bg-red-600 opacity-60";
} else if (probability < 0.4) {
return "bg-red-600 opacity-50";
} else if (probability < 0.5) {
return "bg-gray-500";
} else if (probability < 0.6) {
return "bg-gray-500";
} else if (probability < 0.7) {
return "bg-green-600 opacity-50";
} else if (probability < 0.8) {
return "bg-green-600 opacity-60";
} else if (probability < 0.9) {
return "bg-green-600 opacity-70";
} else if (probability < 0.97) {
return "bg-green-600 opacity-80";
} else {
return "bg-green-600";
}
};
let textColor = (probability) => {
if (probability < 0.03) {
return "text-red-600";
} else if (probability < 0.1) {
return "text-red-600 opacity-80";
} else if (probability < 0.2) {
return "text-red-600 opacity-80";
} else if (probability < 0.3) {
return "text-red-600 opacity-70";
} else if (probability < 0.4) {
return "text-red-600 opacity-70";
} else if (probability < 0.5) {
return "text-gray-500";
} else if (probability < 0.6) {
return "text-gray-500";
} else if (probability < 0.7) {
return "text-green-600 opacity-70";
} else if (probability < 0.8) {
return "text-green-600 opacity-70";
} else if (probability < 0.9) {
return "text-green-600 opacity-80";
} else if (probability < 0.97) {
return "text-green-600 opacity-80";
} else {
return "text-green-600";
}
};
let primaryEstimateAsText = (probability) => {
if (probability < 0.03) {
return "Exceptionally unlikely";
} else if (probability < 0.1) {
return "Very unlikely";
} else if (probability < 0.4) {
return "Unlikely";
} else if (probability < 0.6) {
return "About Even";
} else if (probability < 0.9) {
return "Likely";
} else if (probability < 0.97) {
return "Very likely";
} else {
return "Virtually certain";
}
};
let textColorFromScore = (score) => {
if (score < 0.4) {
return ["text-gray-900", "text-gray-900"];
} else {
return ["text-gray-400", "text-gray-400"];
}
};
let opacityFromScore = (score) => {
if (score < 0.4) {
return "opacity-100";
} else {
return "opacity-50";
}
};
let formatQualityIndicator = (indicator) => {
let result;
switch (indicator) {
case "numforecasts":
result = null;
break;
case "stars":
result = null;
break;
case "volume":
result = "Volume";
break;
case "numforecasters":
result = "Forecasters";
break;
case "yes_bid":
result = null; // "Yes bid"
break;
case "yes_ask":
result = null; // "Yes ask"
break;
case "spread":
result = "Spread";
break;
case "shares_volume":
result = "Shares vol.";
break;
case "open_interest":
result = "Interest";
break;
case "resolution_data":
result = null;
break;
case "liquidity":
result = "Liquidity";
break;
case "tradevolume":
result = "Volume";
break;
}
return result;
};
// Logical checks
let checkIfDisplayTimeStampAtBottom = (qualityIndicators) => {
let indicators = Object.keys(qualityIndicators);
if (indicators.length == 1 && indicators[0] == "stars") {
return true;
} else {
return false;
}
};
let getCurrencySymbolIfNeeded = ({
indicator,
platform,
}: {
indicator: any;
platform: string;
}) => {
let indicatorsWhichNeedCurrencySymbol = ["Volume", "Interest", "Liquidity"];
let dollarPlatforms = ["predictit", "kalshi", "polymarket"];
if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) {
if (dollarPlatforms.includes(platform)) {
return "$";
} else {
return "£";
}
} else {
return "";
}
};
let getPercentageSymbolIfNeeded = ({ indicator, platform }) => {
let indicatorsWhichNeedPercentageSymbol = ["Spread"];
if (indicatorsWhichNeedPercentageSymbol.includes(indicator)) {
return "%";
} else {
return "";
}
};
/* Display functions*/
// Auxiliary
let displayMarkdown = (description) => {
let formatted = truncateText(250, cleanText(description)); //, description)//
// overflow-hidden overflow-ellipsis h-24
return formatted === "" ? (
""
) : (
<div className="overflow-clip">
<ReactMarkdown linkTarget="_blank" className="font-normal">
{formatted}
</ReactMarkdown>
</div>
);
};
let generateOptionRow = (option) => {
let chooseColor = (probability) => {
if (probability < 0.1) {
return "bg-blue-50 text-blue-500";
} else if (probability < 0.3) {
return "bg-blue-100 text-blue-600";
} else if (probability < 0.7) {
return "bg-blue-200 text-blue-700";
} else {
return "bg-blue-300 text-blue-800";
}
};
return (
<div className="items-center flex">
<div
className={`${chooseColor(
option.probability
)} w-14 flex-none rounded-md py-0.5 my-1 text-sm text-center`}
>
{formatProbability(option.probability)}
</div>
<div className="flex-auto text-gray-700 pl-3 leading-snug text-sm">
{option.name}
</div>
</div>
);
};
let formatForecastOptions = (options) => {
let optionsSorted = options.sort((a, b) => b.probability - a.probability);
let optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options.
let result = optionsMax5.map((option) => generateOptionRow(option));
return result;
};
let showFirstQualityIndicator = ({
numforecasts,
timestamp,
showTimeStamp,
qualityindicators,
}) => {
if (!!numforecasts) {
return (
<div className="flex col-span-1 row-span-1">
{/*<span>{` ${numforecasts == 1 ? "Forecast" : "Forecasts:"}`}</span>&nbsp;*/}
<span>{"Forecasts:"}</span>&nbsp;
<span className="font-bold">{Number(numforecasts).toFixed(0)}</span>
</div>
);
} else if (
showTimeStamp &&
checkIfDisplayTimeStampAtBottom(qualityindicators)
) {
return (
<span
className={`hidden sm:flex items-center justify-center text-gray-600 mt-2`}
>
<svg className="ml-4 mr-1 mt-1" height="10" width="16">
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
</svg>
{`Last updated: ${
timestamp && !!timestamp.slice ? timestamp.slice(0, 10) : "unknown"
}`}
</span>
);
} else {
return null;
}
};
const displayQualityIndicators: React.FC<{
numforecasts: number;
timestamp: number;
showTimeStamp: boolean;
qualityindicators: any;
platform: string; // id string - e.g. "goodjudgment", not "Good Judgment"
}> = ({
numforecasts,
timestamp,
showTimeStamp,
qualityindicators,
platform,
}) => {
// grid grid-cols-1
return (
<div className="text-sm">
{showFirstQualityIndicator({
numforecasts,
timestamp,
showTimeStamp,
qualityindicators,
})}
{Object.entries(formatQualityIndicators(qualityindicators)).map(
(entry, i) => {
return (
<div className="col-span-1 row-span-1">
<span>{`${entry[0]}:`}</span>&nbsp;
<span className="font-bold">
{`${getCurrencySymbolIfNeeded({
indicator: entry[0],
platform,
})}${formatNumber(entry[1])}${getPercentageSymbolIfNeeded({
indicator: entry[0],
platform,
})}`}
</span>
</div>
);
}
)}
</div>
);
};
// Main display functions
let forecastFooter = ({
stars,
platform,
platformLabel,
numforecasts,
qualityindicators,
timestamp,
showTimeStamp,
expandFooterToFullWidth,
}) => {
// I experimented with justify-evenly, justify-around, etc., here: https://tailwindcss.com/docs/justify-content
// I came to the conclusion that as long as the description isn't justified too, aligning the footer symmetrically doesn't make sense
// because the contrast is jarring.
let debuggingWithBackground = false;
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(stars)} ${
debuggingWithBackground ? "bg-red-200" : ""
}`}
>
{getstars(stars)}
</div>
<div
className={`${
expandFooterToFullWidth ? "place-self-center" : "self-center"
} col-span-1 font-bold ${debuggingWithBackground ? "bg-red-100" : ""}`}
>
{platformLabel
.replace("Good Judgment Open", "GJOpen")
.replace(/ /g, "\u00a0")}
</div>
<div
className={`${
expandFooterToFullWidth
? "justify-self-end mr-4"
: "justify-self-center"
} col-span-1 ${debuggingWithBackground ? "bg-red-100" : ""}`}
>
{displayQualityIndicators({
numforecasts,
timestamp,
showTimeStamp,
qualityindicators,
platform,
})}
</div>
</div>
);
};
/* Body */
interface SingleProps {
forecast: FrontendForecast;
showTimeStamp: boolean;
expandFooterToFullWidth: boolean;
showIdToggle?: boolean;
}
export const DisplayForecast: React.FC<SingleProps> = ({
forecast: {
id,
title,
url,
platform,
platformLabel,
description,
options,
qualityindicators,
timestamp,
visualization,
},
showTimeStamp,
expandFooterToFullWidth,
showIdToggle,
}) => {
// const [isJustCopiedSignalVisible, setIsJustCopiedSignalVisible] = useState(false)
const isJustCopiedSignalVisible = false;
return (
<a
key={`displayForecast-${id}`}
href={url}
className="hover:bg-gray-100 hover:no-underline cursor-pointer flex flex-col px-4 py-3 bg-white rounded-md shadow place-content-stretch flex-grow no-underline"
target="_blank"
>
<div className="flex-grow">
<div
className={`text-gray-800 ${opacityFromScore(
0
)} text-lg mb-2 font-medium justify-self-start`}
>
<div
className={`${
showIdToggle ? "" : "hidden"
} flex items-center justify-center mt-2 mb-3 text-sm bg-transparent py-4 px-4 border rounded mt-5 p-10 text-center mb-2 mr-10 ml-10 hover:bg-blue-300 text-blue-400 text-blue-700 ${
isJustCopiedSignalVisible
? " hover:text-white hover:border-blue-700 border-blue-400 hover:bg-blue-700"
: "border-blue-400 hover:text-white hover:border-transparent"
}`}
id="toggle"
onClick={(e) => {
e.preventDefault();
navigator.clipboard.writeText(`"${id}"`);
// setIsJustCopiedSignalVisible(true);
// setTimeout(() => setIsJustCopiedSignalVisible(false), 1000);
// This is just personal preference.
// I prefer to not show the whole text area selected.
}}
>
<span className={``}>{`[${id}]`}</span>
<FaRegClipboard className={`ml-3`} />
</div>
{title.replace("</a>", "")}
</div>
{options.length == 2 &&
(options[0].name == "Yes" || options[0].name == "No") && (
<div>
<div className="grid mb-5 mt-4 mb-5 grid-cols-1 sm:grid-rows-1">
<div className="flex-grow col-span-1 w-full items-center justify-center">
<span
className={`${primaryForecastColor(
options[0].probability
)} text-white w-16 rounded-md px-1.5 py-0.5 font-bold `}
>
{formatProbability(options[0].probability)}
</span>
<span
className={`${textColor(
options[0].probability
)} ml-2 text-gray-500 inline-block`}
>
{primaryEstimateAsText(options[0].probability)}
</span>
</div>
<div
className={`hidden sm:${
showTimeStamp &&
!checkIfDisplayTimeStampAtBottom(qualityindicators)
? "flex"
: "hidden"
} ${opacityFromScore(
0
)} row-end-2 col-start-2 col-end-2 row-start-1 row-end-1 col-span-1 items-center justify-center text-gray-600 ml-3 mr-2 `}
>
<svg className="mt-1" height="10" width="16">
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
</svg>
{`Last updated: ${
timestamp ? timestamp.slice(0, 10) : "unknown"
}`}
</div>
</div>
</div>
)}
{(options.length != 2 ||
(options[0].name != "Yes" && options[0].name != "No")) && (
<>
<div className={`mb-2 mt-2 ${opacityFromScore(0)}`}>
{formatForecastOptions(options)}
</div>
<div
className={`hidden sm:${
showTimeStamp &&
!checkIfDisplayTimeStampAtBottom(qualityindicators)
? "flex"
: "hidden"
} ${opacityFromScore(
0
)} col-start-2 col-end-2 row-start-1 row-end-1 text-gray-600 mt-3 mb-3`}
>
<svg className="ml-6 mr-1 mt-2" height="10" width="16">
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
</svg>
{`Last updated: ${
timestamp ? timestamp.slice(0, 10) : "unknown"
}`}
</div>
</>
)}
{platform !== "guesstimate" && options.length < 3 && (
<div className={`text-gray-500 ${opacityFromScore(0)} mt-4`}>
{displayMarkdown(description)}
</div>
)}
{platform === "guesstimate" && (
<img
className="rounded-sm mb-1"
src={visualization}
alt="Guesstimate Screenshot"
/>
)}
</div>
<div
className={`flex sm:hidden ${
!showTimeStamp ? "hidden" : ""
} items-center justify-center mt-2 mb-4 text-gray-600`}
>
{/* This one is exclusively for mobile*/}
<svg className="" height="10" width="16">
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
</svg>
{`Last updated: ${timestamp ? timestamp.slice(0, 10) : "unknown"}`}
</div>
<div className={`${opacityFromScore(0)} w-full`}>
{forecastFooter({
stars: qualityindicators.stars,
platform: platform,
platformLabel: platformLabel || platform, // author || platformLabel,
numforecasts: qualityindicators.numforecasts,
qualityindicators,
timestamp,
showTimeStamp,
expandFooterToFullWidth,
})}
</div>
</a>
);
};
import { DisplayForecast } from "./DisplayForecast";
interface Props {
results: FrontendForecast[];
@ -748,7 +9,7 @@ interface Props {
showIdToggle: boolean;
}
const DisplayForecasts: React.FC<Props> = ({
export const DisplayForecasts: React.FC<Props> = ({
results,
numDisplay,
showIdToggle,
@ -765,6 +26,7 @@ const DisplayForecasts: React.FC<Props> = ({
: displayForecast({ ...fuseSearchResult.item });
*/
<DisplayForecast
key={result.id}
forecast={result}
showTimeStamp={false}
expandFooterToFullWidth={false}
@ -774,5 +36,3 @@ const DisplayForecasts: React.FC<Props> = ({
</>
);
};
export default DisplayForecasts;

View File

@ -1,4 +1,4 @@
import displayForecasts from "./displayForecasts";
import { DisplayForecasts } from "./DisplayForecasts";
import displayOneForecast from "./displayOneForecastForCapture";
export function displayForecastsWrapperForSearch({
@ -8,7 +8,11 @@ export function displayForecastsWrapperForSearch({
}) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{displayForecasts({ results: results || [], numDisplay, showIdToggle })}
<DisplayForecasts
results={results || []}
numDisplay={numDisplay}
showIdToggle={showIdToggle}
/>
</div>
);
}

View File

@ -4,7 +4,7 @@ import { CopyToClipboard } from "react-copy-to-clipboard";
import { FrontendForecast } from "../platforms";
import { uploadToImgur } from "../worker/uploadToImgur";
import { DisplayForecast } from "./displayForecasts";
import { DisplayForecast } from "./DisplayForecast";
function displayOneForecastInner(result: FrontendForecast, containerRef) {
return (

View File

@ -67,7 +67,7 @@ class ErrorBoundary extends React.Component<
}
/* Main */
export default function Layout({ page, children }) {
export const Layout = ({ page, children }) => {
let lastUpdated = calculateLastUpdate();
// The correct way to do this would be by passing a prop to Layout,
// and to get the last updating using server side props.
@ -150,4 +150,4 @@ export default function Layout({ page, children }) {
</div>
</div>
);
}
};

View File

@ -1,10 +1,10 @@
import { useRouter } from "next/router";
import React, { DependencyList, EffectCallback, Fragment, useEffect, useState } from "react";
import ButtonsForStars from "../display/buttonsForStars";
import Form from "../display/form";
import { MultiSelectPlatform } from "../display/multiSelectPlatforms";
import { SliderElement } from "../display/slider";
import { ButtonsForStars } from "../display/ButtonsForStars";
import { MultiSelectPlatform } from "../display/MultiSelectPlatform";
import { QueryForm } from "../display/QueryForm";
import { SliderElement } from "../display/SliderElement";
import { FrontendForecast } from "../platforms";
import searchAccordingToQueryData from "../worker/searchAccordingToQueryData";
import { Props as AnySearchPageProps, QueryParameters } from "./anySearchPage";
@ -76,7 +76,7 @@ const CommonDisplay: React.FC<Props> = ({
numDisplay,
};
let filterManually = (
const filterManually = (
queryData: QueryParameters,
results: FrontendForecast[]
) => {
@ -102,7 +102,7 @@ const CommonDisplay: React.FC<Props> = ({
const queryIsEmpty =
!queryData || queryData.query == "" || queryData.query == undefined;
let results = queryIsEmpty
const results = queryIsEmpty
? filterManually(queryData, defaultResults)
: await searchAccordingToQueryData(queryData, numDisplay);
@ -110,8 +110,8 @@ const CommonDisplay: React.FC<Props> = ({
}
// I don't want the function which display forecasts (displayForecasts) to change with a change in queryParameters. But I want it to have access to the queryParameters, and in particular access to queryParameters.numDisplay. Hence why this function lives inside Home.
let getInfoToDisplayForecastsFunction = () => {
let numDisplayRounded =
const getInfoToDisplayForecastsFunction = () => {
const numDisplayRounded =
numDisplay % 3 != 0
? numDisplay + (3 - (Math.round(numDisplay) % 3))
: numDisplay;
@ -156,7 +156,7 @@ const CommonDisplay: React.FC<Props> = ({
useNoInitialEffect(() => {
setResults([]);
let newTimeoutId = setTimeout(() => {
const newTimeoutId = setTimeout(() => {
updateRoute();
executeSearchOrAnswerWithDefaultResults();
}, 500);
@ -170,7 +170,7 @@ const CommonDisplay: React.FC<Props> = ({
/* State controllers */
/* Change the stars threshold */
let onChangeStars = (value: number) => {
const onChangeStars = (value: number) => {
setQueryParameters({
...queryParameters,
starsThreshold: value,
@ -178,7 +178,7 @@ const CommonDisplay: React.FC<Props> = ({
};
/* Change the number of elements to display */
let displayFunctionNumDisplaySlider = (value) => {
const displayFunctionNumDisplaySlider = (value: number) => {
return (
"Show " +
Math.round(value) +
@ -186,16 +186,16 @@ const CommonDisplay: React.FC<Props> = ({
(Math.round(value) === 1 ? "" : "s")
);
};
let onChangeSliderForNumDisplay = (event) => {
const onChangeSliderForNumDisplay = (event) => {
setNumDisplay(Math.round(event[0]));
setForceSearch(forceSearch + 1); // FIXME - force new search iff numDisplay is greater than last search limit
};
/* Change the forecast threshold */
let displayFunctionNumForecasts = (value: number) => {
const displayFunctionNumForecasts = (value: number) => {
return "# Forecasts > " + Math.round(value);
};
let onChangeSliderForNumForecasts = (event) => {
const onChangeSliderForNumForecasts = (event) => {
setQueryParameters({
...queryParameters,
forecastsThreshold: Math.round(event[0]),
@ -203,7 +203,7 @@ const CommonDisplay: React.FC<Props> = ({
};
/* Change on the search bar */
let onChangeSearchBar = (value: string) => {
const onChangeSearchBar = (value: string) => {
setQueryParameters({
...queryParameters,
query: value,
@ -211,7 +211,7 @@ const CommonDisplay: React.FC<Props> = ({
};
/* Change selected platforms */
let onChangeSelectedPlatforms = (value) => {
const onChangeSelectedPlatforms = (value) => {
setQueryParameters({
...queryParameters,
forecastingPlatforms: value,
@ -219,18 +219,18 @@ const CommonDisplay: React.FC<Props> = ({
};
// Change show id
let onChangeShowId = () => {
const onChangeShowId = () => {
setShowIdToggle(!showIdToggle);
};
// Capture functionality
let onClickBack = () => {
let decreaseUntil0 = (num: number) => (num - 1 > 0 ? num - 1 : 0);
const onClickBack = () => {
const decreaseUntil0 = (num: number) => (num - 1 > 0 ? num - 1 : 0);
setWhichResultToDisplayAndCapture(
decreaseUntil0(whichResultToDisplayAndCapture)
);
};
let onClickForward = (whichResultToDisplayAndCapture: number) => {
const onClickForward = (whichResultToDisplayAndCapture: number) => {
setWhichResultToDisplayAndCapture(whichResultToDisplayAndCapture + 1);
};
@ -240,7 +240,7 @@ const CommonDisplay: React.FC<Props> = ({
<label className="mb-4 mt-4 flex flex-row justify-center items-center">
{hasSearchbar ? (
<div className="w-10/12 mb-2">
<Form
<QueryForm
value={queryParameters.query}
onChange={onChangeSearchBar}
placeholder={placeholder}

View File

@ -58,10 +58,6 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
).split("|");
}
const platformNameToLabel = Object.fromEntries(
platforms.map((platform) => [platform.name, platform.label])
);
const defaultNumDisplay = 21;
const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay;