feat: question page style changes
This commit is contained in:
		
							parent
							
								
									69e5a6ece5
								
							
						
					
					
						commit
						6e25f8bcd2
					
				
							
								
								
									
										23
									
								
								src/web/common/BoxedLink.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/web/common/BoxedLink.tsx
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| import { FaExternalLinkAlt } from "react-icons/fa"; | ||||
| 
 | ||||
| type Props = { | ||||
|   url: string; | ||||
|   size?: "normal" | "small"; | ||||
| }; | ||||
| 
 | ||||
| export const BoxedLink: React.FC<Props> = ({ | ||||
|   url, | ||||
|   size = "normal", | ||||
|   children, | ||||
| }) => ( | ||||
|   <a | ||||
|     className={`px-2 py-1 border-2 border-gray-400 rounded-lg text-black no-underline text-normal hover:bg-gray-100 inline-flex flex-nowrap space-x-1 items-center ${ | ||||
|       size === "small" ? "text-sm" : "" | ||||
|     }`}
 | ||||
|     href={url} | ||||
|     target="_blank" | ||||
|   > | ||||
|     <span>{children}</span> | ||||
|     <FaExternalLinkAlt className="text-gray-400 inline" /> | ||||
|   </a> | ||||
| ); | ||||
|  | @ -1,7 +1,6 @@ | |||
| import domtoimage from "dom-to-image"; // https://github.com/tsayen/dom-to-image
 | ||||
| import { Resizable } from "re-resizable"; | ||||
| import { useEffect, useRef, useState } from "react"; | ||||
| 
 | ||||
| import { Button } from "../../common/Button"; | ||||
| import { CopyParagraph } from "../../common/CopyParagraph"; | ||||
| import { QuestionFragment } from "../../fragments.generated"; | ||||
|  | @ -80,6 +79,15 @@ const MetaculusSource: React.FC<{ | |||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const GrayContainer: React.FC<{ title: string }> = ({ title, children }) => ( | ||||
|   <div className="bg-gray-100 p-2 space-y-1"> | ||||
|     <div className="uppercase text-xs font-bold tracking-wide text-gray-600"> | ||||
|       {title}: | ||||
|     </div> | ||||
|     <div>{children}</div> | ||||
|   </div> | ||||
| ); | ||||
| 
 | ||||
| interface Props { | ||||
|   question: QuestionFragment; | ||||
| } | ||||
|  | @ -121,42 +129,53 @@ export const CaptureQuestion: React.FC<Props> = ({ question }) => { | |||
| 
 | ||||
|   if (imgSrc) { | ||||
|     return ( | ||||
|       <div className="flex flex-col items-center space-y-4"> | ||||
|         <div> | ||||
|           <img src={imgSrc} /> | ||||
|         </div> | ||||
|       <div className="space-y-4"> | ||||
|         <GrayContainer title="Generated image"> | ||||
|           <a href={imgSrc} target="_blank"> | ||||
|             <img src={imgSrc} /> | ||||
|           </a> | ||||
|         </GrayContainer> | ||||
|         <div> | ||||
|           <ImageSource question={question} imgSrc={imgSrc} /> | ||||
|         </div> | ||||
|         <div className="justify-self-stretch"> | ||||
|           <MetaculusEmbed question={question} /> | ||||
|         </div> | ||||
|         <div> | ||||
|           <MetaculusSource question={question} /> | ||||
|         </div> | ||||
|         {question.platform.id === "metaculus" ? ( | ||||
|           <> | ||||
|             <div className="justify-self-stretch"> | ||||
|               <MetaculusEmbed question={question} /> | ||||
|             </div> | ||||
|             <div> | ||||
|               <MetaculusSource question={question} /> | ||||
|             </div> | ||||
|           </> | ||||
|         ) : null} | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="flex flex-col items-center space-y-4"> | ||||
|       <Resizable | ||||
|         minWidth={320} | ||||
|         bounds="parent" | ||||
|         enable={{ right: true, left: true }} | ||||
|       > | ||||
|         <div ref={containerRef}> | ||||
|           <QuestionCard | ||||
|             question={question} | ||||
|             showTimeStamp={true} | ||||
|             showExpandButton={false} | ||||
|             expandFooterToFullWidth={true} | ||||
|           /> | ||||
|         </div> | ||||
|       </Resizable> | ||||
|       <div> | ||||
|         <Button onClick={onCaptureButtonClick}>{mainButtonText}</Button> | ||||
|       </div> | ||||
|     <div className="space-y-2 flex flex-col"> | ||||
|       <GrayContainer title="Resizable preview"> | ||||
|         <Resizable | ||||
|           minWidth={320} | ||||
|           bounds="window" | ||||
|           enable={{ right: true, left: true }} | ||||
|         > | ||||
|           <div ref={containerRef}> | ||||
|             <QuestionCard | ||||
|               container={(children) => ( | ||||
|                 <div className="px-4 py-3 bg-white">{children}</div> | ||||
|               )} | ||||
|               question={question} | ||||
|               showTimeStamp={true} | ||||
|               showExpandButton={false} | ||||
|               expandFooterToFullWidth={true} | ||||
|             /> | ||||
|           </div> | ||||
|         </Resizable> | ||||
|       </GrayContainer> | ||||
|       <Button onClick={onCaptureButtonClick} size="small"> | ||||
|         {mainButtonText} | ||||
|       </Button> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  |  | |||
|  | @ -1,16 +1,6 @@ | |||
| import { FaExternalLinkAlt } from "react-icons/fa"; | ||||
| 
 | ||||
| import { BoxedLink } from "../../common/BoxedLink"; | ||||
| import { QuestionFragment } from "../../fragments.generated"; | ||||
| 
 | ||||
| export const PlatformLink: React.FC<{ question: QuestionFragment }> = ({ | ||||
|   question, | ||||
| }) => ( | ||||
|   <a | ||||
|     className="px-2 py-1 border-2 border-gray-400 rounded-lg text-black no-underline text-normal hover:bg-gray-100 flex flex-nowrap space-x-1 items-center" | ||||
|     href={question.url} | ||||
|     target="_blank" | ||||
|   > | ||||
|     <span>{question.platform.label}</span> | ||||
|     <FaExternalLinkAlt className="text-gray-400 inline sm:text-md text-md" /> | ||||
|   </a> | ||||
| ); | ||||
| }) => <BoxedLink url={question.url}>{question.platform.label}</BoxedLink>; | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import Link from "next/link"; | ||||
| import { ReactElement, ReactNode } from "react"; | ||||
| import { FaExpand } from "react-icons/fa"; | ||||
| import ReactMarkdown from "react-markdown"; | ||||
| 
 | ||||
| import { Card } from "../../../common/Card"; | ||||
| import { CopyText } from "../../../common/CopyText"; | ||||
| import { QuestionFragment } from "../../../fragments.generated"; | ||||
|  | @ -63,6 +63,7 @@ const LastUpdated: React.FC<{ timestamp: Date }> = ({ timestamp }) => ( | |||
| // Main component
 | ||||
| 
 | ||||
| interface Props { | ||||
|   container: (children: ReactNode) => ReactElement; | ||||
|   question: QuestionFragment; | ||||
|   showTimeStamp: boolean; | ||||
|   expandFooterToFullWidth: boolean; | ||||
|  | @ -71,6 +72,7 @@ interface Props { | |||
| } | ||||
| 
 | ||||
| export const QuestionCard: React.FC<Props> = ({ | ||||
|   container = (children) => <Card>{children}</Card>, | ||||
|   question, | ||||
|   showTimeStamp, | ||||
|   expandFooterToFullWidth, | ||||
|  | @ -82,72 +84,70 @@ export const QuestionCard: React.FC<Props> = ({ | |||
| 
 | ||||
|   const isBinary = isQuestionBinary(question); | ||||
| 
 | ||||
|   return ( | ||||
|     <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={question.id} displayText={`[${question.id}]`} /> | ||||
|             </div> | ||||
|           ) : null} | ||||
|           <div> | ||||
|             {showExpandButton ? ( | ||||
|               <Link href={`/questions/${question.id}`} passHref> | ||||
|                 <a className="float-right block ml-2 mt-1.5"> | ||||
|                   <FaExpand | ||||
|                     size="18" | ||||
|                     className="text-gray-400 hover:text-gray-700" | ||||
|                   /> | ||||
|                 </a> | ||||
|               </Link> | ||||
|             ) : null} | ||||
|             <Card.Title> | ||||
|               <a | ||||
|                 className="text-black no-underline" | ||||
|                 href={question.url} | ||||
|                 target="_blank" | ||||
|               > | ||||
|                 {question.title} | ||||
|   return container( | ||||
|     <div className="h-full flex flex-col space-y-4"> | ||||
|       <div className="flex-grow space-y-4"> | ||||
|         {showIdToggle ? ( | ||||
|           <div className="mx-10"> | ||||
|             <CopyText text={question.id} displayText={`[${question.id}]`} /> | ||||
|           </div> | ||||
|         ) : null} | ||||
|         <div> | ||||
|           {showExpandButton ? ( | ||||
|             <Link href={`/questions/${question.id}`} passHref> | ||||
|               <a className="float-right block ml-2 mt-1.5"> | ||||
|                 <FaExpand | ||||
|                   size="18" | ||||
|                   className="text-gray-400 hover:text-gray-700" | ||||
|                 /> | ||||
|               </a> | ||||
|             </Card.Title> | ||||
|           </div> | ||||
|           <div className={isBinary ? "flex justify-between" : "space-y-4"}> | ||||
|             <QuestionOptions question={question} maxNumOptions={5} /> | ||||
|             <div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}> | ||||
|               <LastUpdated timestamp={lastUpdated} /> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           {question.platform.id !== "guesstimate" && options.length < 3 && ( | ||||
|             <div className="text-gray-500"> | ||||
|               <DisplayMarkdown description={question.description} /> | ||||
|             </div> | ||||
|           )} | ||||
| 
 | ||||
|           {question.platform.id === "guesstimate" && question.visualization && ( | ||||
|             <img | ||||
|               className="rounded-sm" | ||||
|               src={question.visualization} | ||||
|               alt="Guesstimate Screenshot" | ||||
|             /> | ||||
|           )} | ||||
|             </Link> | ||||
|           ) : null} | ||||
|           <Card.Title> | ||||
|             <a | ||||
|               className="text-black no-underline" | ||||
|               href={question.url} | ||||
|               target="_blank" | ||||
|             > | ||||
|               {question.title} | ||||
|             </a> | ||||
|           </Card.Title> | ||||
|         </div> | ||||
|         <div | ||||
|           className={`sm:hidden ${!showTimeStamp ? "hidden" : ""} self-center`} | ||||
|         > | ||||
|           {/* This one is exclusively for mobile*/} | ||||
|           <LastUpdated timestamp={lastUpdated} /> | ||||
|         </div> | ||||
|         <div className="w-full"> | ||||
|           <div className="mb-2 mt-1"> | ||||
|             <QuestionFooter | ||||
|               question={question} | ||||
|               expandFooterToFullWidth={expandFooterToFullWidth} | ||||
|             /> | ||||
|         <div className={isBinary ? "flex justify-between" : "space-y-4"}> | ||||
|           <QuestionOptions question={question} maxNumOptions={5} /> | ||||
|           <div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}> | ||||
|             <LastUpdated timestamp={lastUpdated} /> | ||||
|           </div> | ||||
|         </div> | ||||
| 
 | ||||
|         {question.platform.id !== "guesstimate" && options.length < 3 && ( | ||||
|           <div className="text-gray-500"> | ||||
|             <DisplayMarkdown description={question.description} /> | ||||
|           </div> | ||||
|         )} | ||||
| 
 | ||||
|         {question.platform.id === "guesstimate" && question.visualization && ( | ||||
|           <img | ||||
|             className="rounded-sm" | ||||
|             src={question.visualization} | ||||
|             alt="Guesstimate Screenshot" | ||||
|           /> | ||||
|         )} | ||||
|       </div> | ||||
|       <div | ||||
|         className={`sm:hidden ${!showTimeStamp ? "hidden" : ""} self-center`} | ||||
|       > | ||||
|         {/* This one is exclusively for mobile*/} | ||||
|         <LastUpdated timestamp={lastUpdated} /> | ||||
|       </div> | ||||
|       <div className="w-full"> | ||||
|         <div className="mb-2 mt-1"> | ||||
|           <QuestionFooter | ||||
|             question={question} | ||||
|             expandFooterToFullWidth={expandFooterToFullWidth} | ||||
|           /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </Card> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
|  |  | |||
|  | @ -1,11 +1,10 @@ | |||
| import { GetServerSideProps, NextPage } from "next"; | ||||
| import NextError from "next/error"; | ||||
| import ReactMarkdown from "react-markdown"; | ||||
| 
 | ||||
| import { BoxedLink } from "../../common/BoxedLink"; | ||||
| import { Card } from "../../common/Card"; | ||||
| import { CopyParagraph } from "../../common/CopyParagraph"; | ||||
| import { Layout } from "../../common/Layout"; | ||||
| import { LineHeader } from "../../common/LineHeader"; | ||||
| import { Query } from "../../common/Query"; | ||||
| import { QuestionWithHistoryFragment } from "../../fragments.generated"; | ||||
| import { ssrUrql } from "../../urql"; | ||||
|  | @ -44,12 +43,33 @@ export const getServerSideProps: GetServerSideProps<Props> = async ( | |||
| }; | ||||
| 
 | ||||
| const Section: React.FC<{ title: string }> = ({ title, children }) => ( | ||||
|   <div className="space-y-2 flex flex-col items-start"> | ||||
|     <h2 className="text-xl text-gray-900">{title}</h2> | ||||
|   <div className="space-y-4 flex flex-col items-start"> | ||||
|     <div className="border-b-2 border-gray-200 w-full"> | ||||
|       <h2 className="text-xl leading-3 text-gray-900">{title}</h2> | ||||
|     </div> | ||||
|     <div>{children}</div> | ||||
|   </div> | ||||
| ); | ||||
| 
 | ||||
| const EmbedSection: React.FC<{ question: QuestionWithHistoryFragment }> = ({ | ||||
|   question, | ||||
| }) => { | ||||
|   const url = getBasePath() + `/questions/embed/${question.id}`; | ||||
|   return ( | ||||
|     <Section title="Embed"> | ||||
|       <div className="mb-2"> | ||||
|         <BoxedLink url={url} size="small"> | ||||
|           Preview | ||||
|         </BoxedLink> | ||||
|       </div> | ||||
|       <CopyParagraph | ||||
|         text={`<iframe src="${url}" height="600" width="600" frameborder="0" />`} | ||||
|         buttonText="Copy HTML" | ||||
|       /> | ||||
|     </Section> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const LargeQuestionCard: React.FC<{ | ||||
|   question: QuestionWithHistoryFragment; | ||||
| }> = ({ question }) => { | ||||
|  | @ -65,7 +85,7 @@ const LargeQuestionCard: React.FC<{ | |||
|         <QuestionChartOrVisualization question={question} /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div className="mx-auto max-w-prose"> | ||||
|       <div className="mx-auto max-w-prose space-y-8"> | ||||
|         <Section title="Question description"> | ||||
|           <ReactMarkdown | ||||
|             linkTarget="_blank" | ||||
|  | @ -74,43 +94,18 @@ const LargeQuestionCard: React.FC<{ | |||
|             {question.description.replaceAll("---", "")} | ||||
|           </ReactMarkdown> | ||||
|         </Section> | ||||
|         <div className="mt-5"> | ||||
|           <Section title="Indicators"> | ||||
|             <IndicatorsTable question={question} /> | ||||
|           </Section> | ||||
|         </div> | ||||
|         <Section title="Indicators"> | ||||
|           <IndicatorsTable question={question} /> | ||||
|         </Section> | ||||
|         <Section title="Capture"> | ||||
|           <CaptureQuestion question={question} /> | ||||
|         </Section> | ||||
|         <EmbedSection question={question} /> | ||||
|       </div> | ||||
|     </Card> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const QuestionScreen: React.FC<{ question: QuestionWithHistoryFragment }> = ({ | ||||
|   question, | ||||
| }) => ( | ||||
|   <div className="space-y-8"> | ||||
|     <LargeQuestionCard question={question} /> | ||||
|     <div className="space-y-4"> | ||||
|       <LineHeader> | ||||
|         <h1>Capture</h1> | ||||
|       </LineHeader> | ||||
|       <CaptureQuestion question={question} /> | ||||
|     </div> | ||||
|     <div className="space-y-4"> | ||||
|       <LineHeader> | ||||
|         <h1>Embed</h1> | ||||
|       </LineHeader> | ||||
|       <div className="max-w-md mx-auto"> | ||||
|         <CopyParagraph | ||||
|           text={`<iframe src="${ | ||||
|             getBasePath() + `/questions/embed/${question.id}` | ||||
|           }" height="600" width="600" frameborder="0" />`}
 | ||||
|           buttonText="Copy HTML" | ||||
|         /> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| ); | ||||
| 
 | ||||
| const QuestionPage: NextPage<Props> = ({ id }) => { | ||||
|   return ( | ||||
|     <Layout page="question"> | ||||
|  | @ -118,7 +113,7 @@ const QuestionPage: NextPage<Props> = ({ id }) => { | |||
|         <Query document={QuestionPageDocument} variables={{ id }}> | ||||
|           {({ data }) => | ||||
|             data.result ? ( | ||||
|               <QuestionScreen question={data.result} /> | ||||
|               <LargeQuestionCard question={data.result} /> | ||||
|             ) : ( | ||||
|               <NextError statusCode={404} /> | ||||
|             ) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user