refactor: question components

This commit is contained in:
Vyacheslav Matyukhin 2022-04-26 01:34:18 +04:00
parent ab6f17ffe0
commit 73a47d94c3
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
4 changed files with 210 additions and 273 deletions

View File

@ -3,57 +3,28 @@ import { QuestionFragment } from "../../search/queries.generated";
type QualityIndicator = QuestionFragment["qualityIndicators"]; type QualityIndicator = QuestionFragment["qualityIndicators"];
type IndicatorName = keyof QualityIndicator; type IndicatorName = keyof QualityIndicator;
const formatQualityIndicator = (indicator: IndicatorName) => { // this duplication can probably be simplified with typescript magic, but this is good enough for now
let result: string | null = null; type UsedIndicatorName =
switch (indicator) { | "volume"
case "numForecasts": | "numForecasters"
result = null; | "spread"
break; | "sharesVolume"
| "liquidity"
| "tradeVolume"
| "openInterest";
case "stars": const qualityIndicatorLabels: { [k in UsedIndicatorName]: string } = {
result = null; // numForecasts: null,
break; // stars: null,
// yesBid: "Yes bid",
case "volume": // yesAsk: "Yes ask",
result = "Volume"; volume: "Volume",
break; numForecasters: "Forecasters",
spread: "Spread",
case "numForecasters": sharesVolume: "Shares vol.",
result = "Forecasters"; liquidity: "Liquidity",
break; tradeVolume: "Volume",
openInterest: "Interest",
// case "yesBid":
// result = null; // "Yes bid"
// break;
// case "yesAsk":
// result = null; // "Yes ask"
// break;
case "spread":
result = "Spread";
break;
case "sharesVolume":
result = "Shares vol.";
break;
case "openInterest":
result = "Interest";
break;
// case "resolution_data":
// result = null;
// break;
case "liquidity":
result = "Liquidity";
break;
case "tradeVolume":
result = "Volume";
break;
}
return result;
}; };
const formatNumber = (num) => { const formatNumber = (num) => {
@ -66,27 +37,16 @@ const formatNumber = (num) => {
} }
}; };
const formatQualityIndicators = (qualityIndicators: QualityIndicator) => {
let newQualityIndicators: { [k: string]: string | number } = {};
for (const key of Object.keys(qualityIndicators)) {
const newKey = formatQualityIndicator(key as IndicatorName);
if (newKey && qualityIndicators[key] !== null) {
newQualityIndicators[newKey] = qualityIndicators[key];
}
}
return newQualityIndicators;
};
/* Display functions*/ /* Display functions*/
const getPercentageSymbolIfNeeded = ({ const getPercentageSymbolIfNeeded = ({
indicator, indicator,
platform, platform,
}: { }: {
indicator: string; indicator: UsedIndicatorName;
platform: string; platform: string;
}) => { }) => {
let indicatorsWhichNeedPercentageSymbol = ["Spread"]; let indicatorsWhichNeedPercentageSymbol: IndicatorName[] = ["spread"];
if (indicatorsWhichNeedPercentageSymbol.includes(indicator)) { if (indicatorsWhichNeedPercentageSymbol.includes(indicator)) {
return "%"; return "%";
} else { } else {
@ -98,10 +58,15 @@ const getCurrencySymbolIfNeeded = ({
indicator, indicator,
platform, platform,
}: { }: {
indicator: string; indicator: UsedIndicatorName;
platform: string; platform: string;
}) => { }) => {
let indicatorsWhichNeedCurrencySymbol = ["Volume", "Interest", "Liquidity"]; const indicatorsWhichNeedCurrencySymbol: IndicatorName[] = [
"volume",
"tradeVolume",
"openInterest",
"liquidity",
];
let dollarPlatforms = ["predictit", "kalshi", "polymarket"]; let dollarPlatforms = ["predictit", "kalshi", "polymarket"];
if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) { if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) {
if (dollarPlatforms.includes(platform)) { if (dollarPlatforms.includes(platform)) {
@ -114,66 +79,50 @@ const getCurrencySymbolIfNeeded = ({
} }
}; };
const showFirstQualityIndicator: React.FC<{ const FirstQualityIndicator: React.FC<{
question: QuestionFragment; question: QuestionFragment;
showTimeStamp: boolean; }> = ({ question }) => {
}> = ({ question, showTimeStamp }) => { if (question.qualityIndicators.numForecasts) {
const lastUpdated = new Date(question.timestamp * 1000);
if (!!question.qualityIndicators.numForecasts) {
return ( return (
<div className="flex col-span-1 row-span-1"> <div className="flex">
{/*<span>{` ${numforecasts == 1 ? "Forecast" : "Forecasts:"}`}</span>&nbsp;*/}
<span>Forecasts:</span>&nbsp; <span>Forecasts:</span>&nbsp;
<span className="font-bold"> <span className="font-bold">
{Number(question.qualityIndicators.numForecasts).toFixed(0)} {Number(question.qualityIndicators.numForecasts).toFixed(0)}
</span> </span>
</div> </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: ${
lastUpdated ? lastUpdated.toISOString().slice(0, 10) : "unknown"
}`}
</span>
);
} else { } else {
return null; return null;
} }
}; };
const displayQualityIndicators: React.FC<{ const QualityIndicatorsList: React.FC<{
question: QuestionFragment; question: QuestionFragment;
showTimeStamp: boolean; }> = ({ question }) => {
}> = ({ question, showTimeStamp }) => {
const { qualityIndicators } = question;
return ( return (
<div className="text-sm"> <div className="text-sm">
{showFirstQualityIndicator({ <FirstQualityIndicator question={question} />
question, {Object.entries(question.qualityIndicators).map((entry, i) => {
showTimeStamp, const indicatorLabel = qualityIndicatorLabels[entry[0]];
})} if (!indicatorLabel || entry[1] === null) return;
{Object.entries(formatQualityIndicators(question.qualityIndicators)).map( const indicator = entry[0] as UsedIndicatorName; // guaranteed by the previous line
(entry, i) => { const value = entry[1];
return ( return (
<div className="col-span-1 row-span-1"> <div key={indicator}>
<span>${entry[0]}:</span>&nbsp; <span>{indicatorLabel}:</span>&nbsp;
<span className="font-bold"> <span className="font-bold">
{`${getCurrencySymbolIfNeeded({ {`${getCurrencySymbolIfNeeded({
indicator: entry[0], indicator,
platform: question.platform.id, platform: question.platform.id,
})}${formatNumber(entry[1])}${getPercentageSymbolIfNeeded({ })}${formatNumber(value)}${getPercentageSymbolIfNeeded({
indicator: entry[0], indicator,
platform: question.platform.id, platform: question.platform.id,
})}`} })}`}
</span> </span>
</div> </div>
); );
} })}
)}
</div> </div>
); );
}; };
@ -235,16 +184,13 @@ function getStarsColor(numstars: number) {
interface Props { interface Props {
question: QuestionFragment; question: QuestionFragment;
showTimeStamp: boolean;
expandFooterToFullWidth: boolean; expandFooterToFullWidth: boolean;
} }
export const QuestionFooter: React.FC<Props> = ({ export const QuestionFooter: React.FC<Props> = ({
question, question,
showTimeStamp,
expandFooterToFullWidth, expandFooterToFullWidth,
}) => { }) => {
let debuggingWithBackground = false;
return ( return (
<div <div
className={`grid grid-cols-3 ${ className={`grid grid-cols-3 ${
@ -254,14 +200,14 @@ export const QuestionFooter: React.FC<Props> = ({
<div <div
className={`self-center col-span-1 ${getStarsColor( className={`self-center col-span-1 ${getStarsColor(
question.qualityIndicators.stars question.qualityIndicators.stars
)} ${debuggingWithBackground ? "bg-red-200" : ""}`} )}`}
> >
{getstars(question.qualityIndicators.stars)} {getstars(question.qualityIndicators.stars)}
</div> </div>
<div <div
className={`${ className={`${
expandFooterToFullWidth ? "place-self-center" : "self-center" expandFooterToFullWidth ? "place-self-center" : "self-center"
} col-span-1 font-bold ${debuggingWithBackground ? "bg-red-100" : ""}`} } col-span-1 font-bold`}
> >
{question.platform.label {question.platform.label
.replace("Good Judgment Open", "GJOpen") .replace("Good Judgment Open", "GJOpen")
@ -272,12 +218,9 @@ export const QuestionFooter: React.FC<Props> = ({
expandFooterToFullWidth expandFooterToFullWidth
? "justify-self-end mr-4" ? "justify-self-end mr-4"
: "justify-self-center" : "justify-self-center"
} col-span-1 ${debuggingWithBackground ? "bg-red-100" : ""}`} } col-span-1`}
> >
{displayQualityIndicators({ <QualityIndicatorsList question={question} />
question,
showTimeStamp,
})}
</div> </div>
</div> </div>
); );

View File

@ -4,7 +4,6 @@ import ReactMarkdown from "react-markdown";
import { CopyText } from "../../common/CopyText"; import { CopyText } from "../../common/CopyText";
import { QuestionOptions } from "../../questions/components/QuestionOptions"; import { QuestionOptions } from "../../questions/components/QuestionOptions";
import { formatProbability } from "../../questions/utils";
import { QuestionFragment } from "../../search/queries.generated"; import { QuestionFragment } from "../../search/queries.generated";
import { Card } from "../Card"; import { Card } from "../Card";
import { QuestionFooter } from "./QuestionFooter"; import { QuestionFooter } from "./QuestionFooter";
@ -87,99 +86,12 @@ const cleanText = (text: string): string => {
return 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 // Auxiliary components
const DisplayMarkdown: React.FC<{ description: string }> = ({ const DisplayMarkdown: React.FC<{ description: string }> = ({
description, description,
}) => { }) => {
let formatted = truncateText(250, cleanText(description)); const formatted = truncateText(250, cleanText(description));
// overflow-hidden overflow-ellipsis h-24 // overflow-hidden overflow-ellipsis h-24
return formatted === "" ? null : ( return formatted === "" ? null : (
<div className="overflow-clip"> <div className="overflow-clip">
@ -217,19 +129,10 @@ export const DisplayQuestion: React.FC<Props> = ({
expandFooterToFullWidth, expandFooterToFullWidth,
showIdToggle, showIdToggle,
}) => { }) => {
const { const { options } = question;
platform, const lastUpdated = new Date(question.timestamp * 1000);
description,
options,
qualityIndicators,
timestamp,
visualization,
} = question;
const lastUpdated = new Date(timestamp * 1000);
const displayTimestampAtBottom =
checkIfDisplayTimeStampAtBottom(qualityIndicators);
const yesNoOptions = const isBinary =
options.length === 2 && options.length === 2 &&
(options[0].name === "Yes" || options[0].name === "No"); (options[0].name === "Yes" || options[0].name === "No");
@ -243,7 +146,7 @@ export const DisplayQuestion: React.FC<Props> = ({
</div> </div>
) : null} ) : null}
<div> <div>
<Link href={`/questions/${question.id}`}> <Link href={`/questions/${question.id}`} passHref>
<a className="float-right block ml-2 mt-1.5"> <a className="float-right block ml-2 mt-1.5">
<FaExpand <FaExpand
size="18" size="18"
@ -261,41 +164,17 @@ export const DisplayQuestion: React.FC<Props> = ({
</a> </a>
</Card.Title> </Card.Title>
</div> </div>
{yesNoOptions && ( {isBinary ? (
<div className="flex justify-between"> <div className="flex justify-between">
<div className="space-x-2"> <QuestionOptions options={options} />
<span <div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}>
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={lastUpdated} /> <LastUpdated timestamp={lastUpdated} />
</div> </div>
</div> </div>
)} ) : (
{!yesNoOptions && (
<div className="space-y-2"> <div className="space-y-2">
<QuestionOptions options={options} /> <QuestionOptions options={options} />
<div <div className={`hidden ${showTimeStamp ? "sm:block" : ""} ml-6`}>
className={`hidden ${
showTimeStamp && !displayTimestampAtBottom ? "sm:block" : ""
} ml-6`}
>
<LastUpdated timestamp={lastUpdated} /> <LastUpdated timestamp={lastUpdated} />
</div> </div>
</div> </div>
@ -303,14 +182,14 @@ export const DisplayQuestion: React.FC<Props> = ({
{question.platform.id !== "guesstimate" && options.length < 3 && ( {question.platform.id !== "guesstimate" && options.length < 3 && (
<div className="text-gray-500"> <div className="text-gray-500">
<DisplayMarkdown description={description} /> <DisplayMarkdown description={question.description} />
</div> </div>
)} )}
{question.platform.id === "guesstimate" && ( {question.platform.id === "guesstimate" && (
<img <img
className="rounded-sm" className="rounded-sm"
src={visualization} src={question.visualization}
alt="Guesstimate Screenshot" alt="Guesstimate Screenshot"
/> />
)} )}
@ -324,7 +203,6 @@ export const DisplayQuestion: React.FC<Props> = ({
<div className="w-full"> <div className="w-full">
<QuestionFooter <QuestionFooter
question={question} question={question}
showTimeStamp={showTimeStamp && displayTimestampAtBottom}
expandFooterToFullWidth={expandFooterToFullWidth} expandFooterToFullWidth={expandFooterToFullWidth}
/> />
</div> </div>

View File

@ -1,7 +1,83 @@
import { QuestionFragment } from "../../search/queries.generated";
import { formatProbability } from "../utils"; import { formatProbability } from "../utils";
const OptionRow: React.FC<{ option: any }> = ({ option }) => { type Option = QuestionFragment["options"][0];
const chooseColor = (probability: number) => {
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 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 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";
}
};
const chooseColor = (probability: number) => {
if (probability < 0.1) { if (probability < 0.1) {
return "bg-blue-50 text-blue-500"; return "bg-blue-50 text-blue-500";
} else if (probability < 0.3) { } else if (probability < 0.3) {
@ -11,8 +87,9 @@ const OptionRow: React.FC<{ option: any }> = ({ option }) => {
} else { } else {
return "bg-blue-300 text-blue-800"; return "bg-blue-300 text-blue-800";
} }
}; };
const OptionRow: React.FC<{ option: Option }> = ({ option }) => {
return ( return (
<div className="flex items-center"> <div className="flex items-center">
<div <div
@ -29,9 +106,36 @@ const OptionRow: React.FC<{ option: any }> = ({ option }) => {
); );
}; };
export const QuestionOptions: React.FC<{ options: any[] }> = ({ options }) => { export const QuestionOptions: React.FC<{ options: Option[] }> = ({
options,
}) => {
const isBinary =
options.length === 2 &&
(options[0].name === "Yes" || options[0].name === "No");
const optionsSorted = options.sort((a, b) => b.probability - a.probability); const optionsSorted = options.sort((a, b) => b.probability - a.probability);
const optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options. const optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options.
if (isBinary) {
return (
<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>
);
} else {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{optionsMax5.map((option, i) => ( {optionsMax5.map((option, i) => (
@ -39,4 +143,5 @@ export const QuestionOptions: React.FC<{ options: any[] }> = ({ options }) => {
))} ))}
</div> </div>
); );
}
}; };

View File

@ -3,6 +3,7 @@ import ReactMarkdown from "react-markdown";
import { Query } from "../../common/Query"; import { Query } from "../../common/Query";
import { Card } from "../../display/Card"; import { Card } from "../../display/Card";
import { QuestionFooter } from "../../display/DisplayQuestion/QuestionFooter";
import { Layout } from "../../display/Layout"; import { Layout } from "../../display/Layout";
import { QuestionFragment } from "../../search/queries.generated"; import { QuestionFragment } from "../../search/queries.generated";
import { ssrUrql } from "../../urql"; import { ssrUrql } from "../../urql";
@ -39,8 +40,18 @@ const QuestionCardContents: React.FC<{ question: QuestionFragment }> = ({
question, question,
}) => ( }) => (
<div className="space-y-4"> <div className="space-y-4">
<h1>{question.title}</h1> <h1>
<a
className="text-black no-underline"
href={question.url}
target="_blank"
>
{question.title}
</a>
</h1>
<QuestionFooter question={question} expandFooterToFullWidth={true} />
<QuestionOptions options={question.options} /> <QuestionOptions options={question.options} />
<ReactMarkdown linkTarget="_blank" className="font-normal"> <ReactMarkdown linkTarget="_blank" className="font-normal">
{question.description} {question.description}
</ReactMarkdown> </ReactMarkdown>