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

View File

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

View File

@ -1,6 +1,82 @@
import { QuestionFragment } from "../../search/queries.generated";
import { formatProbability } from "../utils";
const OptionRow: React.FC<{ option: any }> = ({ option }) => {
type Option = QuestionFragment["options"][0];
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) {
return "bg-blue-50 text-blue-500";
@ -13,6 +89,7 @@ const OptionRow: React.FC<{ option: any }> = ({ option }) => {
}
};
const OptionRow: React.FC<{ option: Option }> = ({ option }) => {
return (
<div className="flex items-center">
<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 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 (
<div className="space-y-2">
{optionsMax5.map((option, i) => (
@ -39,4 +143,5 @@ export const QuestionOptions: React.FC<{ options: any[] }> = ({ options }) => {
))}
</div>
);
}
};

View File

@ -3,6 +3,7 @@ import ReactMarkdown from "react-markdown";
import { Query } from "../../common/Query";
import { Card } from "../../display/Card";
import { QuestionFooter } from "../../display/DisplayQuestion/QuestionFooter";
import { Layout } from "../../display/Layout";
import { QuestionFragment } from "../../search/queries.generated";
import { ssrUrql } from "../../urql";
@ -39,8 +40,18 @@ const QuestionCardContents: React.FC<{ question: QuestionFragment }> = ({
question,
}) => (
<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} />
<ReactMarkdown linkTarget="_blank" className="font-normal">
{question.description}
</ReactMarkdown>