refactor: question components
This commit is contained in:
		
							parent
							
								
									ab6f17ffe0
								
							
						
					
					
						commit
						73a47d94c3
					
				|  | @ -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> */} | ||||
|       <div className="flex"> | ||||
|         <span>Forecasts:</span>  | ||||
|         <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, | ||||
|       <FirstQualityIndicator question={question} /> | ||||
|       {Object.entries(question.qualityIndicators).map((entry, i) => { | ||||
|         const indicatorLabel = qualityIndicatorLabels[entry[0]]; | ||||
|         if (!indicatorLabel || entry[1] === null) return; | ||||
|         const indicator = entry[0] as UsedIndicatorName; // guaranteed by the previous line
 | ||||
|         const value = entry[1]; | ||||
| 
 | ||||
|         return ( | ||||
|           <div key={indicator}> | ||||
|             <span>{indicatorLabel}:</span>  | ||||
|             <span className="font-bold"> | ||||
|               {`${getCurrencySymbolIfNeeded({ | ||||
|                 indicator, | ||||
|                 platform: question.platform.id, | ||||
|               })}${formatNumber(value)}${getPercentageSymbolIfNeeded({ | ||||
|                 indicator, | ||||
|                 platform: question.platform.id, | ||||
|               })}`}
 | ||||
|             </span> | ||||
|           </div> | ||||
|         ); | ||||
|       })} | ||||
|       {Object.entries(formatQualityIndicators(question.qualityIndicators)).map( | ||||
|         (entry, i) => { | ||||
|           return ( | ||||
|             <div className="col-span-1 row-span-1"> | ||||
|               <span>${entry[0]}:</span>  | ||||
|               <span className="font-bold"> | ||||
|                 {`${getCurrencySymbolIfNeeded({ | ||||
|                   indicator: entry[0], | ||||
|                   platform: question.platform.id, | ||||
|                 })}${formatNumber(entry[1])}${getPercentageSymbolIfNeeded({ | ||||
|                   indicator: entry[0], | ||||
|                   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> | ||||
|   ); | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -1,18 +1,95 @@ | |||
| import { QuestionFragment } from "../../search/queries.generated"; | ||||
| import { formatProbability } from "../utils"; | ||||
| 
 | ||||
| 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"; | ||||
|     } | ||||
|   }; | ||||
| 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"; | ||||
|   } 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"; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const OptionRow: React.FC<{ option: Option }> = ({ option }) => { | ||||
|   return ( | ||||
|     <div className="flex items-center"> | ||||
|       <div | ||||
|  | @ -29,14 +106,42 @@ 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.
 | ||||
|   return ( | ||||
|     <div className="space-y-2"> | ||||
|       {optionsMax5.map((option, i) => ( | ||||
|         <OptionRow option={option} key={i} /> | ||||
|       ))} | ||||
|     </div> | ||||
|   ); | ||||
| 
 | ||||
|   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) => ( | ||||
|           <OptionRow option={option} key={i} /> | ||||
|         ))} | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| }; | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user