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 domtoimage from "dom-to-image"; // https://github.com/tsayen/dom-to-image
|
||||||
import { Resizable } from "re-resizable";
|
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";
|
||||||
|
@ -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 {
|
interface Props {
|
||||||
question: QuestionFragment;
|
question: QuestionFragment;
|
||||||
}
|
}
|
||||||
|
@ -121,42 +129,53 @@ export const CaptureQuestion: React.FC<Props> = ({ question }) => {
|
||||||
|
|
||||||
if (imgSrc) {
|
if (imgSrc) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<GrayContainer title="Generated image">
|
||||||
<img src={imgSrc} />
|
<a href={imgSrc} target="_blank">
|
||||||
</div>
|
<img src={imgSrc} />
|
||||||
|
</a>
|
||||||
|
</GrayContainer>
|
||||||
<div>
|
<div>
|
||||||
<ImageSource question={question} imgSrc={imgSrc} />
|
<ImageSource question={question} imgSrc={imgSrc} />
|
||||||
</div>
|
</div>
|
||||||
<div className="justify-self-stretch">
|
{question.platform.id === "metaculus" ? (
|
||||||
<MetaculusEmbed question={question} />
|
<>
|
||||||
</div>
|
<div className="justify-self-stretch">
|
||||||
<div>
|
<MetaculusEmbed question={question} />
|
||||||
<MetaculusSource question={question} />
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
<MetaculusSource question={question} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center space-y-4">
|
<div className="space-y-2 flex flex-col">
|
||||||
<Resizable
|
<GrayContainer title="Resizable preview">
|
||||||
minWidth={320}
|
<Resizable
|
||||||
bounds="parent"
|
minWidth={320}
|
||||||
enable={{ right: true, left: true }}
|
bounds="window"
|
||||||
>
|
enable={{ right: true, left: true }}
|
||||||
<div ref={containerRef}>
|
>
|
||||||
<QuestionCard
|
<div ref={containerRef}>
|
||||||
question={question}
|
<QuestionCard
|
||||||
showTimeStamp={true}
|
container={(children) => (
|
||||||
showExpandButton={false}
|
<div className="px-4 py-3 bg-white">{children}</div>
|
||||||
expandFooterToFullWidth={true}
|
)}
|
||||||
/>
|
question={question}
|
||||||
</div>
|
showTimeStamp={true}
|
||||||
</Resizable>
|
showExpandButton={false}
|
||||||
<div>
|
expandFooterToFullWidth={true}
|
||||||
<Button onClick={onCaptureButtonClick}>{mainButtonText}</Button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Resizable>
|
||||||
|
</GrayContainer>
|
||||||
|
<Button onClick={onCaptureButtonClick} size="small">
|
||||||
|
{mainButtonText}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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";
|
||||||
|
@ -44,12 +43,33 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const Section: React.FC<{ title: string }> = ({ title, children }) => (
|
const Section: React.FC<{ title: string }> = ({ title, children }) => (
|
||||||
<div className="space-y-2 flex flex-col items-start">
|
<div className="space-y-4 flex flex-col items-start">
|
||||||
<h2 className="text-xl text-gray-900">{title}</h2>
|
<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>{children}</div>
|
||||||
</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<{
|
const LargeQuestionCard: React.FC<{
|
||||||
question: QuestionWithHistoryFragment;
|
question: QuestionWithHistoryFragment;
|
||||||
}> = ({ question }) => {
|
}> = ({ question }) => {
|
||||||
|
@ -65,7 +85,7 @@ 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">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
linkTarget="_blank"
|
linkTarget="_blank"
|
||||||
|
@ -74,43 +94,18 @@ const LargeQuestionCard: React.FC<{
|
||||||
{question.description.replaceAll("---", "")}
|
{question.description.replaceAll("---", "")}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</Section>
|
</Section>
|
||||||
<div className="mt-5">
|
<Section title="Indicators">
|
||||||
<Section title="Indicators">
|
<IndicatorsTable question={question} />
|
||||||
<IndicatorsTable question={question} />
|
</Section>
|
||||||
</Section>
|
<Section title="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} />
|
|
||||||
</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 }) => {
|
const QuestionPage: NextPage<Props> = ({ id }) => {
|
||||||
return (
|
return (
|
||||||
<Layout page="question">
|
<Layout page="question">
|
||||||
|
@ -118,7 +113,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} />
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user