feat: capture on question pages, fixes, new frontpage condition
This commit is contained in:
parent
3ae7a68cb2
commit
3b85c32c9d
62
package-lock.json
generated
62
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
`;
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
35
src/web/common/CopyParagraph.tsx
Normal file
35
src/web/common/CopyParagraph.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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"
|
||||
|
|
|
@ -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 }) => (
|
||||
<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"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
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-blue-500 cursor-pointer rounded-md shadow text-white hover:bg-blue-600 active:bg-gray-700 ${padding}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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.
|
|
@ -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,14 +131,16 @@ export const DisplayQuestion: React.FC<Props> = ({
|
|||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<Link href={`/questions/${question.id}`} passHref>
|
||||
<a className="float-right block ml-2 mt-1.5">
|
||||
<FaExpand
|
||||
size="18"
|
||||
className="text-gray-400 hover:text-gray-700"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
{showExpandButton ? (
|
||||
<Link href={`/questions/${question.id}`} passHref>
|
||||
<a className="float-right block ml-2 mt-1.5">
|
||||
<FaExpand
|
||||
size="18"
|
||||
className="text-gray-400 hover:text-gray-700"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
) : null}
|
||||
<Card.Title>
|
||||
<a
|
||||
className="text-black no-underline"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
155
src/web/questions/components/CaptureQuestion.tsx
Normal file
155
src/web/questions/components/CaptureQuestion.tsx
Normal 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.
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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,61 +216,29 @@ 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}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="w-10/12 mb-2">
|
||||
<QueryForm
|
||||
value={queryParameters.query}
|
||||
onChange={onChangeSearchBar}
|
||||
placeholder="Find forecasts about..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{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"
|
||||
onClick={() => showAdvancedOptions(!advancedOptions)}
|
||||
>
|
||||
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}
|
||||
<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"
|
||||
onClick={() => showAdvancedOptions(!advancedOptions)}
|
||||
>
|
||||
Advanced options ▼
|
||||
</button>
|
||||
</div>
|
||||
</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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user