Merge pull request #90 from quantified-uncertainty/embed-question-page-2

Embed question page 2
This commit is contained in:
Vyacheslav Matyukhin 2022-06-01 00:34:53 +03:00 committed by GitHub
commit c051f0dc7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 214 additions and 140 deletions

16
package-lock.json generated
View File

@ -61,6 +61,7 @@
"postcss-preset-env": "^7.3.2", "postcss-preset-env": "^7.3.2",
"prisma": "^3.11.1", "prisma": "^3.11.1",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"re-resizable": "^6.9.9",
"react": "^17.0.2", "react": "^17.0.2",
"react-component-export-image": "^1.0.6", "react-component-export-image": "^1.0.6",
"react-compound-slider": "^3.3.1", "react-compound-slider": "^3.3.1",
@ -37219,6 +37220,15 @@
"rc": "cli.js" "rc": "cli.js"
} }
}, },
"node_modules/re-resizable": {
"version": "6.9.9",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.9.tgz",
"integrity": "sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA==",
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
@ -67696,6 +67706,12 @@
"strip-json-comments": "~2.0.1" "strip-json-comments": "~2.0.1"
} }
}, },
"re-resizable": {
"version": "6.9.9",
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.9.tgz",
"integrity": "sha512-l+MBlKZffv/SicxDySKEEh42hR6m5bAHfNu3Tvxks2c4Ah+ldnWjfnVRwxo/nxF27SsUsxDS0raAzFuJNKABXA==",
"requires": {}
},
"react": { "react": {
"version": "17.0.2", "version": "17.0.2",
"resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",

View File

@ -79,6 +79,7 @@
"postcss-preset-env": "^7.3.2", "postcss-preset-env": "^7.3.2",
"prisma": "^3.11.1", "prisma": "^3.11.1",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"re-resizable": "^6.9.9",
"react": "^17.0.2", "react": "^17.0.2",
"react-component-export-image": "^1.0.6", "react-component-export-image": "^1.0.6",
"react-compound-slider": "^3.3.1", "react-compound-slider": "^3.3.1",

View 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>
);

View File

@ -1,6 +1,6 @@
import domtoimage from "dom-to-image"; // https://github.com/tsayen/dom-to-image 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 { useEffect, useRef, useState } from "react";
import { Button } from "../../common/Button"; import { Button } from "../../common/Button";
import { CopyParagraph } from "../../common/CopyParagraph"; import { CopyParagraph } from "../../common/CopyParagraph";
import { QuestionFragment } from "../../fragments.generated"; import { QuestionFragment } from "../../fragments.generated";
@ -79,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 { interface Props {
question: QuestionFragment; question: QuestionFragment;
} }
@ -118,35 +127,55 @@ export const CaptureQuestion: React.FC<Props> = ({ question }) => {
await exportAsPictureAndCode(); await exportAsPictureAndCode();
}; };
return ( if (imgSrc) {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 place-items-center"> return (
<div ref={containerRef}> <div className="space-y-4">
<QuestionCard <GrayContainer title="Generated image">
question={question} <a href={imgSrc} target="_blank">
showTimeStamp={true}
showExpandButton={false}
expandFooterToFullWidth={true}
/>
</div>
<div>
<Button onClick={onCaptureButtonClick}>{mainButtonText}</Button>
</div>
{imgSrc ? (
<>
<div>
<img src={imgSrc} /> <img src={imgSrc} />
</a>
</GrayContainer>
<div>
<ImageSource question={question} imgSrc={imgSrc} />
</div>
{question.platform.id === "metaculus" ? (
<>
<div className="justify-self-stretch">
<MetaculusEmbed question={question} />
</div>
<div>
<MetaculusSource question={question} />
</div>
</>
) : null}
</div>
);
}
return (
<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> </div>
<div> </Resizable>
<ImageSource question={question} imgSrc={imgSrc} /> </GrayContainer>
</div> <Button onClick={onCaptureButtonClick} size="small">
<div className="justify-self-stretch"> {mainButtonText}
<MetaculusEmbed question={question} /> </Button>
</div>
<div>
<MetaculusSource question={question} />
</div>
</>
) : null}
</div> </div>
); );
}; };

View File

@ -1,16 +1,6 @@
import { FaExternalLinkAlt } from "react-icons/fa"; import { BoxedLink } from "../../common/BoxedLink";
import { QuestionFragment } from "../../fragments.generated"; import { QuestionFragment } from "../../fragments.generated";
export const PlatformLink: React.FC<{ question: QuestionFragment }> = ({ export const PlatformLink: React.FC<{ question: QuestionFragment }> = ({
question, question,
}) => ( }) => <BoxedLink url={question.url}>{question.platform.label}</BoxedLink>;
<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>
);

View File

@ -1,7 +1,7 @@
import Link from "next/link"; import Link from "next/link";
import { ReactElement, ReactNode } from "react";
import { FaExpand } from "react-icons/fa"; import { FaExpand } from "react-icons/fa";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { Card } from "../../../common/Card"; import { Card } from "../../../common/Card";
import { CopyText } from "../../../common/CopyText"; import { CopyText } from "../../../common/CopyText";
import { QuestionFragment } from "../../../fragments.generated"; import { QuestionFragment } from "../../../fragments.generated";
@ -63,6 +63,7 @@ const LastUpdated: React.FC<{ timestamp: Date }> = ({ timestamp }) => (
// Main component // Main component
interface Props { interface Props {
container?: (children: ReactNode) => ReactElement;
question: QuestionFragment; question: QuestionFragment;
showTimeStamp: boolean; showTimeStamp: boolean;
expandFooterToFullWidth: boolean; expandFooterToFullWidth: boolean;
@ -71,6 +72,7 @@ interface Props {
} }
export const QuestionCard: React.FC<Props> = ({ export const QuestionCard: React.FC<Props> = ({
container = (children) => <Card>{children}</Card>,
question, question,
showTimeStamp, showTimeStamp,
expandFooterToFullWidth, expandFooterToFullWidth,
@ -82,72 +84,70 @@ export const QuestionCard: React.FC<Props> = ({
const isBinary = isQuestionBinary(question); const isBinary = isQuestionBinary(question);
return ( return container(
<Card> <div className="h-full flex flex-col space-y-4">
<div className="h-full flex flex-col space-y-4"> <div className="flex-grow space-y-4">
<div className="flex-grow space-y-4"> {showIdToggle ? (
{showIdToggle ? ( <div className="mx-10">
<div className="mx-10"> <CopyText text={question.id} displayText={`[${question.id}]`} />
<CopyText text={question.id} displayText={`[${question.id}]`} /> </div>
</div> ) : null}
) : null} <div>
<div> {showExpandButton ? (
{showExpandButton ? ( <Link href={`/questions/${question.id}`} passHref>
<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" className="text-gray-400 hover:text-gray-700"
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}
</a> </a>
</Card.Title> </Link>
</div> ) : null}
<div className={isBinary ? "flex justify-between" : "space-y-4"}> <Card.Title>
<QuestionOptions question={question} maxNumOptions={5} /> <a
<div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}> className="text-black no-underline"
<LastUpdated timestamp={lastUpdated} /> href={question.url}
</div> target="_blank"
</div> >
{question.title}
{question.platform.id !== "guesstimate" && options.length < 3 && ( </a>
<div className="text-gray-500"> </Card.Title>
<DisplayMarkdown description={question.description} />
</div>
)}
{question.platform.id === "guesstimate" && question.visualization && (
<img
className="rounded-sm"
src={question.visualization}
alt="Guesstimate Screenshot"
/>
)}
</div> </div>
<div <div className={isBinary ? "flex justify-between" : "space-y-4"}>
className={`sm:hidden ${!showTimeStamp ? "hidden" : ""} self-center`} <QuestionOptions question={question} maxNumOptions={5} />
> <div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}>
{/* This one is exclusively for mobile*/} <LastUpdated timestamp={lastUpdated} />
<LastUpdated timestamp={lastUpdated} />
</div>
<div className="w-full">
<div className="mb-2 mt-1">
<QuestionFooter
question={question}
expandFooterToFullWidth={expandFooterToFullWidth}
/>
</div> </div>
</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> </div>
</Card> </div>
); );
}; };

View File

@ -1,11 +1,10 @@
import { GetServerSideProps, NextPage } from "next"; import { GetServerSideProps, NextPage } from "next";
import NextError from "next/error"; import NextError from "next/error";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { BoxedLink } from "../../common/BoxedLink";
import { Card } from "../../common/Card"; import { Card } from "../../common/Card";
import { CopyParagraph } from "../../common/CopyParagraph"; import { CopyParagraph } from "../../common/CopyParagraph";
import { Layout } from "../../common/Layout"; import { Layout } from "../../common/Layout";
import { LineHeader } from "../../common/LineHeader";
import { Query } from "../../common/Query"; import { Query } from "../../common/Query";
import { QuestionWithHistoryFragment } from "../../fragments.generated"; import { QuestionWithHistoryFragment } from "../../fragments.generated";
import { ssrUrql } from "../../urql"; import { ssrUrql } from "../../urql";
@ -43,13 +42,51 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
}; };
}; };
const Section: React.FC<{ title: string }> = ({ title, children }) => ( const Section: React.FC<{ title: string; id?: string }> = ({
<div className="space-y-2 flex flex-col items-start"> title,
<h2 className="text-xl text-gray-900">{title}</h2> children,
id,
}) => (
<div className="space-y-4 flex flex-col items-start" id={id}>
<div className="border-b-2 border-gray-200 w-full group">
<h2 className="text-xl leading-3 text-gray-900">
<span>{title}</span>
{id ? (
<>
{" "}
<a
className="text-gray-300 no-underline hidden group-hover:inline"
href={`#${id}`}
>
#
</a>
</>
) : null}
</h2>
</div>
<div>{children}</div> <div>{children}</div>
</div> </div>
); );
const EmbedSection: React.FC<{ question: QuestionWithHistoryFragment }> = ({
question,
}) => {
const url = getBasePath() + `/questions/embed/${question.id}`;
return (
<Section title="Embed" id="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<{ const LargeQuestionCard: React.FC<{
question: QuestionWithHistoryFragment; question: QuestionWithHistoryFragment;
}> = ({ question }) => { }> = ({ question }) => {
@ -65,8 +102,8 @@ const LargeQuestionCard: React.FC<{
<QuestionChartOrVisualization question={question} /> <QuestionChartOrVisualization question={question} />
</div> </div>
<div className="mx-auto max-w-prose"> <div className="mx-auto max-w-prose space-y-8">
<Section title="Question description"> <Section title="Question description" id="description">
<ReactMarkdown <ReactMarkdown
linkTarget="_blank" linkTarget="_blank"
className="font-normal text-gray-900" className="font-normal text-gray-900"
@ -74,39 +111,17 @@ const LargeQuestionCard: React.FC<{
{question.description.replaceAll("---", "")} {question.description.replaceAll("---", "")}
</ReactMarkdown> </ReactMarkdown>
</Section> </Section>
<div className="mt-5"> <Section title="Indicators" id="indicators">
<Section title="Indicators"> <IndicatorsTable question={question} />
<IndicatorsTable question={question} /> </Section>
</Section> <Section title="Capture" id="capture">
</div> <CaptureQuestion question={question} />
</Section>
<EmbedSection question={question} />
</div> </div>
</Card> </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} />
<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 }) => { const QuestionPage: NextPage<Props> = ({ id }) => {
return ( return (
@ -115,7 +130,7 @@ const QuestionPage: NextPage<Props> = ({ id }) => {
<Query document={QuestionPageDocument} variables={{ id }}> <Query document={QuestionPageDocument} variables={{ id }}>
{({ data }) => {({ data }) =>
data.result ? ( data.result ? (
<QuestionScreen question={data.result} /> <LargeQuestionCard question={data.result} />
) : ( ) : (
<NextError statusCode={404} /> <NextError statusCode={404} />
) )