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

View File

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

View File

@ -1,25 +1,16 @@
import { NextPage } from "next"; import { NextPage } from "next";
import React from "react"; import React from "react";
import { displayQuestionsWrapperForSearch } from "../web/display/displayQuestionsWrappers";
import { Layout } from "../web/display/Layout"; import { Layout } from "../web/display/Layout";
import { Props } from "../web/search/anySearchPage"; import { Props } from "../web/search/anySearchPage";
import CommonDisplay from "../web/search/CommonDisplay"; import { CommonDisplay } from "../web/search/CommonDisplay";
export { getServerSideProps } from "../web/search/anySearchPage"; export { getServerSideProps } from "../web/search/anySearchPage";
const IndexPage: NextPage<Props> = (props) => { const IndexPage: NextPage<Props> = (props) => {
return ( return (
<Layout page="search"> <Layout page="search">
<CommonDisplay <CommonDisplay {...props} />
{...props}
hasSearchbar={true}
hasCapture={false}
hasAdvancedOptions={true}
placeholder={"Find forecasts about..."}
displaySeeMoreHint={true}
displayQuestionsWrapper={displayQuestionsWrapperForSearch}
/>
</Layout> </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; displayText: string;
} }
// https://stackoverflow.com/questions/39501289/in-reactjs-how-to-copy-text-to-clipboard
export const CopyText: React.FC<Props> = ({ text, displayText }) => ( export const CopyText: React.FC<Props> = ({ text, displayText }) => (
<div <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" 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> = ({
<button children,
{...rest} size = "normal",
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" ...rest
> }) => {
{children} const padding = size === "normal" ? "px-5 py-4" : "px-3 py-2";
</button> return (
); <button
{...rest}
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; showTimeStamp: boolean;
expandFooterToFullWidth: boolean; expandFooterToFullWidth: boolean;
showIdToggle?: boolean; showIdToggle?: boolean;
showExpandButton?: boolean;
} }
export const DisplayQuestion: React.FC<Props> = ({ export const DisplayQuestion: React.FC<Props> = ({
@ -111,6 +112,7 @@ export const DisplayQuestion: React.FC<Props> = ({
showTimeStamp, showTimeStamp,
expandFooterToFullWidth, expandFooterToFullWidth,
showIdToggle, showIdToggle,
showExpandButton = true,
}) => { }) => {
const { options } = question; const { options } = question;
const lastUpdated = new Date(question.timestamp * 1000); const lastUpdated = new Date(question.timestamp * 1000);
@ -129,14 +131,16 @@ export const DisplayQuestion: React.FC<Props> = ({
</div> </div>
) : null} ) : null}
<div> <div>
<Link href={`/questions/${question.id}`} passHref> {showExpandButton ? (
<a className="float-right block ml-2 mt-1.5"> <Link href={`/questions/${question.id}`} passHref>
<FaExpand <a className="float-right block ml-2 mt-1.5">
size="18" <FaExpand
className="text-gray-400 hover:text-gray-700" size="18"
/> className="text-gray-400 hover:text-gray-700"
</a> />
</Link> </a>
</Link>
) : null}
<Card.Title> <Card.Title>
<a <a
className="text-black no-underline" 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"]; 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 }) => { const getVictoryGroup = ({ data, i }: { data: DataSet; i: number }) => {
return ( return (
<VictoryGroup color={colors[i] || "darkgray"} data={dataAsXy(data)} key={i}> <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 { Query } from "../../common/Query";
import { Card } from "../../display/Card"; import { Card } from "../../display/Card";
import { DisplayOneQuestionForCapture } from "../../display/DisplayOneQuestionForCapture";
import { Layout } from "../../display/Layout"; import { Layout } from "../../display/Layout";
import { LineHeader } from "../../display/LineHeader"; import { LineHeader } from "../../display/LineHeader";
import { QuestionWithHistoryFragment } from "../../fragments.generated"; import { QuestionWithHistoryFragment } from "../../fragments.generated";
import { ssrUrql } from "../../urql"; import { ssrUrql } from "../../urql";
import { CaptureQuestion } from "../components/CaptureQuestion";
import { HistoryChart } from "../components/HistoryChart"; import { HistoryChart } from "../components/HistoryChart";
import { IndicatorsTable } from "../components/IndicatorsTable"; import { IndicatorsTable } from "../components/IndicatorsTable";
import { QuestionPageDocument } from "../queries.generated"; import { QuestionPageDocument } from "../queries.generated";
@ -93,7 +93,7 @@ const QuestionPage: NextPage<Props> = ({ id }) => {
<LineHeader> <LineHeader>
<h1>Capture</h1> <h1>Capture</h1>
</LineHeader> </LineHeader>
<DisplayOneQuestionForCapture result={data.result} /> <CaptureQuestion question={data.result} />
</div> </div>
</div> </div>
)} )}

View File

@ -3,6 +3,7 @@ import React, { Fragment, useMemo, useState } from "react";
import { useQuery } from "urql"; import { useQuery } from "urql";
import { ButtonsForStars } from "../display/ButtonsForStars"; import { ButtonsForStars } from "../display/ButtonsForStars";
import { DisplayQuestions } from "../display/DisplayQuestions";
import { MultiSelectPlatform } from "../display/MultiSelectPlatform"; import { MultiSelectPlatform } from "../display/MultiSelectPlatform";
import { QueryForm } from "../display/QueryForm"; import { QueryForm } from "../display/QueryForm";
import { SliderElement } from "../display/SliderElement"; import { SliderElement } from "../display/SliderElement";
@ -11,34 +12,16 @@ import { useIsFirstRender, useNoInitialEffect } from "../hooks";
import { Props as AnySearchPageProps, QueryParameters } from "./anySearchPage"; import { Props as AnySearchPageProps, QueryParameters } from "./anySearchPage";
import { SearchDocument } from "./queries.generated"; import { SearchDocument } from "./queries.generated";
interface Props extends AnySearchPageProps { 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;
}
/* Body */ /* Body */
const CommonDisplay: React.FC<Props> = ({ export const CommonDisplay: React.FC<Props> = ({
defaultResults, defaultResults,
initialQueryParameters, initialQueryParameters,
defaultQueryParameters, defaultQueryParameters,
initialNumDisplay, initialNumDisplay,
defaultNumDisplay, defaultNumDisplay,
platformsConfig, platformsConfig,
hasSearchbar,
hasCapture,
hasAdvancedOptions,
placeholder,
displaySeeMoreHint,
displayQuestionsWrapper,
}) => { }) => {
/* States */ /* States */
const router = useRouter(); const router = useRouter();
@ -119,12 +102,15 @@ const CommonDisplay: React.FC<Props> = ({
numDisplay % 3 != 0 numDisplay % 3 != 0
? numDisplay + (3 - (Math.round(numDisplay) % 3)) ? numDisplay + (3 - (Math.round(numDisplay) % 3))
: numDisplay; : numDisplay;
return displayQuestionsWrapper({ return (
results, <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
numDisplay: numDisplayRounded, <DisplayQuestions
whichResultToDisplayAndCapture, results={results}
showIdToggle, numDisplay={numDisplayRounded}
}); showIdToggle={showIdToggle}
/>
</div>
);
}; };
const updateRoute = () => { const updateRoute = () => {
@ -230,61 +216,29 @@ const CommonDisplay: React.FC<Props> = ({
setShowIdToggle(!showIdToggle); 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 */ /* Final return */
return ( return (
<Fragment> <Fragment>
<label className="mb-4 mt-4 flex flex-row justify-center items-center"> <label className="mb-4 mt-4 flex flex-row justify-center items-center">
{hasSearchbar ? ( <div className="w-10/12 mb-2">
<div className="w-10/12 mb-2"> <QueryForm
<QueryForm value={queryParameters.query}
value={queryParameters.query} onChange={onChangeSearchBar}
onChange={onChangeSearchBar} placeholder="Find forecasts about..."
placeholder={placeholder} />
/> </div>
</div>
) : null}
{hasAdvancedOptions ? ( <div className="w-2/12 flex justify-center ml-4 md:ml-2 lg:ml-0">
<div className="w-2/12 flex justify-center ml-4 md:ml-2 lg:ml-0"> <button
<button className="text-gray-500 text-sm mb-2"
className="text-gray-500 text-sm mb-2" onClick={() => showAdvancedOptions(!advancedOptions)}
onClick={() => showAdvancedOptions(!advancedOptions)} >
> Advanced options
Advanced options </button>
</button> </div>
</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> </label>
{hasAdvancedOptions && advancedOptions ? ( {advancedOptions ? (
<div className="flex-1 flex-col mx-auto justify-center items-center w-full"> <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="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"> <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> <div>{getInfoToDisplayQuestionsFunction()}</div>
{displaySeeMoreHint && {!results || (results.length !== 0 && numDisplay < results.length) ? (
(!results || (results.length !== 0 && numDisplay < results.length)) ? (
<div> <div>
<p className="mt-4 mb-4"> <p className="mt-4 mb-4">
{"Can't find what you were looking for?"} {"Can't find what you were looking for?"}
@ -357,5 +310,3 @@ const CommonDisplay: React.FC<Props> = ({
</Fragment> </Fragment>
); );
}; };
export default CommonDisplay;

View File

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

View File

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