feat: capture on question pages, fixes, new frontpage condition

This commit is contained in:
Vyacheslav Matyukhin 2022-05-04 01:32:14 +04:00
parent 3ae7a68cb2
commit 3b85c32c9d
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
16 changed files with 328 additions and 403 deletions

62
package-lock.json generated
View File

@ -16,15 +16,18 @@
"@prisma/client": "^3.11.1",
"@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.1",
"@types/dom-to-image": "^2.6.4",
"@types/jsdom": "^16.2.14",
"@types/nprogress": "^0.2.0",
"@types/react": "^17.0.39",
"@types/react-copy-to-clipboard": "^5.0.2",
"airtable": "^0.11.1",
"algoliasearch": "^4.10.3",
"autoprefixer": "^10.1.0",
"axios": "^0.25.0",
"chroma-js": "^2.4.2",
"critters": "^0.0.16",
"date-fns": "^2.28.0",
"dom-to-image": "^2.6.0",
"dotenv": "^16.0.0",
"fetch": "^1.1.0",
@ -3157,6 +3160,11 @@
"@types/ms": "*"
}
},
"node_modules/@types/dom-to-image": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.4.tgz",
"integrity": "sha512-UddUdGF1qulrSDulkz3K2Ypq527MR6ixlgAzqLbxSiQ0icx0XDlIV+h4+edmjq/1dqn0KgN0xGSe1kI9t+vGuw=="
},
"node_modules/@types/hast": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@types%2fhast/-/hast-2.3.4.tgz",
@ -3288,6 +3296,14 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-copy-to-clipboard": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz",
"integrity": "sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types%2freact-transition-group/-/react-transition-group-4.4.4.tgz",
@ -5250,10 +5266,16 @@
"dev": true
},
"node_modules/date-fns": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
"integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
"dev": true
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debounce": {
"version": "1.2.1",
@ -8055,6 +8077,12 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"node_modules/listr-verbose-renderer/node_modules/date-fns": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
"integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
"dev": true
},
"node_modules/listr-verbose-renderer/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@ -42331,6 +42359,11 @@
"@types/ms": "*"
}
},
"@types/dom-to-image": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.4.tgz",
"integrity": "sha512-UddUdGF1qulrSDulkz3K2Ypq527MR6ixlgAzqLbxSiQ0icx0XDlIV+h4+edmjq/1dqn0KgN0xGSe1kI9t+vGuw=="
},
"@types/hast": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@types%2fhast/-/hast-2.3.4.tgz",
@ -42453,6 +42486,14 @@
"csstype": "^3.0.2"
}
},
"@types/react-copy-to-clipboard": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz",
"integrity": "sha512-O29AThfxrkUFRsZXjfSWR2yaWo0ppB1yLEnHA+Oh24oNetjBAwTDu1PmolIqdJKzsZiO4J1jn6R6TmO96uBvGg==",
"requires": {
"@types/react": "*"
}
},
"@types/react-transition-group": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types%2freact-transition-group/-/react-transition-group-4.4.4.tgz",
@ -43924,10 +43965,9 @@
"dev": true
},
"date-fns": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
"integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
"dev": true
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
},
"debounce": {
"version": "1.2.1",
@ -45991,6 +46031,12 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"date-fns": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz",
"integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==",
"dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",

View File

@ -34,15 +34,18 @@
"@prisma/client": "^3.11.1",
"@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.1",
"@types/dom-to-image": "^2.6.4",
"@types/jsdom": "^16.2.14",
"@types/nprogress": "^0.2.0",
"@types/react": "^17.0.39",
"@types/react-copy-to-clipboard": "^5.0.2",
"airtable": "^0.11.1",
"algoliasearch": "^4.10.3",
"autoprefixer": "^10.1.0",
"axios": "^0.25.0",
"chroma-js": "^2.4.2",
"critters": "^0.0.16",
"date-fns": "^2.28.0",
"dom-to-image": "^2.6.0",
"dotenv": "^16.0.0",
"fetch": "^1.1.0",

View File

@ -19,11 +19,14 @@ export async function getFrontpage(): Promise<Question[]> {
export async function rebuildFrontpage() {
await measureTime(async () => {
const rows = await prisma.$queryRaw<{ id: string }[]>`
SELECT id FROM questions
SELECT questions.id FROM questions, history
WHERE
(qualityindicators->>'stars')::int >= 3
AND description != ''
AND JSONB_ARRAY_LENGTH(options) > 0
questions.id = history.id
AND (questions.qualityindicators->>'stars')::int >= 3
AND questions.description != ''
AND JSONB_ARRAY_LENGTH(questions.options) > 0
GROUP BY questions.id
HAVING COUNT(DISTINCT history.timestamp) >= 7
ORDER BY RANDOM() LIMIT 50
`;

View File

@ -1,25 +1,16 @@
import { NextPage } from "next";
import React from "react";
import { displayQuestionsWrapperForSearch } from "../web/display/displayQuestionsWrappers";
import { Layout } from "../web/display/Layout";
import { Props } from "../web/search/anySearchPage";
import CommonDisplay from "../web/search/CommonDisplay";
import { CommonDisplay } from "../web/search/CommonDisplay";
export { getServerSideProps } from "../web/search/anySearchPage";
const IndexPage: NextPage<Props> = (props) => {
return (
<Layout page="search">
<CommonDisplay
{...props}
hasSearchbar={true}
hasCapture={false}
hasAdvancedOptions={true}
placeholder={"Find forecasts about..."}
displaySeeMoreHint={true}
displayQuestionsWrapper={displayQuestionsWrapperForSearch}
/>
<CommonDisplay {...props} />
</Layout>
);
};

View File

@ -0,0 +1,35 @@
import { useState } from "react";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { Button } from "../display/Button";
// https://stackoverflow.com/questions/39501289/in-reactjs-how-to-copy-text-to-clipboard
export const CopyParagraph: React.FC<{ text: string; buttonText: string }> = ({
text,
buttonText: initialButtonText,
}) => {
const [buttonText, setButtonText] = useState(initialButtonText);
const handleButton = () => {
setButtonText("Copied");
setTimeout(async () => {
setButtonText(initialButtonText);
}, 2000);
};
return (
<div className="flex flex-col items-stretch">
<p
className="bg-gray-100 cursor-pointer px-3 py-2 rounded-md shadow text-gray-700 font-mono text-sm"
onClick={(e) => {
e.preventDefault();
navigator.clipboard.writeText(text);
}}
>
{text}
</p>
<CopyToClipboard text={text} onCopy={handleButton}>
<Button size="small">{buttonText}</Button>
</CopyToClipboard>
</div>
);
};

View File

@ -5,6 +5,8 @@ interface Props {
displayText: string;
}
// https://stackoverflow.com/questions/39501289/in-reactjs-how-to-copy-text-to-clipboard
export const CopyText: React.FC<Props> = ({ text, displayText }) => (
<div
className="flex items-center justify-center p-4 space-x-3 border rounded border-blue-400 hover:border-transparent bg-transparent hover:bg-blue-300 text-sm font-medium text-blue-400 hover:text-white cursor-pointer"

View File

@ -1,10 +1,19 @@
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
size?: "small" | "normal";
}
export const Button: React.FC<Props> = ({ children, ...rest }) => (
export const Button: React.FC<Props> = ({
children,
size = "normal",
...rest
}) => {
const padding = size === "normal" ? "px-5 py-4" : "px-3 py-2";
return (
<button
{...rest}
className="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded text-center"
className={`bg-blue-500 cursor-pointer rounded-md shadow text-white hover:bg-blue-600 active:bg-gray-700 ${padding}`}
>
{children}
</button>
);
};

View File

@ -1,253 +0,0 @@
import domtoimage from "dom-to-image"; // https://github.com/tsayen/dom-to-image
import { useEffect, useRef, useState } from "react";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { QuestionFragment } from "../fragments.generated";
import { uploadToImgur } from "../worker/uploadToImgur";
import { DisplayQuestion } from "./DisplayQuestion";
function displayOneQuestionInner(result: QuestionFragment, containerRef) {
return (
<div ref={containerRef}>
{result ? (
<DisplayQuestion
question={result}
showTimeStamp={true}
expandFooterToFullWidth={true}
/>
) : null}
</div>
);
}
let domToImageWrapper = (reactRef) => {
let node = reactRef.current;
const scale = 3; // Increase for better quality
const style = {
transform: "scale(" + scale + ")",
transformOrigin: "top left",
width: node.offsetWidth + "px",
height: node.offsetHeight + "px",
};
const param = {
height: node.offsetHeight * scale,
width: node.offsetWidth * scale,
quality: 1,
style,
};
let image = domtoimage.toPng(node, param);
return image;
};
let generateHtml = (result, imgSrc) => {
let html = `<a href="${result.url} target="_blank""><img src="${imgSrc}" alt="Metaforecast.org snapshot of ''${result.title}'', from ${result.platform}"></a>`;
return html;
};
let generateMarkdown = (result, imgSrc) => {
let markdown = `[![](${imgSrc})](${result.url})`;
return markdown;
};
let generateSource = (result, imgSrc, hasDisplayBeenCaptured) => {
const [htmlButtonStatus, setHtmlButtonStatus] = useState("Copy HTML");
const [markdownButtonStatus, setMarkdownButtonStatus] =
useState("Copy markdown");
let handleHtmlButton = () => {
setHtmlButtonStatus("Copied");
let newtimeoutId = setTimeout(async () => {
setHtmlButtonStatus("Copy HTML");
}, 2000);
};
let handleMarkdownButton = () => {
setMarkdownButtonStatus("Copied");
let newtimeoutId = setTimeout(async () => {
setMarkdownButtonStatus("Copy markdown");
}, 2000);
};
if (result && imgSrc && hasDisplayBeenCaptured) {
return (
<div className="grid">
<p className="bg-gray-100 cursor-pointer px-3 py-2 rounded-md shadow text-grey-7000 font-mono text-sm">
{generateMarkdown(result, imgSrc)}
</p>
<CopyToClipboard
text={generateMarkdown(result, imgSrc)}
onCopy={() => handleMarkdownButton()}
>
<button className="bg-blue-500 cursor-pointer px-3 py-2 rounded-md shadow text-white hover:bg-blue-600 active:scale-120">
{markdownButtonStatus}
</button>
</CopyToClipboard>
<p className="bg-gray-100 cursor-pointer px-3 py-2 rounded-md shadow text-grey-7000 font-mono text-sm">
{generateHtml(result, imgSrc)}
</p>
<CopyToClipboard
text={generateHtml(result, imgSrc)}
onCopy={() => handleHtmlButton()}
>
<button className="bg-blue-500 cursor-pointer px-3 py-2 rounded-md shadow text-white mb-4 hover:bg-blue-600">
{htmlButtonStatus}
</button>
</CopyToClipboard>
</div>
);
} else {
return null;
}
};
let generateIframeURL = (result) => {
let iframeURL = "";
if (result) {
// if check not strictly necessary today
let parts = result.url.replace("questions", "questions/embed").split("/");
parts.pop();
parts.pop();
iframeURL = parts.join("/");
}
return iframeURL;
};
let metaculusEmbed = (result) => {
let platform = "";
let iframeURL = "";
if (result) {
iframeURL = generateIframeURL(result);
platform = result.platform;
}
return (
<iframe
className={`${
platform == "Metaculus" ? "" : "hidden"
} flex h-80 w-full justify-self-center self-center`}
src={iframeURL}
/>
);
};
let generateMetaculusIframeHTML = (result) => {
if (result) {
let iframeURL = generateIframeURL(result);
return `<iframe src="${iframeURL}" height="400" width="600"/>`;
} else {
return null;
}
};
let generateMetaculusSource = (result, hasDisplayBeenCaptured) => {
const [htmlButtonStatus, setHtmlButtonStatus] = useState("Copy HTML");
let handleHtmlButton = () => {
setHtmlButtonStatus("Copied");
let newtimeoutId = setTimeout(async () => {
setHtmlButtonStatus("Copy HTML");
}, 2000);
};
if (result && hasDisplayBeenCaptured && result.platform == "Metaculus") {
return (
<div className="grid">
<p className="bg-gray-100 cursor-pointer px-3 py-2 rounded-md shadow text-grey-7000 font-mono text-sm">
{generateMetaculusIframeHTML(result)}
</p>
<CopyToClipboard
text={generateMetaculusIframeHTML(result)}
onCopy={() => handleHtmlButton()}
>
<button className="bg-blue-500 cursor-pointer px-3 py-2 rounded-md shadow text-white mb-4 hover:bg-blue-600">
{htmlButtonStatus}
</button>
</CopyToClipboard>
</div>
);
} else {
return null;
}
};
interface Props {
result: QuestionFragment;
}
export const DisplayOneQuestionForCapture: React.FC<Props> = ({ result }) => {
const [hasDisplayBeenCaptured, setHasDisplayBeenCaptured] = useState(false);
useEffect(() => {
setHasDisplayBeenCaptured(false);
}, [result]);
const containerRef = useRef(null);
const [imgSrc, setImgSrc] = useState("");
const [mainButtonStatus, setMainButtonStatus] = useState(
"Capture image and generate code"
);
let exportAsPictureAndCode = () => {
let handleGettingImgurlImage = (imgurUrl) => {
setImgSrc(imgurUrl);
setMainButtonStatus("Done!");
let newtimeoutId = setTimeout(async () => {
setMainButtonStatus("Capture image and generate code");
}, 2000);
};
domToImageWrapper(containerRef)
.then(async function (dataUrl) {
if (dataUrl) {
uploadToImgur(dataUrl, handleGettingImgurlImage);
}
})
.catch(function (error) {
console.error("oops, something went wrong!", error);
});
}; //
let onCaptureButtonClick = () => {
exportAsPictureAndCode();
setMainButtonStatus("Processing...");
setHasDisplayBeenCaptured(true);
setImgSrc("");
};
function generateCaptureButton(result, onCaptureButtonClick) {
if (result) {
return (
<button
onClick={() => onCaptureButtonClick()}
className="bg-blue-500 cursor-pointer px-5 py-4 rounded-md shadow text-white hover:bg-blue-600 active:bg-gray-700"
>
{mainButtonStatus}
</button>
);
}
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-center">
<div className="flex col-span-1 items-center justify-center">
{displayOneQuestionInner(result, containerRef)}
</div>
<div className="flex col-span-1 items-center justify-center">
{generateCaptureButton(result, onCaptureButtonClick)}
</div>
<div className="flex col-span-1 items-center justify-center">
<img src={imgSrc} className={hasDisplayBeenCaptured ? "" : "hidden"} />
</div>
<div className="flex col-span-1 items-center justify-center">
<div>{generateSource(result, imgSrc, hasDisplayBeenCaptured)}</div>
</div>
<div className="flex col-span-1 items-center justify-center mb-8">
{metaculusEmbed(result)}
</div>
<div className="flex col-span-1 items-center justify-center">
<div>{generateMetaculusSource(result, hasDisplayBeenCaptured)}</div>
</div>
<br></br>
</div>
);
};
// https://stackoverflow.com/questions/39501289/in-reactjs-how-to-copy-text-to-clipboard
// Note: https://stackoverflow.com/questions/66016033/can-no-longer-upload-images-to-imgur-from-localhost
// Use: http://imgurtester:3000/embed for testing.

View File

@ -104,6 +104,7 @@ interface Props {
showTimeStamp: boolean;
expandFooterToFullWidth: boolean;
showIdToggle?: boolean;
showExpandButton?: boolean;
}
export const DisplayQuestion: React.FC<Props> = ({
@ -111,6 +112,7 @@ export const DisplayQuestion: React.FC<Props> = ({
showTimeStamp,
expandFooterToFullWidth,
showIdToggle,
showExpandButton = true,
}) => {
const { options } = question;
const lastUpdated = new Date(question.timestamp * 1000);
@ -129,6 +131,7 @@ export const DisplayQuestion: React.FC<Props> = ({
</div>
) : null}
<div>
{showExpandButton ? (
<Link href={`/questions/${question.id}`} passHref>
<a className="float-right block ml-2 mt-1.5">
<FaExpand
@ -137,6 +140,7 @@ export const DisplayQuestion: React.FC<Props> = ({
/>
</a>
</Link>
) : null}
<Card.Title>
<a
className="text-black no-underline"

View File

@ -1,17 +0,0 @@
import { DisplayQuestions } from "./DisplayQuestions";
export function displayQuestionsWrapperForSearch({
results,
numDisplay,
showIdToggle,
}) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<DisplayQuestions
results={results || []}
numDisplay={numDisplay}
showIdToggle={showIdToggle}
/>
</div>
);
}

View File

@ -0,0 +1,155 @@
import domtoimage from "dom-to-image"; // https://github.com/tsayen/dom-to-image
import { useEffect, useRef, useState } from "react";
import { CopyParagraph } from "../../common/CopyParagraph";
import { Button } from "../../display/Button";
import { DisplayQuestion } from "../../display/DisplayQuestion";
import { QuestionFragment } from "../../fragments.generated";
import { uploadToImgur } from "../../worker/uploadToImgur";
const domToImageWrapper = async (node: HTMLDivElement) => {
const scale = 3; // Increase for better quality
const style = {
transform: "scale(" + scale + ")",
transformOrigin: "top left",
width: node.offsetWidth + "px",
height: node.offsetHeight + "px",
};
const param = {
height: node.offsetHeight * scale,
width: node.offsetWidth * scale,
quality: 1,
style,
};
const image = await domtoimage.toPng(node, param);
return image;
};
const ImageSource: React.FC<{ question: QuestionFragment; imgSrc: string }> = ({
question,
imgSrc,
}) => {
if (!imgSrc) {
return null;
}
const html = `<a href="${question.url}" target="_blank"><img src="${imgSrc}" alt="Metaforecast.org snapshot of ''${question.title}'', from ${question.platform.label}"></a>`;
const markdown = `[![](${imgSrc})](${question.url})`;
return (
<div className="space-y-4">
<CopyParagraph text={markdown} buttonText="Copy markdown" />
<CopyParagraph text={html} buttonText="Copy HTML" />
</div>
);
};
const generateMetaculusIframeURL = (question: QuestionFragment) => {
let parts = question.url.replace("questions", "questions/embed").split("/");
parts.pop();
parts.pop();
const iframeURL = parts.join("/");
return iframeURL;
};
const generateMetaculusIframeHTML = (question: QuestionFragment) => {
const iframeURL = generateMetaculusIframeURL(question);
return `<iframe src="${iframeURL}" height="400" width="600"/>`;
};
const MetaculusEmbed: React.FC<{ question: QuestionFragment }> = ({
question,
}) => {
if (question.platform.id !== "metaculus") return null;
const iframeURL = generateMetaculusIframeURL(question);
return <iframe className="w-full h-80" src={iframeURL} />;
};
const MetaculusSource: React.FC<{
question: QuestionFragment;
}> = ({ question }) => {
if (question.platform.id !== "metaculus") return null;
return (
<CopyParagraph
text={generateMetaculusIframeHTML(question)}
buttonText="Copy HTML"
/>
);
};
interface Props {
question: QuestionFragment;
}
export const CaptureQuestion: React.FC<Props> = ({ question }) => {
const [imgSrc, setImgSrc] = useState<string | null>(null);
useEffect(() => {
setImgSrc(null);
}, [question]);
const containerRef = useRef<HTMLDivElement | null>(null);
const initialMainButtonText = "Capture image and generate code";
const [mainButtonText, setMainButtonText] = useState(initialMainButtonText);
const exportAsPictureAndCode = async () => {
if (!containerRef.current) {
return;
}
try {
const dataUrl = await domToImageWrapper(containerRef.current);
const imgurUrl = await uploadToImgur(dataUrl);
setImgSrc(imgurUrl);
setMainButtonText("Done!");
setTimeout(async () => {
setMainButtonText(initialMainButtonText);
}, 2000);
} catch (error) {
console.error("oops, something went wrong!", error);
}
};
const onCaptureButtonClick = async () => {
setMainButtonText("Processing...");
setImgSrc(null);
await exportAsPictureAndCode();
};
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 place-items-center">
<div ref={containerRef}>
<DisplayQuestion
question={question}
showTimeStamp={true}
showExpandButton={false}
expandFooterToFullWidth={true}
/>
</div>
<div>
<Button onClick={onCaptureButtonClick}>{mainButtonText}</Button>
</div>
{imgSrc ? (
<>
<div>
<img src={imgSrc} />
</div>
<div>
<ImageSource question={question} imgSrc={imgSrc} />
</div>
<div className="justify-self-stretch">
<MetaculusEmbed question={question} />
</div>
<div>
<MetaculusSource question={question} />
</div>
</>
) : null}
</div>
);
};
// Note: https://stackoverflow.com/questions/66016033/can-no-longer-upload-images-to-imgur-from-localhost
// Use: http://imgurtester:3000/embed for testing.

View File

@ -41,7 +41,7 @@ const dataAsXy = (data: DataSet) =>
}));
const colors = ["dodgerblue", "crimson", "seagreen", "darkviolet", "turquoise"];
// can't be replaced with React component, VictoryChar requires VictoryGroup elements to be immediate children
// can't be replaced with React component, VictoryChart requires VictoryGroup elements to be immediate children
const getVictoryGroup = ({ data, i }: { data: DataSet; i: number }) => {
return (
<VictoryGroup color={colors[i] || "darkgray"} data={dataAsXy(data)} key={i}>

View File

@ -4,11 +4,11 @@ import ReactMarkdown from "react-markdown";
import { Query } from "../../common/Query";
import { Card } from "../../display/Card";
import { DisplayOneQuestionForCapture } from "../../display/DisplayOneQuestionForCapture";
import { Layout } from "../../display/Layout";
import { LineHeader } from "../../display/LineHeader";
import { QuestionWithHistoryFragment } from "../../fragments.generated";
import { ssrUrql } from "../../urql";
import { CaptureQuestion } from "../components/CaptureQuestion";
import { HistoryChart } from "../components/HistoryChart";
import { IndicatorsTable } from "../components/IndicatorsTable";
import { QuestionPageDocument } from "../queries.generated";
@ -93,7 +93,7 @@ const QuestionPage: NextPage<Props> = ({ id }) => {
<LineHeader>
<h1>Capture</h1>
</LineHeader>
<DisplayOneQuestionForCapture result={data.result} />
<CaptureQuestion question={data.result} />
</div>
</div>
)}

View File

@ -3,6 +3,7 @@ import React, { Fragment, useMemo, useState } from "react";
import { useQuery } from "urql";
import { ButtonsForStars } from "../display/ButtonsForStars";
import { DisplayQuestions } from "../display/DisplayQuestions";
import { MultiSelectPlatform } from "../display/MultiSelectPlatform";
import { QueryForm } from "../display/QueryForm";
import { SliderElement } from "../display/SliderElement";
@ -11,34 +12,16 @@ import { useIsFirstRender, useNoInitialEffect } from "../hooks";
import { Props as AnySearchPageProps, QueryParameters } from "./anySearchPage";
import { SearchDocument } from "./queries.generated";
interface Props extends AnySearchPageProps {
hasSearchbar: boolean;
hasCapture: boolean;
hasAdvancedOptions: boolean;
placeholder: string;
displaySeeMoreHint: boolean;
displayQuestionsWrapper: (opts: {
results: QuestionFragment[];
numDisplay: number;
whichResultToDisplayAndCapture: number;
showIdToggle: boolean;
}) => React.ReactNode;
}
interface Props extends AnySearchPageProps {}
/* Body */
const CommonDisplay: React.FC<Props> = ({
export const CommonDisplay: React.FC<Props> = ({
defaultResults,
initialQueryParameters,
defaultQueryParameters,
initialNumDisplay,
defaultNumDisplay,
platformsConfig,
hasSearchbar,
hasCapture,
hasAdvancedOptions,
placeholder,
displaySeeMoreHint,
displayQuestionsWrapper,
}) => {
/* States */
const router = useRouter();
@ -119,12 +102,15 @@ const CommonDisplay: React.FC<Props> = ({
numDisplay % 3 != 0
? numDisplay + (3 - (Math.round(numDisplay) % 3))
: numDisplay;
return displayQuestionsWrapper({
results,
numDisplay: numDisplayRounded,
whichResultToDisplayAndCapture,
showIdToggle,
});
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<DisplayQuestions
results={results}
numDisplay={numDisplayRounded}
showIdToggle={showIdToggle}
/>
</div>
);
};
const updateRoute = () => {
@ -230,32 +216,18 @@ const CommonDisplay: React.FC<Props> = ({
setShowIdToggle(!showIdToggle);
};
// Capture functionality
const onClickBack = () => {
const decreaseUntil0 = (num: number) => (num - 1 > 0 ? num - 1 : 0);
setWhichResultToDisplayAndCapture(
decreaseUntil0(whichResultToDisplayAndCapture)
);
};
const onClickForward = (whichResultToDisplayAndCapture: number) => {
setWhichResultToDisplayAndCapture(whichResultToDisplayAndCapture + 1);
};
/* Final return */
return (
<Fragment>
<label className="mb-4 mt-4 flex flex-row justify-center items-center">
{hasSearchbar ? (
<div className="w-10/12 mb-2">
<QueryForm
value={queryParameters.query}
onChange={onChangeSearchBar}
placeholder={placeholder}
placeholder="Find forecasts about..."
/>
</div>
) : null}
{hasAdvancedOptions ? (
<div className="w-2/12 flex justify-center ml-4 md:ml-2 lg:ml-0">
<button
className="text-gray-500 text-sm mb-2"
@ -264,27 +236,9 @@ const CommonDisplay: React.FC<Props> = ({
Advanced options
</button>
</div>
) : null}
{hasCapture ? (
<div className="w-2/12 flex justify-center ml-4 md:ml-2 gap-1 lg:ml-0">
<button
className="text-blue-500 cursor-pointer text-xl mb-3 pr-3 hover:text-blue-600"
onClick={() => onClickBack()}
>
</button>
<button
className="text-blue-500 cursor-pointer text-xl mb-3 pl-3 hover:text-blue-600"
onClick={() => onClickForward(whichResultToDisplayAndCapture)}
>
</button>
</div>
) : null}
</label>
{hasAdvancedOptions && advancedOptions ? (
{advancedOptions ? (
<div className="flex-1 flex-col mx-auto justify-center items-center w-full">
<div className="grid sm:grid-rows-4 sm:grid-cols-1 md:grid-rows-2 lg:grid-rows-2 grid-cols-1 md:grid-cols-3 lg:grid-cols-3 items-center content-center bg-gray-50 rounded-md px-8 pt-4 pb-1 shadow mb-4">
<div className="flex row-start-1 row-end-1 col-start-1 col-end-4 md:row-span-1 md:col-start-1 md:col-end-1 md:row-start-1 md:row-end-1 lg:row-span-1 lg:col-start-1 lg:col-end-1 lg:row-start-1 lg:row-end-1 items-center justify-center mb-4">
@ -326,8 +280,7 @@ const CommonDisplay: React.FC<Props> = ({
<div>{getInfoToDisplayQuestionsFunction()}</div>
{displaySeeMoreHint &&
(!results || (results.length !== 0 && numDisplay < results.length)) ? (
{!results || (results.length !== 0 && numDisplay < results.length) ? (
<div>
<p className="mt-4 mb-4">
{"Can't find what you were looking for?"}
@ -357,5 +310,3 @@ const CommonDisplay: React.FC<Props> = ({
</Fragment>
);
};
export default CommonDisplay;

View File

@ -5,7 +5,7 @@ import { QuestionFragment } from "../fragments.generated";
import { ssrUrql } from "../urql";
import { FrontpageDocument, SearchDocument } from "./queries.generated";
/* Common code for / and /capture */
/* Common code for / and /capture (/capture is deprecated, TODO - refactor) */
export interface QueryParameters {
query: string;

View File

@ -1,8 +1,7 @@
// import fetch from "fetch"
import axios, { AxiosRequestConfig } from "axios";
export async function uploadToImgur(dataURL, handleGettingImgurlImage) {
let request: AxiosRequestConfig = {
export async function uploadToImgur(dataURL: string): Promise<string> {
const request: AxiosRequestConfig = {
method: "post",
url: "https://api.imgur.com/3/image",
headers: {
@ -12,18 +11,15 @@ export async function uploadToImgur(dataURL, handleGettingImgurlImage) {
type: "base64",
image: dataURL.split(",")[1],
},
// redirect: "follow",
};
let url;
let url = "https://i.imgur.com/qcThRRz.gif"; // Error image
try {
let response = await axios(request).then((response) => response.data);
// console.log(dataURL)
// console.log(response)
const response = await axios(request).then((response) => response.data);
url = `https://i.imgur.com/${response.data.id}.png`;
} catch (error) {
console.log("error", error);
}
let errorImageURL = "https://i.imgur.com/qcThRRz.gif"; // Error image
url = url || errorImageURL;
handleGettingImgurlImage(url);
return url;
}