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 ReactMarkdown from "react-markdown";
import gfm from "remark-gfm"; 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). 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 React from "react";
import { displayForecastsWrapperForCapture } from "../web/display/displayForecastsWrappers"; 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 { Props } from "../web/search/anySearchPage";
import CommonDisplay from "../web/search/CommonDisplay"; import CommonDisplay from "../web/search/CommonDisplay";

View File

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

View File

@ -1,6 +1,7 @@
import { NextPage } from "next";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
function Recursion() { const Recursion: NextPage = () => {
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; 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> <h2>You have now reached the fourth level of recursion!!</h2>
</div> </div>
); );
} };
export default Recursion; 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 { useState } from "react";
import { getPlatformsConfig } from "../backend/platforms"; import { getPlatformsConfig } from "../backend/platforms";
import displayForecasts from "../web/display/displayForecasts"; import { DisplayForecasts } from "../web/display/DisplayForecasts";
import { addLabelsToForecasts } from "../web/platforms"; import { addLabelsToForecasts } from "../web/platforms";
import { getDashboardForecastsByDashboardId } from "../web/worker/getDashboardForecasts"; import { getDashboardForecastsByDashboardId } from "../web/worker/getDashboardForecasts";
@ -88,11 +88,11 @@ export default function Home({
numCols || 3 numCols || 3
} gap-4 mb-6`} } gap-4 mb-6`}
> >
{displayForecasts({ <DisplayForecasts
results: dashboardForecasts, results={dashboardForecasts}
numDisplay: dashboardForecasts.length, numDisplay={dashboardForecasts.length}
showIdToggle: false, showIdToggle={false}
})} />
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -1,85 +1,76 @@
import Link from "next/link"; import Link from "next/link";
import React from "react"; 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 */ /* Display one tool */
function displayTool({ const ToolCard: React.FC<Tool> = (tool) => {
sameWebpage, const inner = (
title, <Card>
description, <div className="grid content-start gap-3">
link, <div className="text-gray-800 text-lg font-medium">{tool.title}</div>
url, <div className="text-gray-500">{tool.description}</div>
img, {tool.img && <img src={tool.img} className="text-gray-500" />}
i, </div>
}: any) { </Card>
switch (sameWebpage) { );
case true:
if ("innerLink" in tool) {
return ( return (
<Link href={link} passHref key={`tool-${i}`}> <Link href={tool.innerLink} passHref>
<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"> <a className="textinherit no-underline">{inner}</a>
<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> </Link>
); );
break; } else if ("externalLink" in tool) {
default:
return ( return (
<a <a href={tool.externalLink} className="textinherit no-underline">
href={url} {inner}
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> </a>
); );
break; } else {
} return inner;
} }
};
export default function Tools({ lastUpdated }) { export default function Tools({ lastUpdated }) {
let tools = [ let tools: Tool[] = [
{ {
title: "Search", title: "Search",
description: "Find forecasting questions on many platforms", description: "Find forecasting questions on many platforms.",
link: "/", innerLink: "/",
sameWebpage: true,
img: "https://i.imgur.com/Q94gVqG.png", img: "https://i.imgur.com/Q94gVqG.png",
}, },
{ {
title: "[Beta] Present", title: "[Beta] Present",
description: "Present forecasts in dashboards.", description: "Present forecasts in dashboards.",
sameWebpage: true, innerLink: "/dashboards",
link: "/dashboards",
img: "https://i.imgur.com/x8qkuHQ.png", img: "https://i.imgur.com/x8qkuHQ.png",
}, },
{ {
title: "Capture", title: "Capture",
description: 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.", "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", innerLink: "/capture",
sameWebpage: true,
img: "https://i.imgur.com/EXkFBzz.png", img: "https://i.imgur.com/EXkFBzz.png",
}, },
{ {
title: "Summon", title: "Summon",
description: description:
"Summon metaforecast on Twitter by mentioning @metaforecast, or on Discord by using Fletcher and !metaforecast, followed by search terms", "Summon metaforecast on Twitter by mentioning @metaforecast, or on Discord by using Fletcher and !metaforecast, followed by search terms.",
url: "https://twitter.com/metaforecast", externalLink: "https://twitter.com/metaforecast",
img: "https://i.imgur.com/BQ4Zzjw.png", img: "https://i.imgur.com/BQ4Zzjw.png",
}, },
{ {
@ -87,7 +78,6 @@ export default function Tools({ lastUpdated }) {
description: description:
"Interact with metaforecast's API and fetch forecasts for your application. Currently possible but documentation is poor, get in touch.", "Interact with metaforecast's API and fetch forecasts for your application. Currently possible but documentation is poor, get in touch.",
}, },
{ {
title: "[Upcoming] Record", title: "[Upcoming] Record",
description: "Save your forecasts or bets.", description: "Save your forecasts or bets.",
@ -95,8 +85,10 @@ export default function Tools({ lastUpdated }) {
]; ];
return ( return (
<Layout page="tools"> <Layout page="tools">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4 mb-8"> <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) => displayTool({ ...tool, i }))} {tools.map((tool, i) => (
<ToolCard {...tool} key={`tool-${i}`} />
))}
</div> </div>
</Layout> </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"; 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) => { const handleInputChange = (event) => {
event.preventDefault(); event.preventDefault();
onChange(event.target.value); // In this case, the query, e.g. "COVID.19" 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> </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 */ /* Body */
// Two functions, essentially identical. // Two functions, essentially identical.
export function SliderElement({ onChange, value, displayFunction }) { export const SliderElement: React.FC<Props> = ({
onChange,
value,
displayFunction,
}) => {
return ( return (
<Slider <Slider
rootStyle={ rootStyle={
@ -122,4 +132,4 @@ export function SliderElement({ onChange, value, displayFunction }) {
</Tracks> </Tracks>
</Slider> </Slider>
); );
} };

View File

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

View File

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

View File

@ -1,746 +1,7 @@
/* Imports */
import React from "react"; import React from "react";
import { FaRegClipboard } from "react-icons/fa";
import ReactMarkdown from "react-markdown";
import { FrontendForecast } from "../platforms"; import { FrontendForecast } from "../platforms";
import { DisplayForecast } from "./DisplayForecast";
/* 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>
);
};
interface Props { interface Props {
results: FrontendForecast[]; results: FrontendForecast[];
@ -748,7 +9,7 @@ interface Props {
showIdToggle: boolean; showIdToggle: boolean;
} }
const DisplayForecasts: React.FC<Props> = ({ export const DisplayForecasts: React.FC<Props> = ({
results, results,
numDisplay, numDisplay,
showIdToggle, showIdToggle,
@ -765,6 +26,7 @@ const DisplayForecasts: React.FC<Props> = ({
: displayForecast({ ...fuseSearchResult.item }); : displayForecast({ ...fuseSearchResult.item });
*/ */
<DisplayForecast <DisplayForecast
key={result.id}
forecast={result} forecast={result}
showTimeStamp={false} showTimeStamp={false}
expandFooterToFullWidth={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"; import displayOneForecast from "./displayOneForecastForCapture";
export function displayForecastsWrapperForSearch({ export function displayForecastsWrapperForSearch({
@ -8,7 +8,11 @@ export function displayForecastsWrapperForSearch({
}) { }) {
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <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> </div>
); );
} }

View File

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

View File

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

View File

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

View File

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