Merge pull request #53 from quantified-uncertainty/forecast-html-css
Frontend refactorings & dashboard features
This commit is contained in:
commit
e320d50276
29
docs/coding-style.md
Normal file
29
docs/coding-style.md
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# TypeScript
|
||||||
|
|
||||||
|
- avoid `any`; get rid of any existing `any` whenever you can so that we can enable `"strict": true` later on in `tsconfig.json`
|
||||||
|
- define custom types for common data structures
|
||||||
|
- don't worry about `interface` vs `type`, [both are fine](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces)
|
||||||
|
|
||||||
|
## Typescript and React/Next
|
||||||
|
|
||||||
|
- use `React.FC<Props>` type for React components, e.g. `const MyComponent: React.FC<Props> = ({ ... }) => { ... };`
|
||||||
|
- use `NextPage<Props>` for typing stuff in `src/pages/`
|
||||||
|
- use generic versions of `GetServerSideProps<Props>` and `GetStaticProps<Props>`
|
||||||
|
|
||||||
|
# React
|
||||||
|
|
||||||
|
- create one file per one component (tiny helper components in the same file are fine)
|
||||||
|
- name file identically to the component it describes (e.g. `const DisplayForecasts: React.FC<Props> = ...` in `DisplayForecasts.ts`)
|
||||||
|
- use named export instead of default export for all React components
|
||||||
|
- it's better for refactoring
|
||||||
|
- and it plays well with `React.FC` typing
|
||||||
|
|
||||||
|
# Styles
|
||||||
|
|
||||||
|
- use [Tailwind](https://tailwindcss.com/)
|
||||||
|
- avoid positioning styles in components, position elements from the outside (e.g. with [space-\*](https://tailwindcss.com/docs/space) or grid/flexbox)
|
||||||
|
|
||||||
|
# General notes
|
||||||
|
|
||||||
|
- use `const` instead of `let` whenever possible
|
||||||
|
- set up [prettier](https://prettier.io/) to format code on save
|
30
src/pages/_middleware.ts
Normal file
30
src/pages/_middleware.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { NextURL } from "next/dist/server/web/next-url";
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function middleware(req: NextRequest) {
|
||||||
|
const { pathname, searchParams } = req.nextUrl;
|
||||||
|
|
||||||
|
console.log(pathname);
|
||||||
|
if (pathname === "/dashboards") {
|
||||||
|
const dashboardId = searchParams.get("dashboardId");
|
||||||
|
if (dashboardId) {
|
||||||
|
return NextResponse.redirect(
|
||||||
|
new URL(`/dashboards/view/${dashboardId}`, req.url)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (pathname === "/secretDashboard") {
|
||||||
|
const dashboardId = searchParams.get("dashboardId");
|
||||||
|
if (dashboardId) {
|
||||||
|
const url = new URL(`/dashboards/embed/${dashboardId}`, req.url);
|
||||||
|
const numCols = searchParams.get("numCols");
|
||||||
|
if (numCols) {
|
||||||
|
url.searchParams.set("numCols", numCols);
|
||||||
|
}
|
||||||
|
return NextResponse.redirect(url);
|
||||||
|
} else {
|
||||||
|
return NextResponse.rewrite(new NextURL("/404", req.url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next();
|
||||||
|
}
|
|
@ -2,9 +2,9 @@ import React from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import gfm from "remark-gfm";
|
import gfm from "remark-gfm";
|
||||||
|
|
||||||
import Layout from "../web/display/layout";
|
import { Layout } from "../web/display/Layout";
|
||||||
|
|
||||||
let readmeMarkdownText = `# About
|
const readmeMarkdownText = `# About
|
||||||
|
|
||||||
This webpage is a search engine for probabilities. Given a query, it searches for relevant questions in various prediction markets and forecasting platforms. For example, try searching for "China", "North Korea", "Semiconductors", "COVID", "Trump", or "X-risk". In addition to search, we also provide various [tools](http://localhost:3000/tools).
|
This webpage is a search engine for probabilities. Given a query, it searches for relevant questions in various prediction markets and forecasting platforms. For example, try searching for "China", "North Korea", "Semiconductors", "COVID", "Trump", or "X-risk". In addition to search, we also provide various [tools](http://localhost:3000/tools).
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { NextPage } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { displayForecastsWrapperForCapture } from "../web/display/displayForecastsWrappers";
|
import { displayForecastsWrapperForCapture } from "../web/display/displayForecastsWrappers";
|
||||||
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";
|
||||||
|
|
||||||
|
|
|
@ -1,210 +0,0 @@
|
||||||
/* Imports */
|
|
||||||
import axios from "axios";
|
|
||||||
import { GetServerSideProps, NextPage } from "next";
|
|
||||||
import { useRouter } from "next/router"; // https://nextjs.org/docs/api-reference/next/router
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { DashboardItem } from "../backend/dashboards";
|
|
||||||
import { getPlatformsConfig, PlatformConfig } from "../backend/platforms";
|
|
||||||
import { DashboardCreator } from "../web/display/dashboardCreator";
|
|
||||||
import displayForecasts from "../web/display/displayForecasts";
|
|
||||||
import Layout from "../web/display/layout";
|
|
||||||
import { addLabelsToForecasts, FrontendForecast } from "../web/platforms";
|
|
||||||
import { getDashboardForecastsByDashboardId } from "../web/worker/getDashboardForecasts";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
initialDashboardForecasts: FrontendForecast[];
|
|
||||||
initialDashboardId: string | null;
|
|
||||||
initialDashboardItem: DashboardItem | null;
|
|
||||||
platformsConfig: PlatformConfig[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Props> = async (
|
|
||||||
context
|
|
||||||
) => {
|
|
||||||
const dashboardIdQ = context.query.dashboardId;
|
|
||||||
const dashboardId: string | undefined =
|
|
||||||
typeof dashboardIdQ === "object" ? dashboardIdQ[0] : dashboardIdQ;
|
|
||||||
|
|
||||||
const platformsConfig = getPlatformsConfig({ withGuesstimate: false });
|
|
||||||
|
|
||||||
if (!dashboardId) {
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
platformsConfig,
|
|
||||||
initialDashboardForecasts: [],
|
|
||||||
initialDashboardId: null,
|
|
||||||
initialDashboardItem: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { dashboardForecasts, dashboardItem } =
|
|
||||||
await getDashboardForecastsByDashboardId({
|
|
||||||
dashboardId,
|
|
||||||
});
|
|
||||||
const frontendDashboardForecasts = addLabelsToForecasts(
|
|
||||||
dashboardForecasts,
|
|
||||||
platformsConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
initialDashboardForecasts: frontendDashboardForecasts,
|
|
||||||
initialDashboardId: dashboardId,
|
|
||||||
initialDashboardItem: dashboardItem,
|
|
||||||
platformsConfig,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Body */
|
|
||||||
const DashboardsPage: NextPage<Props> = ({
|
|
||||||
initialDashboardForecasts,
|
|
||||||
initialDashboardItem,
|
|
||||||
platformsConfig,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const [dashboardForecasts, setDashboardForecasts] = useState(
|
|
||||||
initialDashboardForecasts
|
|
||||||
);
|
|
||||||
const [dashboardItem, setDashboardItem] = useState(initialDashboardItem);
|
|
||||||
|
|
||||||
let handleSubmit = async (data) => {
|
|
||||||
console.log(data);
|
|
||||||
// Send to server to create
|
|
||||||
// Get back the id
|
|
||||||
let response = await axios({
|
|
||||||
url: `/api/create-dashboard-from-ids`,
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
}).then((res) => res.data);
|
|
||||||
let dashboardId = response.dashboardId;
|
|
||||||
if (!!dashboardId) {
|
|
||||||
console.log("response: ", response);
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
let urlWithoutDefaultParameters = `/dashboards?dashboardId=${dashboardId}`;
|
|
||||||
if (!window.location.href.includes(urlWithoutDefaultParameters)) {
|
|
||||||
window.history.replaceState(
|
|
||||||
null,
|
|
||||||
"Metaforecast",
|
|
||||||
urlWithoutDefaultParameters
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// router.push(`?dashboardId=${dashboardId}`)
|
|
||||||
// display it
|
|
||||||
|
|
||||||
let { dashboardForecasts, dashboardItem } =
|
|
||||||
await getDashboardForecastsByDashboardId({
|
|
||||||
dashboardId,
|
|
||||||
});
|
|
||||||
setDashboardForecasts(
|
|
||||||
addLabelsToForecasts(dashboardForecasts, platformsConfig)
|
|
||||||
);
|
|
||||||
setDashboardItem(dashboardItem);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let isGraubardEasterEgg = (name) => (name == "Clay Graubard" ? true : false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout page="dashboard">
|
|
||||||
{/* Display forecasts */}
|
|
||||||
<div className="mt-7 mb-7">
|
|
||||||
<h1
|
|
||||||
className={
|
|
||||||
!!dashboardItem && !!dashboardItem.title
|
|
||||||
? "text-4xl text-center text-gray-600 mt-2 mb-2"
|
|
||||||
: "hidden"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!!dashboardItem ? dashboardItem.title : ""}
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
className={
|
|
||||||
!!dashboardItem &&
|
|
||||||
!!dashboardItem.creator &&
|
|
||||||
!isGraubardEasterEgg(dashboardItem.creator)
|
|
||||||
? "text-lg text-center text-gray-600 mt-2 mb-2"
|
|
||||||
: "hidden"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!!dashboardItem ? `Created by: ${dashboardItem.creator}` : ""}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className={
|
|
||||||
!!dashboardItem &&
|
|
||||||
!!dashboardItem.creator &&
|
|
||||||
isGraubardEasterEgg(dashboardItem.creator)
|
|
||||||
? "text-lg text-center text-gray-600 mt-2 mb-2"
|
|
||||||
: "hidden"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!!dashboardItem ? `Created by: @` : ""}
|
|
||||||
<a
|
|
||||||
href={"https://twitter.com/ClayGraubard"}
|
|
||||||
className="text-blue-600"
|
|
||||||
>
|
|
||||||
Clay Graubard
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className={
|
|
||||||
!!dashboardItem && !!dashboardItem.description
|
|
||||||
? "text-lg text-center text-gray-600 mt-2 mb-2"
|
|
||||||
: "hidden"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{!!dashboardItem ? `${dashboardItem.description}` : ""}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
|
||||||
{displayForecasts({
|
|
||||||
results: dashboardForecasts,
|
|
||||||
numDisplay: dashboardForecasts.length,
|
|
||||||
showIdToggle: false,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{/* */}
|
|
||||||
<h3 className="flex items-center col-start-2 col-end-2 w-full justify-center mt-8 mb-4">
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="flex-grow bg-gray-300 rounded h-0.5"
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
!!dashboardForecasts && dashboardForecasts.length > 0
|
|
||||||
? `mx-3 text-md font-medium text-center`
|
|
||||||
: "hidden"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Or create your own
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
!dashboardForecasts || dashboardForecasts.length == 0
|
|
||||||
? `mx-3 text-md font-medium text-center`
|
|
||||||
: "hidden"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Create a dashboard!
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="flex-grow bg-gray-300 rounded h-0.5"
|
|
||||||
></span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-3 justify-center">
|
|
||||||
<div className="flex col-start-2 col-end-2 items-center justify-center">
|
|
||||||
<DashboardCreator handleSubmit={handleSubmit} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DashboardsPage;
|
|
69
src/pages/dashboards/embed/[id].tsx
Normal file
69
src/pages/dashboards/embed/[id].tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
|
import Error from "next/error";
|
||||||
|
|
||||||
|
import { DashboardItem } from "../../../backend/dashboards";
|
||||||
|
import { DisplayForecasts } from "../../../web/display/DisplayForecasts";
|
||||||
|
import { FrontendForecast } from "../../../web/platforms";
|
||||||
|
import { getDashboardForecastsByDashboardId } from "../../../web/worker/getDashboardForecasts";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
dashboardForecasts: FrontendForecast[];
|
||||||
|
dashboardItem: DashboardItem;
|
||||||
|
numCols?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const dashboardId = context.query.id as string;
|
||||||
|
const numCols = Number(context.query.numCols);
|
||||||
|
|
||||||
|
const { dashboardItem, dashboardForecasts } =
|
||||||
|
await getDashboardForecastsByDashboardId({
|
||||||
|
dashboardId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dashboardItem) {
|
||||||
|
context.res.statusCode = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
dashboardForecasts,
|
||||||
|
dashboardItem,
|
||||||
|
numCols: !numCols ? null : numCols < 5 ? numCols : 4,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmbedDashboardPage: NextPage<Props> = ({
|
||||||
|
dashboardForecasts,
|
||||||
|
dashboardItem,
|
||||||
|
numCols,
|
||||||
|
}) => {
|
||||||
|
if (!dashboardItem) {
|
||||||
|
return <Error statusCode={404} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4 mt-3 flex flex-row justify-left items-center">
|
||||||
|
<div className="mx-2 place-self-left">
|
||||||
|
<div
|
||||||
|
className={`grid grid-cols-${numCols || 1} sm:grid-cols-${
|
||||||
|
numCols || 1
|
||||||
|
} md:grid-cols-${numCols || 2} lg:grid-cols-${
|
||||||
|
numCols || 3
|
||||||
|
} gap-4 mb-6`}
|
||||||
|
>
|
||||||
|
<DisplayForecasts
|
||||||
|
results={dashboardForecasts}
|
||||||
|
numDisplay={dashboardForecasts.length}
|
||||||
|
showIdToggle={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmbedDashboardPage;
|
37
src/pages/dashboards/index.tsx
Normal file
37
src/pages/dashboards/index.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { DashboardCreator } from "../../web/display/DashboardCreator";
|
||||||
|
import { Layout } from "../../web/display/Layout";
|
||||||
|
import { LineHeader } from "../../web/display/LineHeader";
|
||||||
|
|
||||||
|
const DashboardsPage: NextPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleSubmit = async (data) => {
|
||||||
|
// Send to server to create
|
||||||
|
// Get back the id
|
||||||
|
let response = await axios({
|
||||||
|
url: "/api/create-dashboard-from-ids",
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
data: JSON.stringify(data),
|
||||||
|
}).then((res) => res.data);
|
||||||
|
await router.push(`/dashboards/view/${response.dashboardId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout page="dashboard">
|
||||||
|
<div className="flex flex-col my-8 space-y-8">
|
||||||
|
<LineHeader>Create a dashboard!</LineHeader>
|
||||||
|
|
||||||
|
<div className="self-center">
|
||||||
|
<DashboardCreator handleSubmit={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardsPage;
|
115
src/pages/dashboards/view/[id].tsx
Normal file
115
src/pages/dashboards/view/[id].tsx
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
|
import Error from "next/error";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { DashboardItem } from "../../../backend/dashboards";
|
||||||
|
import { DisplayForecasts } from "../../../web/display/DisplayForecasts";
|
||||||
|
import { InfoBox } from "../../../web/display/InfoBox";
|
||||||
|
import { Layout } from "../../../web/display/Layout";
|
||||||
|
import { LineHeader } from "../../../web/display/LineHeader";
|
||||||
|
import { FrontendForecast } from "../../../web/platforms";
|
||||||
|
import { getDashboardForecastsByDashboardId } from "../../../web/worker/getDashboardForecasts";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
dashboardForecasts: FrontendForecast[];
|
||||||
|
dashboardItem: DashboardItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const dashboardId = context.query.id as string;
|
||||||
|
|
||||||
|
const { dashboardForecasts, dashboardItem } =
|
||||||
|
await getDashboardForecastsByDashboardId({
|
||||||
|
dashboardId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dashboardItem) {
|
||||||
|
context.res.statusCode = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
dashboardForecasts,
|
||||||
|
dashboardItem,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const DashboardMetadata: React.FC<{ dashboardItem: DashboardItem }> = ({
|
||||||
|
dashboardItem,
|
||||||
|
}) => (
|
||||||
|
<div>
|
||||||
|
{dashboardItem?.title ? (
|
||||||
|
<h1 className="text-4xl text-center text-gray-600 mt-2 mb-2">
|
||||||
|
{dashboardItem.title}
|
||||||
|
</h1>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{dashboardItem && dashboardItem.creator ? (
|
||||||
|
<p className="text-lg text-center text-gray-600 mt-2 mb-2">
|
||||||
|
Created by:{" "}
|
||||||
|
{dashboardItem.creator === "Clay Graubard" ? (
|
||||||
|
<>
|
||||||
|
@
|
||||||
|
<a
|
||||||
|
href="https://twitter.com/ClayGraubard"
|
||||||
|
className="text-blue-600"
|
||||||
|
>
|
||||||
|
Clay Graubard
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
dashboardItem.creator
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{dashboardItem?.description ? (
|
||||||
|
<p className="text-lg text-center text-gray-600 mt-2 mb-2">
|
||||||
|
{dashboardItem.description}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Body */
|
||||||
|
const ViewDashboardPage: NextPage<Props> = ({
|
||||||
|
dashboardForecasts,
|
||||||
|
dashboardItem,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Layout page="view-dashboard">
|
||||||
|
<div className="flex flex-col my-8 space-y-8">
|
||||||
|
{dashboardItem ? (
|
||||||
|
<DashboardMetadata dashboardItem={dashboardItem} />
|
||||||
|
) : (
|
||||||
|
<Error statusCode={404} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<DisplayForecasts
|
||||||
|
results={dashboardForecasts}
|
||||||
|
numDisplay={dashboardForecasts.length}
|
||||||
|
showIdToggle={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-xl self-center">
|
||||||
|
<InfoBox>
|
||||||
|
Dashboards cannot be changed after they are created.
|
||||||
|
</InfoBox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LineHeader>
|
||||||
|
<Link href="/dashboards" passHref>
|
||||||
|
<a>Create your own dashboard</a>
|
||||||
|
</Link>
|
||||||
|
</LineHeader>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewDashboardPage;
|
|
@ -2,7 +2,7 @@ import { NextPage } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { displayForecastsWrapperForSearch } from "../web/display/displayForecastsWrappers";
|
import { displayForecastsWrapperForSearch } from "../web/display/displayForecastsWrappers";
|
||||||
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";
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
import { NextPage } from "next";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
|
||||||
function Recursion() {
|
const Recursion: NextPage = () => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
|
window.location.href = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
|
||||||
|
@ -12,6 +13,6 @@ function Recursion() {
|
||||||
<h2>You have now reached the fourth level of recursion!!</h2>
|
<h2>You have now reached the fourth level of recursion!!</h2>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Recursion;
|
export default Recursion;
|
||||||
|
|
|
@ -1,100 +0,0 @@
|
||||||
/* Imports */
|
|
||||||
|
|
||||||
// React
|
|
||||||
import { useRouter } from "next/router"; // https://nextjs.org/docs/api-reference/next/router
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { getPlatformsConfig } from "../backend/platforms";
|
|
||||||
import displayForecasts from "../web/display/displayForecasts";
|
|
||||||
import { addLabelsToForecasts } from "../web/platforms";
|
|
||||||
import { getDashboardForecastsByDashboardId } from "../web/worker/getDashboardForecasts";
|
|
||||||
|
|
||||||
/* get Props */
|
|
||||||
|
|
||||||
export async function getServerSideProps(context) {
|
|
||||||
console.log("getServerSideProps: ");
|
|
||||||
let urlQuery = context.query;
|
|
||||||
console.log(urlQuery);
|
|
||||||
|
|
||||||
let dashboardId = urlQuery.dashboardId;
|
|
||||||
let numCols = urlQuery.numCols;
|
|
||||||
let props;
|
|
||||||
if (!!dashboardId) {
|
|
||||||
console.log(dashboardId);
|
|
||||||
let { dashboardForecasts, dashboardItem } =
|
|
||||||
await getDashboardForecastsByDashboardId({
|
|
||||||
dashboardId,
|
|
||||||
});
|
|
||||||
dashboardForecasts = addLabelsToForecasts(
|
|
||||||
dashboardForecasts,
|
|
||||||
getPlatformsConfig({ withGuesstimate: false })
|
|
||||||
);
|
|
||||||
props = {
|
|
||||||
initialDashboardForecasts: dashboardForecasts,
|
|
||||||
initialDashboardId: urlQuery.dashboardId,
|
|
||||||
initialDashboardItem: dashboardItem,
|
|
||||||
initialNumCols: !numCols ? null : numCols < 5 ? numCols : 4,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log();
|
|
||||||
props = {
|
|
||||||
initialDashboardForecasts: [],
|
|
||||||
initialDashboardId: urlQuery.dashboardId || null,
|
|
||||||
initialDashboardItem: null,
|
|
||||||
initialNumCols: !numCols ? null : numCols < 5 ? numCols : 4,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: props,
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
let dashboardforecasts = await getdashboardforecasts({
|
|
||||||
ids: ["metaculus-6526", "smarkets-20206424"],
|
|
||||||
});
|
|
||||||
let props = {
|
|
||||||
dashboardforecasts: dashboardforecasts,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: props,
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Body */
|
|
||||||
export default function Home({
|
|
||||||
initialDashboardForecasts,
|
|
||||||
initialDashboardItem,
|
|
||||||
initialNumCols,
|
|
||||||
}) {
|
|
||||||
const router = useRouter();
|
|
||||||
const [dashboardForecasts, setDashboardForecasts] = useState(
|
|
||||||
initialDashboardForecasts
|
|
||||||
);
|
|
||||||
const [dashboardItem, setDashboardItem] = useState(initialDashboardItem);
|
|
||||||
const [numCols, setNumCols] = useState(initialNumCols);
|
|
||||||
console.log("initialNumCols", initialNumCols);
|
|
||||||
|
|
||||||
// <div className={`grid ${!!numCols ? `grid-cols-${numCols} md:grid-cols-${numCols} lg:grid-cols-${numCols}`: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"} gap-4 mb-6`}>
|
|
||||||
// <div className={`grid grid-cols-${numCols || 1} md:grid-cols-${numCols || 2} lg:grid-cols-${numCols || 3} gap-4 mb-6`}>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-4 mt-3 flex flex-row justify-left items-center ">
|
|
||||||
<div className="ml-2 mr-2 place-self-left">
|
|
||||||
<div
|
|
||||||
className={`grid grid-cols-${numCols || 1} sm:grid-cols-${
|
|
||||||
numCols || 1
|
|
||||||
} md:grid-cols-${numCols || 2} lg:grid-cols-${
|
|
||||||
numCols || 3
|
|
||||||
} gap-4 mb-6`}
|
|
||||||
>
|
|
||||||
{displayForecasts({
|
|
||||||
results: dashboardForecasts,
|
|
||||||
numDisplay: dashboardForecasts.length,
|
|
||||||
showIdToggle: false,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import { GetServerSideProps, NextPage } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { platforms } from "../backend/platforms";
|
import { platforms } from "../backend/platforms";
|
||||||
import { DisplayForecast } from "../web/display/displayForecasts";
|
import { DisplayForecast } from "../web/display/DisplayForecast";
|
||||||
import { FrontendForecast } from "../web/platforms";
|
import { FrontendForecast } from "../web/platforms";
|
||||||
import searchAccordingToQueryData from "../web/worker/searchAccordingToQueryData";
|
import searchAccordingToQueryData from "../web/worker/searchAccordingToQueryData";
|
||||||
|
|
||||||
|
|
|
@ -1,85 +1,76 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import Layout from "../web/display/layout";
|
import { Card } from "../web/display/Card";
|
||||||
|
import { Layout } from "../web/display/Layout";
|
||||||
|
|
||||||
|
type AnyTool = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
img?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InnerTool = AnyTool & { innerLink: string };
|
||||||
|
type ExternalTool = AnyTool & { externalLink: string };
|
||||||
|
type UpcomingTool = AnyTool;
|
||||||
|
|
||||||
|
type Tool = InnerTool | ExternalTool | UpcomingTool;
|
||||||
|
|
||||||
/* Display one tool */
|
/* Display one tool */
|
||||||
function displayTool({
|
const ToolCard: React.FC<Tool> = (tool) => {
|
||||||
sameWebpage,
|
const inner = (
|
||||||
title,
|
<Card>
|
||||||
description,
|
<div className="grid content-start gap-3">
|
||||||
link,
|
<div className="text-gray-800 text-lg font-medium">{tool.title}</div>
|
||||||
url,
|
<div className="text-gray-500">{tool.description}</div>
|
||||||
img,
|
{tool.img && <img src={tool.img} className="text-gray-500" />}
|
||||||
i,
|
</div>
|
||||||
}: any) {
|
</Card>
|
||||||
switch (sameWebpage) {
|
);
|
||||||
case true:
|
|
||||||
|
if ("innerLink" in tool) {
|
||||||
return (
|
return (
|
||||||
<Link href={link} passHref key={`tool-${i}`}>
|
<Link href={tool.innerLink} passHref>
|
||||||
<div className="hover:bg-gray-100 hover:no-underline cursor-pointer flex flex-col px-4 py-3 bg-white rounded-md shadow place-content-stretch flex-grow no-underline b-6">
|
<a className="text‑inherit no-underline">{inner}</a>
|
||||||
<div className="flex-grow items-stretch">
|
|
||||||
<div className={`text-gray-800 text-lg mb-2 font-medium `}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
<div className={`text-gray-500 mb-3 `}>{description}</div>
|
|
||||||
{}
|
|
||||||
<img src={img} className={`text-gray-500 mb-2`} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
break;
|
} else if ("externalLink" in tool) {
|
||||||
default:
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a href={tool.externalLink} className="text‑inherit no-underline">
|
||||||
href={url}
|
{inner}
|
||||||
key={`tool-${i}`}
|
|
||||||
className="hover:bg-gray-100 hover:no-underline cursor-pointer flex flex-col px-4 py-3 bg-white rounded-md shadow place-content-stretch flex-grow no-underline b-6"
|
|
||||||
>
|
|
||||||
<div className="flex-grow items-stretch">
|
|
||||||
<div className={`text-gray-800 text-lg mb-2 font-medium `}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
<div className={`text-gray-500 mb-3 `}>{description}</div>
|
|
||||||
{}
|
|
||||||
<img src={img} className={`text-gray-500 mb-2`} />
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
break;
|
} else {
|
||||||
|
return inner;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export default function Tools({ lastUpdated }) {
|
export default function Tools({ lastUpdated }) {
|
||||||
let tools = [
|
let tools: Tool[] = [
|
||||||
{
|
{
|
||||||
title: "Search",
|
title: "Search",
|
||||||
description: "Find forecasting questions on many platforms",
|
description: "Find forecasting questions on many platforms.",
|
||||||
link: "/",
|
innerLink: "/",
|
||||||
sameWebpage: true,
|
|
||||||
img: "https://i.imgur.com/Q94gVqG.png",
|
img: "https://i.imgur.com/Q94gVqG.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "[Beta] Present",
|
title: "[Beta] Present",
|
||||||
description: "Present forecasts in dashboards.",
|
description: "Present forecasts in dashboards.",
|
||||||
sameWebpage: true,
|
innerLink: "/dashboards",
|
||||||
link: "/dashboards",
|
|
||||||
img: "https://i.imgur.com/x8qkuHQ.png",
|
img: "https://i.imgur.com/x8qkuHQ.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Capture",
|
title: "Capture",
|
||||||
description:
|
description:
|
||||||
"Capture forecasts save them to Imgur. Useful for posting them somewhere else as images. Currently rate limited by Imgur, so if you get a .gif of a fox falling flat on his face, that's why.",
|
"Capture forecasts save them to Imgur. Useful for posting them somewhere else as images. Currently rate limited by Imgur, so if you get a .gif of a fox falling flat on his face, that's why.",
|
||||||
link: "/capture",
|
innerLink: "/capture",
|
||||||
sameWebpage: true,
|
|
||||||
img: "https://i.imgur.com/EXkFBzz.png",
|
img: "https://i.imgur.com/EXkFBzz.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Summon",
|
title: "Summon",
|
||||||
description:
|
description:
|
||||||
"Summon metaforecast on Twitter by mentioning @metaforecast, or on Discord by using Fletcher and !metaforecast, followed by search terms",
|
"Summon metaforecast on Twitter by mentioning @metaforecast, or on Discord by using Fletcher and !metaforecast, followed by search terms.",
|
||||||
url: "https://twitter.com/metaforecast",
|
externalLink: "https://twitter.com/metaforecast",
|
||||||
img: "https://i.imgur.com/BQ4Zzjw.png",
|
img: "https://i.imgur.com/BQ4Zzjw.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -87,7 +78,6 @@ export default function Tools({ lastUpdated }) {
|
||||||
description:
|
description:
|
||||||
"Interact with metaforecast's API and fetch forecasts for your application. Currently possible but documentation is poor, get in touch.",
|
"Interact with metaforecast's API and fetch forecasts for your application. Currently possible but documentation is poor, get in touch.",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "[Upcoming] Record",
|
title: "[Upcoming] Record",
|
||||||
description: "Save your forecasts or bets.",
|
description: "Save your forecasts or bets.",
|
||||||
|
@ -95,8 +85,10 @@ export default function Tools({ lastUpdated }) {
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<Layout page="tools">
|
<Layout page="tools">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4 mb-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4 mb-8 place-content-stretch">
|
||||||
{tools.map((tool, i) => displayTool({ ...tool, i }))}
|
{tools.map((tool, i) => (
|
||||||
|
<ToolCard {...tool} key={`tool-${i}`} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
10
src/web/display/Button.tsx
Normal file
10
src/web/display/Button.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
|
@ -5,11 +5,11 @@ interface Props {
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ButtonsForStars: React.FC<Props> = ({ onChange, value }) => {
|
export const ButtonsForStars: React.FC<Props> = ({ onChange, value }) => {
|
||||||
const onChangeInner = (buttonPressed: number) => {
|
const onChangeInner = (buttonPressed: number) => {
|
||||||
onChange(buttonPressed);
|
onChange(buttonPressed);
|
||||||
};
|
};
|
||||||
let setStyle = (buttonNumber: number) =>
|
const setStyle = (buttonNumber: number) =>
|
||||||
`flex row-span-1 col-start-${buttonNumber + 1} col-end-${
|
`flex row-span-1 col-start-${buttonNumber + 1} col-end-${
|
||||||
buttonNumber + 2
|
buttonNumber + 2
|
||||||
} items-center justify-center text-center${
|
} items-center justify-center text-center${
|
||||||
|
@ -37,5 +37,3 @@ const ButtonsForStars: React.FC<Props> = ({ onChange, value }) => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ButtonsForStars;
|
|
15
src/web/display/Card.tsx
Normal file
15
src/web/display/Card.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
const CardTitle: React.FC = ({ children }) => (
|
||||||
|
<div className="text-gray-800 text-lg font-medium">{children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type CardType = React.FC & {
|
||||||
|
Title: typeof CardTitle;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Card: CardType = ({ children }) => (
|
||||||
|
<div className="h-full px-4 py-3 bg-white hover:bg-gray-100 rounded-md shadow">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
Card.Title = CardTitle;
|
69
src/web/display/DashboardCreator.tsx
Normal file
69
src/web/display/DashboardCreator.tsx
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import React, { EventHandler, SyntheticEvent, useState } from "react";
|
||||||
|
|
||||||
|
import { Button } from "./Button";
|
||||||
|
import { InfoBox } from "./InfoBox";
|
||||||
|
|
||||||
|
const exampleInput = `{
|
||||||
|
"title": "Random example",
|
||||||
|
"description": "Just a random description of a random example",
|
||||||
|
"ids": [ "metaculus-372", "goodjudgmentopen-2244", "metaculus-7550", "kalshi-09d060ee-b184-4167-b86b-d773e56b4162", "wildeford-5d1a04e1a8", "metaculus-2817" ],
|
||||||
|
"creator": "Peter Parker"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
handleSubmit: (data: any) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DashboardCreator: React.FC<Props> = ({ handleSubmit }) => {
|
||||||
|
const [value, setValue] = useState(exampleInput);
|
||||||
|
const [acting, setActing] = useState(false);
|
||||||
|
|
||||||
|
const handleChange = (event) => {
|
||||||
|
setValue(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitInner: EventHandler<SyntheticEvent> = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newData = JSON.parse(value);
|
||||||
|
|
||||||
|
if (!newData || !newData.ids || newData.ids.length == 0) {
|
||||||
|
throw Error("Not enough objects");
|
||||||
|
} else {
|
||||||
|
setActing(true);
|
||||||
|
await handleSubmit(newData);
|
||||||
|
setActing(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setActing(false);
|
||||||
|
const substituteText = `Error: ${error.message}
|
||||||
|
|
||||||
|
Try something like:
|
||||||
|
${exampleInput}
|
||||||
|
|
||||||
|
Your old input was: ${value}`;
|
||||||
|
setValue(substituteText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmitInner}>
|
||||||
|
<div className="flex flex-col items-center space-y-5 max-w-2xl">
|
||||||
|
<textarea value={value} onChange={handleChange} rows={8} cols={50} />
|
||||||
|
<Button
|
||||||
|
disabled={acting}
|
||||||
|
onClick={acting ? undefined : handleSubmitInner}
|
||||||
|
>
|
||||||
|
{acting ? "Creating..." : "Create dashboard"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<InfoBox>
|
||||||
|
You can find the necessary ids by toggling the advanced options in the
|
||||||
|
search, or by visiting{" "}
|
||||||
|
<a href="/api/all-forecasts">/api/all-forecasts</a>
|
||||||
|
</InfoBox>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
299
src/web/display/DisplayForecast/ForecastFooter.tsx
Normal file
299
src/web/display/DisplayForecast/ForecastFooter.tsx
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
const formatQualityIndicator = (indicator) => {
|
||||||
|
let result;
|
||||||
|
switch (indicator) {
|
||||||
|
case "numforecasts":
|
||||||
|
result = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "stars":
|
||||||
|
result = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "volume":
|
||||||
|
result = "Volume";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "numforecasters":
|
||||||
|
result = "Forecasters";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "yes_bid":
|
||||||
|
result = null; // "Yes bid"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "yes_ask":
|
||||||
|
result = null; // "Yes ask"
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "spread":
|
||||||
|
result = "Spread";
|
||||||
|
break;
|
||||||
|
case "shares_volume":
|
||||||
|
result = "Shares vol.";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "open_interest":
|
||||||
|
result = "Interest";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "resolution_data":
|
||||||
|
result = null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "liquidity":
|
||||||
|
result = "Liquidity";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "tradevolume":
|
||||||
|
result = "Volume";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatNumber = (num) => {
|
||||||
|
if (Number(num) < 1000) {
|
||||||
|
return Number(num).toFixed(0);
|
||||||
|
} else if (num < 10000) {
|
||||||
|
return (Number(num) / 1000).toFixed(1) + "k";
|
||||||
|
} else {
|
||||||
|
return (Number(num) / 1000).toFixed(0) + "k";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatQualityIndicators = (qualityIndicators: any) => {
|
||||||
|
let newQualityIndicators = {};
|
||||||
|
for (let key in qualityIndicators) {
|
||||||
|
let newKey = formatQualityIndicator(key);
|
||||||
|
if (newKey) {
|
||||||
|
newQualityIndicators[newKey] = qualityIndicators[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newQualityIndicators;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Display functions*/
|
||||||
|
|
||||||
|
const getPercentageSymbolIfNeeded = ({ indicator, platform }) => {
|
||||||
|
let indicatorsWhichNeedPercentageSymbol = ["Spread"];
|
||||||
|
if (indicatorsWhichNeedPercentageSymbol.includes(indicator)) {
|
||||||
|
return "%";
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrencySymbolIfNeeded = ({
|
||||||
|
indicator,
|
||||||
|
platform,
|
||||||
|
}: {
|
||||||
|
indicator: any;
|
||||||
|
platform: string;
|
||||||
|
}) => {
|
||||||
|
let indicatorsWhichNeedCurrencySymbol = ["Volume", "Interest", "Liquidity"];
|
||||||
|
let dollarPlatforms = ["predictit", "kalshi", "polymarket"];
|
||||||
|
if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) {
|
||||||
|
if (dollarPlatforms.includes(platform)) {
|
||||||
|
return "$";
|
||||||
|
} else {
|
||||||
|
return "£";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showFirstQualityIndicator = ({
|
||||||
|
numforecasts,
|
||||||
|
timestamp,
|
||||||
|
showTimeStamp,
|
||||||
|
qualityindicators,
|
||||||
|
}) => {
|
||||||
|
if (!!numforecasts) {
|
||||||
|
return (
|
||||||
|
<div className="flex col-span-1 row-span-1">
|
||||||
|
{/*<span>{` ${numforecasts == 1 ? "Forecast" : "Forecasts:"}`}</span> */}
|
||||||
|
<span>{"Forecasts:"}</span>
|
||||||
|
<span className="font-bold">{Number(numforecasts).toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (showTimeStamp) {
|
||||||
|
return (
|
||||||
|
<span className="hidden sm:flex items-center justify-center text-gray-600 mt-2">
|
||||||
|
<svg className="ml-4 mr-1 mt-1" height="10" width="16">
|
||||||
|
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
|
||||||
|
</svg>
|
||||||
|
{`Last updated: ${
|
||||||
|
timestamp && !!timestamp.slice ? timestamp.slice(0, 10) : "unknown"
|
||||||
|
}`}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayQualityIndicators: React.FC<{
|
||||||
|
numforecasts: number;
|
||||||
|
timestamp: number;
|
||||||
|
showTimeStamp: boolean;
|
||||||
|
qualityindicators: any;
|
||||||
|
platform: string; // id string - e.g. "goodjudgment", not "Good Judgment"
|
||||||
|
}> = ({
|
||||||
|
numforecasts,
|
||||||
|
timestamp,
|
||||||
|
showTimeStamp,
|
||||||
|
qualityindicators,
|
||||||
|
platform,
|
||||||
|
}) => {
|
||||||
|
// grid grid-cols-1
|
||||||
|
return (
|
||||||
|
<div className="text-sm">
|
||||||
|
{showFirstQualityIndicator({
|
||||||
|
numforecasts,
|
||||||
|
timestamp,
|
||||||
|
showTimeStamp,
|
||||||
|
qualityindicators,
|
||||||
|
})}
|
||||||
|
{Object.entries(formatQualityIndicators(qualityindicators)).map(
|
||||||
|
(entry, i) => {
|
||||||
|
return (
|
||||||
|
<div className="col-span-1 row-span-1">
|
||||||
|
<span>{`${entry[0]}:`}</span>
|
||||||
|
<span className="font-bold">
|
||||||
|
{`${getCurrencySymbolIfNeeded({
|
||||||
|
indicator: entry[0],
|
||||||
|
platform,
|
||||||
|
})}${formatNumber(entry[1])}${getPercentageSymbolIfNeeded({
|
||||||
|
indicator: entry[0],
|
||||||
|
platform,
|
||||||
|
})}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Database-like functions
|
||||||
|
export function getstars(numstars: number) {
|
||||||
|
let stars = "★★☆☆☆";
|
||||||
|
switch (numstars) {
|
||||||
|
case 0:
|
||||||
|
stars = "☆☆☆☆☆";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
stars = "★☆☆☆☆";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
stars = "★★☆☆☆";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
stars = "★★★☆☆";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
stars = "★★★★☆";
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
stars = "★★★★★";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
stars = "★★☆☆☆";
|
||||||
|
}
|
||||||
|
return stars;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStarsColor(numstars: number) {
|
||||||
|
let color = "text-yellow-400";
|
||||||
|
switch (numstars) {
|
||||||
|
case 0:
|
||||||
|
color = "text-red-400";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
color = "text-red-400";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
color = "text-orange-400";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
color = "text-yellow-400";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
color = "text-green-400";
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
color = "text-blue-400";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
color = "text-yellow-400";
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stars: any;
|
||||||
|
platform: string;
|
||||||
|
platformLabel: string;
|
||||||
|
numforecasts: any;
|
||||||
|
qualityindicators: any;
|
||||||
|
timestamp: any;
|
||||||
|
showTimeStamp: boolean;
|
||||||
|
expandFooterToFullWidth: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ForecastFooter: React.FC<Props> = ({
|
||||||
|
stars,
|
||||||
|
platform,
|
||||||
|
platformLabel,
|
||||||
|
numforecasts,
|
||||||
|
qualityindicators,
|
||||||
|
timestamp,
|
||||||
|
showTimeStamp,
|
||||||
|
expandFooterToFullWidth,
|
||||||
|
}) => {
|
||||||
|
// I experimented with justify-evenly, justify-around, etc., here: https://tailwindcss.com/docs/justify-content
|
||||||
|
// I came to the conclusion that as long as the description isn't justified too, aligning the footer symmetrically doesn't make sense
|
||||||
|
// because the contrast is jarring.
|
||||||
|
let debuggingWithBackground = false;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`grid grid-cols-3 ${
|
||||||
|
expandFooterToFullWidth ? "justify-between" : ""
|
||||||
|
} text-gray-500 mb-2 mt-1`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`self-center col-span-1 ${getStarsColor(stars)} ${
|
||||||
|
debuggingWithBackground ? "bg-red-200" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getstars(stars)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
expandFooterToFullWidth ? "place-self-center" : "self-center"
|
||||||
|
} col-span-1 font-bold ${debuggingWithBackground ? "bg-red-100" : ""}`}
|
||||||
|
>
|
||||||
|
{platformLabel
|
||||||
|
.replace("Good Judgment Open", "GJOpen")
|
||||||
|
.replace(/ /g, "\u00a0")}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
expandFooterToFullWidth
|
||||||
|
? "justify-self-end mr-4"
|
||||||
|
: "justify-self-center"
|
||||||
|
} col-span-1 ${debuggingWithBackground ? "bg-red-100" : ""}`}
|
||||||
|
>
|
||||||
|
{displayQualityIndicators({
|
||||||
|
numforecasts,
|
||||||
|
timestamp,
|
||||||
|
showTimeStamp,
|
||||||
|
qualityindicators,
|
||||||
|
platform,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
390
src/web/display/DisplayForecast/index.tsx
Normal file
390
src/web/display/DisplayForecast/index.tsx
Normal file
|
@ -0,0 +1,390 @@
|
||||||
|
import { FaRegClipboard } from "react-icons/fa";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
|
import { FrontendForecast } from "../../platforms";
|
||||||
|
import { Card } from "../Card";
|
||||||
|
import { ForecastFooter } from "./ForecastFooter";
|
||||||
|
|
||||||
|
const truncateText = (length: number, text: string): string => {
|
||||||
|
if (!text) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (!!text && text.length <= length) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
let breakpoints = " .!?";
|
||||||
|
let lastLetter = null;
|
||||||
|
let lastIndex = null;
|
||||||
|
for (let index = length; index > 0; index--) {
|
||||||
|
let letter = text[index];
|
||||||
|
if (breakpoints.includes(letter)) {
|
||||||
|
lastLetter = letter;
|
||||||
|
lastIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let truncatedText = !!text.slice
|
||||||
|
? text.slice(0, lastIndex) + (lastLetter != "." ? "..." : "..")
|
||||||
|
: "";
|
||||||
|
return truncatedText;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatProbability = (probability: number) => {
|
||||||
|
let percentage = probability * 100;
|
||||||
|
let percentageCapped =
|
||||||
|
percentage < 1
|
||||||
|
? "< 1%"
|
||||||
|
: percentage > 99
|
||||||
|
? "> 99%"
|
||||||
|
: percentage.toFixed(0) + "%";
|
||||||
|
return percentageCapped;
|
||||||
|
};
|
||||||
|
|
||||||
|
// replaceAll polyfill
|
||||||
|
function escapeRegExp(string) {
|
||||||
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceAll(
|
||||||
|
originalString: string,
|
||||||
|
pattern: string | RegExp,
|
||||||
|
substitute
|
||||||
|
) {
|
||||||
|
return originalString.replace(
|
||||||
|
new RegExp(escapeRegExp(pattern), "g"),
|
||||||
|
substitute
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!String.prototype.replaceAll) {
|
||||||
|
String.prototype.replaceAll = function (
|
||||||
|
pattern: string | RegExp,
|
||||||
|
substitute
|
||||||
|
) {
|
||||||
|
let originalString = this;
|
||||||
|
|
||||||
|
// If a regex pattern
|
||||||
|
if (
|
||||||
|
Object.prototype.toString.call(pattern).toLowerCase() ===
|
||||||
|
"[object regexp]"
|
||||||
|
) {
|
||||||
|
return originalString.replace(pattern, substitute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a string
|
||||||
|
return replaceAll(originalString, pattern, substitute);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanText = (text: string): string => {
|
||||||
|
// Note: should no longer be necessary
|
||||||
|
let textString = !!text ? text : "";
|
||||||
|
textString = textString
|
||||||
|
.replaceAll("] (", "](")
|
||||||
|
.replaceAll(") )", "))")
|
||||||
|
.replaceAll("( [", "([")
|
||||||
|
.replaceAll(") ,", "),")
|
||||||
|
.replaceAll("==", "") // Denotes a title in markdown
|
||||||
|
.replaceAll("Background\n", "")
|
||||||
|
.replaceAll("Context\n", "")
|
||||||
|
.replaceAll("--- \n", "- ")
|
||||||
|
.replaceAll(/\[(.*?)\]\(.*?\)/g, "$1");
|
||||||
|
textString = textString.slice(0, 1) == "=" ? textString.slice(1) : textString;
|
||||||
|
//console.log(textString)
|
||||||
|
return textString;
|
||||||
|
};
|
||||||
|
|
||||||
|
const primaryForecastColor = (probability: number) => {
|
||||||
|
if (probability < 0.03) {
|
||||||
|
return "bg-red-600";
|
||||||
|
} else if (probability < 0.1) {
|
||||||
|
return "bg-red-600 opacity-80";
|
||||||
|
} else if (probability < 0.2) {
|
||||||
|
return "bg-red-600 opacity-70";
|
||||||
|
} else if (probability < 0.3) {
|
||||||
|
return "bg-red-600 opacity-60";
|
||||||
|
} else if (probability < 0.4) {
|
||||||
|
return "bg-red-600 opacity-50";
|
||||||
|
} else if (probability < 0.5) {
|
||||||
|
return "bg-gray-500";
|
||||||
|
} else if (probability < 0.6) {
|
||||||
|
return "bg-gray-500";
|
||||||
|
} else if (probability < 0.7) {
|
||||||
|
return "bg-green-600 opacity-50";
|
||||||
|
} else if (probability < 0.8) {
|
||||||
|
return "bg-green-600 opacity-60";
|
||||||
|
} else if (probability < 0.9) {
|
||||||
|
return "bg-green-600 opacity-70";
|
||||||
|
} else if (probability < 0.97) {
|
||||||
|
return "bg-green-600 opacity-80";
|
||||||
|
} else {
|
||||||
|
return "bg-green-600";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const textColor = (probability: number) => {
|
||||||
|
if (probability < 0.03) {
|
||||||
|
return "text-red-600";
|
||||||
|
} else if (probability < 0.1) {
|
||||||
|
return "text-red-600 opacity-80";
|
||||||
|
} else if (probability < 0.2) {
|
||||||
|
return "text-red-600 opacity-80";
|
||||||
|
} else if (probability < 0.3) {
|
||||||
|
return "text-red-600 opacity-70";
|
||||||
|
} else if (probability < 0.4) {
|
||||||
|
return "text-red-600 opacity-70";
|
||||||
|
} else if (probability < 0.5) {
|
||||||
|
return "text-gray-500";
|
||||||
|
} else if (probability < 0.6) {
|
||||||
|
return "text-gray-500";
|
||||||
|
} else if (probability < 0.7) {
|
||||||
|
return "text-green-600 opacity-70";
|
||||||
|
} else if (probability < 0.8) {
|
||||||
|
return "text-green-600 opacity-70";
|
||||||
|
} else if (probability < 0.9) {
|
||||||
|
return "text-green-600 opacity-80";
|
||||||
|
} else if (probability < 0.97) {
|
||||||
|
return "text-green-600 opacity-80";
|
||||||
|
} else {
|
||||||
|
return "text-green-600";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const primaryEstimateAsText = (probability: number) => {
|
||||||
|
if (probability < 0.03) {
|
||||||
|
return "Exceptionally unlikely";
|
||||||
|
} else if (probability < 0.1) {
|
||||||
|
return "Very unlikely";
|
||||||
|
} else if (probability < 0.4) {
|
||||||
|
return "Unlikely";
|
||||||
|
} else if (probability < 0.6) {
|
||||||
|
return "About Even";
|
||||||
|
} else if (probability < 0.9) {
|
||||||
|
return "Likely";
|
||||||
|
} else if (probability < 0.97) {
|
||||||
|
return "Very likely";
|
||||||
|
} else {
|
||||||
|
return "Virtually certain";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logical checks
|
||||||
|
|
||||||
|
const checkIfDisplayTimeStampAtBottom = (qualityIndicators: {
|
||||||
|
[k: string]: any;
|
||||||
|
}) => {
|
||||||
|
let indicators = Object.keys(qualityIndicators);
|
||||||
|
if (indicators.length == 1 && indicators[0] == "stars") {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auxiliary components
|
||||||
|
|
||||||
|
const DisplayMarkdown: React.FC<{ description: string }> = ({
|
||||||
|
description,
|
||||||
|
}) => {
|
||||||
|
let formatted = truncateText(250, cleanText(description));
|
||||||
|
// overflow-hidden overflow-ellipsis h-24
|
||||||
|
return formatted === "" ? null : (
|
||||||
|
<div className="overflow-clip">
|
||||||
|
<ReactMarkdown linkTarget="_blank" className="font-normal">
|
||||||
|
{formatted}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OptionRow: React.FC<{ option: any }> = ({ option }) => {
|
||||||
|
const chooseColor = (probability: number) => {
|
||||||
|
if (probability < 0.1) {
|
||||||
|
return "bg-blue-50 text-blue-500";
|
||||||
|
} else if (probability < 0.3) {
|
||||||
|
return "bg-blue-100 text-blue-600";
|
||||||
|
} else if (probability < 0.7) {
|
||||||
|
return "bg-blue-200 text-blue-700";
|
||||||
|
} else {
|
||||||
|
return "bg-blue-300 text-blue-800";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div
|
||||||
|
className={`${chooseColor(
|
||||||
|
option.probability
|
||||||
|
)} w-14 flex-none rounded-md py-0.5 text-sm text-center`}
|
||||||
|
>
|
||||||
|
{formatProbability(option.probability)}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-700 pl-3 leading-snug text-sm">
|
||||||
|
{option.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ForecastOptions: React.FC<{ options: any[] }> = ({ options }) => {
|
||||||
|
const optionsSorted = options.sort((a, b) => b.probability - a.probability);
|
||||||
|
const optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options.
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{optionsMax5.map((option, i) => (
|
||||||
|
<OptionRow option={option} key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CopyText: React.FC<{ text: string; displayText: string }> = ({
|
||||||
|
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"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{displayText}</span>
|
||||||
|
<FaRegClipboard />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const LastUpdated: React.FC<{ timestamp: string }> = ({ timestamp }) => (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<svg className="mt-1" height="10" width="16">
|
||||||
|
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-gray-600">
|
||||||
|
Last updated: {timestamp ? timestamp.slice(0, 10) : "unknown"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Main component
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
forecast: FrontendForecast;
|
||||||
|
showTimeStamp: boolean;
|
||||||
|
expandFooterToFullWidth: boolean;
|
||||||
|
showIdToggle?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplayForecast: React.FC<Props> = ({
|
||||||
|
forecast: {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
platform,
|
||||||
|
platformLabel,
|
||||||
|
description,
|
||||||
|
options,
|
||||||
|
qualityindicators,
|
||||||
|
timestamp,
|
||||||
|
visualization,
|
||||||
|
},
|
||||||
|
showTimeStamp,
|
||||||
|
expandFooterToFullWidth,
|
||||||
|
showIdToggle,
|
||||||
|
}) => {
|
||||||
|
const displayTimestampAtBottom =
|
||||||
|
checkIfDisplayTimeStampAtBottom(qualityindicators);
|
||||||
|
|
||||||
|
const yesNoOptions =
|
||||||
|
options.length === 2 &&
|
||||||
|
(options[0].name === "Yes" || options[0].name === "No");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a className="text‑inherit no-underline" href={url} target="_blank">
|
||||||
|
<Card>
|
||||||
|
<div className="h-full flex flex-col space-y-4">
|
||||||
|
<div className="flex-grow space-y-4">
|
||||||
|
{showIdToggle ? (
|
||||||
|
<div className="mx-10">
|
||||||
|
<CopyText text={id} displayText={`[${id}]`} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<Card.Title>{title}</Card.Title>
|
||||||
|
{yesNoOptions && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="space-x-2">
|
||||||
|
<span
|
||||||
|
className={`${primaryForecastColor(
|
||||||
|
options[0].probability
|
||||||
|
)} text-white w-16 rounded-md px-1.5 py-0.5 font-bold`}
|
||||||
|
>
|
||||||
|
{formatProbability(options[0].probability)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`${textColor(
|
||||||
|
options[0].probability
|
||||||
|
)} text-gray-500 inline-block`}
|
||||||
|
>
|
||||||
|
{primaryEstimateAsText(options[0].probability)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`hidden ${
|
||||||
|
showTimeStamp && !displayTimestampAtBottom ? "sm:block" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<LastUpdated timestamp={timestamp} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!yesNoOptions && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<ForecastOptions options={options} />
|
||||||
|
<div
|
||||||
|
className={`hidden ${
|
||||||
|
showTimeStamp && !displayTimestampAtBottom ? "sm:block" : ""
|
||||||
|
} ml-6`}
|
||||||
|
>
|
||||||
|
<LastUpdated timestamp={timestamp} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{platform !== "guesstimate" && options.length < 3 && (
|
||||||
|
<div className="text-gray-500">
|
||||||
|
<DisplayMarkdown description={description} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{platform === "guesstimate" && (
|
||||||
|
<img
|
||||||
|
className="rounded-sm"
|
||||||
|
src={visualization}
|
||||||
|
alt="Guesstimate Screenshot"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`sm:hidden ${
|
||||||
|
!showTimeStamp ? "hidden" : ""
|
||||||
|
} self-center`}
|
||||||
|
>
|
||||||
|
{/* This one is exclusively for mobile*/}
|
||||||
|
<LastUpdated timestamp={timestamp} />
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<ForecastFooter
|
||||||
|
stars={qualityindicators.stars}
|
||||||
|
platform={platform}
|
||||||
|
platformLabel={platformLabel || platform} // author || platformLabel,
|
||||||
|
numforecasts={qualityindicators.numforecasts}
|
||||||
|
qualityindicators={qualityindicators}
|
||||||
|
timestamp={timestamp}
|
||||||
|
showTimeStamp={showTimeStamp && displayTimestampAtBottom}
|
||||||
|
expandFooterToFullWidth={expandFooterToFullWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
38
src/web/display/DisplayForecasts.tsx
Normal file
38
src/web/display/DisplayForecasts.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { FrontendForecast } from "../platforms";
|
||||||
|
import { DisplayForecast } from "./DisplayForecast";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
results: FrontendForecast[];
|
||||||
|
numDisplay: number;
|
||||||
|
showIdToggle: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplayForecasts: React.FC<Props> = ({
|
||||||
|
results,
|
||||||
|
numDisplay,
|
||||||
|
showIdToggle,
|
||||||
|
}) => {
|
||||||
|
if (!results) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{results.slice(0, numDisplay).map((result) => (
|
||||||
|
/*let displayWithMetaculusCapture =
|
||||||
|
fuseSearchResult.item.platform == "Metaculus"
|
||||||
|
? metaculusEmbed(fuseSearchResult.item)
|
||||||
|
: displayForecast({ ...fuseSearchResult.item });
|
||||||
|
*/
|
||||||
|
<DisplayForecast
|
||||||
|
key={result.id}
|
||||||
|
forecast={result}
|
||||||
|
showTimeStamp={false}
|
||||||
|
expandFooterToFullWidth={false}
|
||||||
|
showIdToggle={showIdToggle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,7 +4,7 @@ import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
|
|
||||||
import { FrontendForecast } from "../platforms";
|
import { FrontendForecast } from "../platforms";
|
||||||
import { uploadToImgur } from "../worker/uploadToImgur";
|
import { uploadToImgur } from "../worker/uploadToImgur";
|
||||||
import { DisplayForecast } from "./displayForecasts";
|
import { DisplayForecast } from "./DisplayForecast";
|
||||||
|
|
||||||
function displayOneForecastInner(result: FrontendForecast, containerRef) {
|
function displayOneForecastInner(result: FrontendForecast, containerRef) {
|
||||||
return (
|
return (
|
||||||
|
@ -171,7 +171,7 @@ interface Props {
|
||||||
result: FrontendForecast;
|
result: FrontendForecast;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DisplayOneForecast: React.FC<Props> = ({ result }) => {
|
export const DisplayOneForecastForCapture: React.FC<Props> = ({ result }) => {
|
||||||
const [hasDisplayBeenCaptured, setHasDisplayBeenCaptured] = useState(false);
|
const [hasDisplayBeenCaptured, setHasDisplayBeenCaptured] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -248,8 +248,6 @@ const DisplayOneForecast: React.FC<Props> = ({ result }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DisplayOneForecast;
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/39501289/in-reactjs-how-to-copy-text-to-clipboard
|
// 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
|
// Note: https://stackoverflow.com/questions/66016033/can-no-longer-upload-images-to-imgur-from-localhost
|
||||||
// Use: http://imgurtester:3000/embed for testing.
|
// Use: http://imgurtester:3000/embed for testing.
|
5
src/web/display/InfoBox.tsx
Normal file
5
src/web/display/InfoBox.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const InfoBox: React.FC = ({ children }) => (
|
||||||
|
<p className="bg-gray-200 text-gray-700 py-2 px-4 border border-transparent text-center">
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
|
@ -67,7 +67,7 @@ class ErrorBoundary extends React.Component<
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main */
|
/* Main */
|
||||||
export default function Layout({ page, children }) {
|
export const Layout = ({ page, children }) => {
|
||||||
let lastUpdated = calculateLastUpdate();
|
let lastUpdated = calculateLastUpdate();
|
||||||
// The correct way to do this would be by passing a prop to Layout,
|
// The correct way to do this would be by passing a prop to Layout,
|
||||||
// and to get the last updating using server side props.
|
// and to get the last updating using server side props.
|
||||||
|
@ -150,4 +150,4 @@ export default function Layout({ page, children }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
7
src/web/display/LineHeader.tsx
Normal file
7
src/web/display/LineHeader.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const LineHeader: React.FC = ({ children }) => (
|
||||||
|
<h3 className="flex items-center justify-center w-full">
|
||||||
|
<span aria-hidden="true" className="flex-grow bg-gray-300 rounded h-0.5" />
|
||||||
|
<span className="mx-3 text-md font-medium text-center">{children}</span>
|
||||||
|
<span aria-hidden="true" className="flex-grow bg-gray-300 rounded h-0.5" />
|
||||||
|
</h3>
|
||||||
|
);
|
|
@ -1,6 +1,16 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function Form({ value, onChange, placeholder }) {
|
interface Props {
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
placeholder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QueryForm: React.FC<Props> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
}) => {
|
||||||
const handleInputChange = (event) => {
|
const handleInputChange = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onChange(event.target.value); // In this case, the query, e.g. "COVID.19"
|
onChange(event.target.value); // In this case, the query, e.g. "COVID.19"
|
||||||
|
@ -21,4 +31,4 @@ export default function Form({ value, onChange, placeholder }) {
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
};
|
|
@ -76,9 +76,19 @@ function Track({ source, target, getTrackProps }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: number;
|
||||||
|
onChange: (event: any) => void;
|
||||||
|
displayFunction: (value: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
/* Body */
|
/* Body */
|
||||||
// Two functions, essentially identical.
|
// Two functions, essentially identical.
|
||||||
export function SliderElement({ onChange, value, displayFunction }) {
|
export const SliderElement: React.FC<Props> = ({
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
displayFunction,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Slider
|
<Slider
|
||||||
rootStyle={
|
rootStyle={
|
||||||
|
@ -122,4 +132,4 @@ export function SliderElement({ onChange, value, displayFunction }) {
|
||||||
</Tracks>
|
</Tracks>
|
||||||
</Slider>
|
</Slider>
|
||||||
);
|
);
|
||||||
}
|
};
|
|
@ -1,86 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
let exampleInput = `{
|
|
||||||
"title": "Random example",
|
|
||||||
"description": "Just a random description of a random example",
|
|
||||||
"ids": [ "metaculus-372", "goodjudgmentopen-2244", "metaculus-7550", "kalshi-09d060ee-b184-4167-b86b-d773e56b4162", "wildeford-5d1a04e1a8", "metaculus-2817" ],
|
|
||||||
"creator": "Peter Parker"
|
|
||||||
}`;
|
|
||||||
|
|
||||||
export function DashboardCreator({ handleSubmit }) {
|
|
||||||
let [value, setValue] = useState(exampleInput);
|
|
||||||
const [displayingDoneMessage, setDisplayingDoneMessage] = useState(false);
|
|
||||||
const [displayingDoneMessageTimer, setDisplayingDoneMessageTimer] =
|
|
||||||
useState(null);
|
|
||||||
|
|
||||||
let handleChange = (event) => {
|
|
||||||
setValue(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
let handleSubmitInner = (event) => {
|
|
||||||
clearTimeout(displayingDoneMessageTimer);
|
|
||||||
event.preventDefault();
|
|
||||||
//console.log(event)
|
|
||||||
console.log("value@handleSubmitInner@DashboardCreator");
|
|
||||||
//console.log(typeof(value));
|
|
||||||
console.log(value);
|
|
||||||
try {
|
|
||||||
let newData = JSON.parse(value);
|
|
||||||
//console.log(typeof(newData))
|
|
||||||
//console.log(newData)
|
|
||||||
if (!newData || !newData.ids || newData.ids.length == 0) {
|
|
||||||
throw Error("Not enough objects");
|
|
||||||
} else {
|
|
||||||
handleSubmit(newData);
|
|
||||||
setDisplayingDoneMessage(true);
|
|
||||||
let timer = setTimeout(() => setDisplayingDoneMessage(false), 3000);
|
|
||||||
setDisplayingDoneMessageTimer(timer);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setDisplayingDoneMessage(false);
|
|
||||||
//alert(error)
|
|
||||||
//console.log(error)
|
|
||||||
let substituteText = `Error: ${error.message}
|
|
||||||
|
|
||||||
Try something like:
|
|
||||||
${exampleInput}
|
|
||||||
|
|
||||||
Your old input was: ${value}`;
|
|
||||||
setValue(substituteText);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmitInner} className="block place-centers">
|
|
||||||
<textarea
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
rows={8}
|
|
||||||
cols={50}
|
|
||||||
className=""
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<div className="grid grid-cols-3 text-center">
|
|
||||||
<button
|
|
||||||
className="block col-start-2 col-end-2 w-full 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 mt-5 p-10 text-center"
|
|
||||||
onClick={handleSubmitInner}
|
|
||||||
>
|
|
||||||
Create dashboard
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={
|
|
||||||
displayingDoneMessage
|
|
||||||
? "block col-start-2 col-end-2 bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-2 border border-blue-500 hover:border-transparent rounded mt-2 p-2 text-center "
|
|
||||||
: "hidden "
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Done!
|
|
||||||
</button>
|
|
||||||
<p className="block col-start-1 col-end-4 bg-gray-200 text-gray-700 py-2 px-4 border border-transparent mt-5 p-10 text-center mb-6">
|
|
||||||
You can find the necessary ids by toggling the advanced options in the
|
|
||||||
search, or by visiting{" "}
|
|
||||||
<a href="/api/all-forecasts">/api/all-forecasts</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,778 +0,0 @@
|
||||||
/* Imports */
|
|
||||||
import React from "react";
|
|
||||||
import { FaRegClipboard } from "react-icons/fa";
|
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
|
|
||||||
import { FrontendForecast } from "../platforms";
|
|
||||||
|
|
||||||
/* Definitions */
|
|
||||||
|
|
||||||
/* Support functions */
|
|
||||||
|
|
||||||
// Short utils
|
|
||||||
|
|
||||||
let truncateTextSimple = (length, text) => {
|
|
||||||
text.length > length
|
|
||||||
? !!text.slice
|
|
||||||
? text.slice(0, length) + "..."
|
|
||||||
: ""
|
|
||||||
: text;
|
|
||||||
};
|
|
||||||
|
|
||||||
let truncateText = (length, text) => {
|
|
||||||
if (!text) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (!!text && text.length <= length) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
let breakpoints = " .!?";
|
|
||||||
let lastLetter = null;
|
|
||||||
let lastIndex = null;
|
|
||||||
for (let index = length; index > 0; index--) {
|
|
||||||
let letter = text[index];
|
|
||||||
if (breakpoints.includes(letter)) {
|
|
||||||
lastLetter = letter;
|
|
||||||
lastIndex = index;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let truncatedText = !!text.slice
|
|
||||||
? text.slice(0, lastIndex) + (lastLetter != "." ? "..." : "..")
|
|
||||||
: "";
|
|
||||||
return truncatedText;
|
|
||||||
};
|
|
||||||
|
|
||||||
let formatProbability = (probability) => {
|
|
||||||
let percentage = probability * 100;
|
|
||||||
let percentageCapped =
|
|
||||||
percentage < 1
|
|
||||||
? "< 1%"
|
|
||||||
: percentage > 99
|
|
||||||
? "> 99%"
|
|
||||||
: percentage.toFixed(0) + "%";
|
|
||||||
return percentageCapped;
|
|
||||||
};
|
|
||||||
|
|
||||||
let formatNumber = (num) => {
|
|
||||||
if (Number(num) < 1000) {
|
|
||||||
return Number(num).toFixed(0);
|
|
||||||
} else if (num < 10000) {
|
|
||||||
return (Number(num) / 1000).toFixed(1) + "k";
|
|
||||||
} else {
|
|
||||||
return (Number(num) / 1000).toFixed(0) + "k";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let formatQualityIndicators = (qualityIndicators) => {
|
|
||||||
let newQualityIndicators = {};
|
|
||||||
for (let key in qualityIndicators) {
|
|
||||||
let newKey = formatQualityIndicator(key);
|
|
||||||
if (newKey) {
|
|
||||||
newQualityIndicators[newKey] = qualityIndicators[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newQualityIndicators;
|
|
||||||
};
|
|
||||||
|
|
||||||
// replaceAll polyfill
|
|
||||||
function escapeRegExp(string) {
|
|
||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceAll(originalString, pattern, substitute) {
|
|
||||||
return originalString.replace(
|
|
||||||
new RegExp(escapeRegExp(pattern), "g"),
|
|
||||||
substitute
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!String.prototype.replaceAll) {
|
|
||||||
String.prototype.replaceAll = function (pattern, substitute) {
|
|
||||||
let originalString = this;
|
|
||||||
|
|
||||||
// If a regex pattern
|
|
||||||
if (
|
|
||||||
Object.prototype.toString.call(pattern).toLowerCase() ===
|
|
||||||
"[object regexp]"
|
|
||||||
) {
|
|
||||||
return originalString.replace(pattern, substitute);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a string
|
|
||||||
return replaceAll(originalString, pattern, substitute);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let cleanText = (text) => {
|
|
||||||
// Note: should no longer be necessary
|
|
||||||
let textString = !!text ? text : "";
|
|
||||||
textString = textString
|
|
||||||
.replaceAll("] (", "](")
|
|
||||||
.replaceAll(") )", "))")
|
|
||||||
.replaceAll("( [", "([")
|
|
||||||
.replaceAll(") ,", "),")
|
|
||||||
.replaceAll("==", "") // Denotes a title in markdown
|
|
||||||
.replaceAll("Background\n", "")
|
|
||||||
.replaceAll("Context\n", "")
|
|
||||||
.replaceAll("--- \n", "- ")
|
|
||||||
.replaceAll(/\[(.*?)\]\(.*?\)/g, "$1");
|
|
||||||
textString = textString.slice(0, 1) == "=" ? textString.slice(1) : textString;
|
|
||||||
//console.log(textString)
|
|
||||||
return textString;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Faulty regex implementation
|
|
||||||
let cleanText = (text) => {
|
|
||||||
// Note: should no longer be necessary
|
|
||||||
let textString = !!text ? "" : text;
|
|
||||||
textString = textString
|
|
||||||
.replace(/\]\] \(/g, "](")
|
|
||||||
.replace(/\) \)/g, "))")
|
|
||||||
.replace(/\( \[/g, "([")
|
|
||||||
.replace(/\) ,/g, "),")
|
|
||||||
.replace(/==/g, "") // Denotes a title in markdown
|
|
||||||
.replace(/Background\n/g, "")
|
|
||||||
.replace(/Context\n/g, "")
|
|
||||||
.replace(/--- \n/g, "- ")
|
|
||||||
.replace(/\[(.*?)\]\(.*?\)/g, "$1");
|
|
||||||
|
|
||||||
textString = textString.slice(0, 1) == "=" && !!text.slice ? textString.slice(1) : textString;
|
|
||||||
//console.log(textString)
|
|
||||||
return textString;
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Database-like functions
|
|
||||||
export function getstars(numstars) {
|
|
||||||
let stars = "★★☆☆☆";
|
|
||||||
switch (numstars) {
|
|
||||||
case 0:
|
|
||||||
stars = "☆☆☆☆☆";
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
stars = "★☆☆☆☆";
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
stars = "★★☆☆☆";
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
stars = "★★★☆☆";
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
stars = "★★★★☆";
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
stars = "★★★★★";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
stars = "★★☆☆☆";
|
|
||||||
}
|
|
||||||
return stars;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStarsColor(numstars) {
|
|
||||||
let color = "text-yellow-400";
|
|
||||||
switch (numstars) {
|
|
||||||
case 0:
|
|
||||||
color = "text-red-400";
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
color = "text-red-400";
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
color = "text-orange-400";
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
color = "text-yellow-400";
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
color = "text-green-400";
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
color = "text-blue-400";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
color = "text-yellow-400";
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
let primaryForecastColor = (probability) => {
|
|
||||||
if (probability < 0.03) {
|
|
||||||
return "bg-red-600";
|
|
||||||
} else if (probability < 0.1) {
|
|
||||||
return "bg-red-600 opacity-80";
|
|
||||||
} else if (probability < 0.2) {
|
|
||||||
return "bg-red-600 opacity-70";
|
|
||||||
} else if (probability < 0.3) {
|
|
||||||
return "bg-red-600 opacity-60";
|
|
||||||
} else if (probability < 0.4) {
|
|
||||||
return "bg-red-600 opacity-50";
|
|
||||||
} else if (probability < 0.5) {
|
|
||||||
return "bg-gray-500";
|
|
||||||
} else if (probability < 0.6) {
|
|
||||||
return "bg-gray-500";
|
|
||||||
} else if (probability < 0.7) {
|
|
||||||
return "bg-green-600 opacity-50";
|
|
||||||
} else if (probability < 0.8) {
|
|
||||||
return "bg-green-600 opacity-60";
|
|
||||||
} else if (probability < 0.9) {
|
|
||||||
return "bg-green-600 opacity-70";
|
|
||||||
} else if (probability < 0.97) {
|
|
||||||
return "bg-green-600 opacity-80";
|
|
||||||
} else {
|
|
||||||
return "bg-green-600";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let textColor = (probability) => {
|
|
||||||
if (probability < 0.03) {
|
|
||||||
return "text-red-600";
|
|
||||||
} else if (probability < 0.1) {
|
|
||||||
return "text-red-600 opacity-80";
|
|
||||||
} else if (probability < 0.2) {
|
|
||||||
return "text-red-600 opacity-80";
|
|
||||||
} else if (probability < 0.3) {
|
|
||||||
return "text-red-600 opacity-70";
|
|
||||||
} else if (probability < 0.4) {
|
|
||||||
return "text-red-600 opacity-70";
|
|
||||||
} else if (probability < 0.5) {
|
|
||||||
return "text-gray-500";
|
|
||||||
} else if (probability < 0.6) {
|
|
||||||
return "text-gray-500";
|
|
||||||
} else if (probability < 0.7) {
|
|
||||||
return "text-green-600 opacity-70";
|
|
||||||
} else if (probability < 0.8) {
|
|
||||||
return "text-green-600 opacity-70";
|
|
||||||
} else if (probability < 0.9) {
|
|
||||||
return "text-green-600 opacity-80";
|
|
||||||
} else if (probability < 0.97) {
|
|
||||||
return "text-green-600 opacity-80";
|
|
||||||
} else {
|
|
||||||
return "text-green-600";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let primaryEstimateAsText = (probability) => {
|
|
||||||
if (probability < 0.03) {
|
|
||||||
return "Exceptionally unlikely";
|
|
||||||
} else if (probability < 0.1) {
|
|
||||||
return "Very unlikely";
|
|
||||||
} else if (probability < 0.4) {
|
|
||||||
return "Unlikely";
|
|
||||||
} else if (probability < 0.6) {
|
|
||||||
return "About Even";
|
|
||||||
} else if (probability < 0.9) {
|
|
||||||
return "Likely";
|
|
||||||
} else if (probability < 0.97) {
|
|
||||||
return "Very likely";
|
|
||||||
} else {
|
|
||||||
return "Virtually certain";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let textColorFromScore = (score) => {
|
|
||||||
if (score < 0.4) {
|
|
||||||
return ["text-gray-900", "text-gray-900"];
|
|
||||||
} else {
|
|
||||||
return ["text-gray-400", "text-gray-400"];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let opacityFromScore = (score) => {
|
|
||||||
if (score < 0.4) {
|
|
||||||
return "opacity-100";
|
|
||||||
} else {
|
|
||||||
return "opacity-50";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let formatQualityIndicator = (indicator) => {
|
|
||||||
let result;
|
|
||||||
switch (indicator) {
|
|
||||||
case "numforecasts":
|
|
||||||
result = null;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "stars":
|
|
||||||
result = null;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "volume":
|
|
||||||
result = "Volume";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "numforecasters":
|
|
||||||
result = "Forecasters";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "yes_bid":
|
|
||||||
result = null; // "Yes bid"
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "yes_ask":
|
|
||||||
result = null; // "Yes ask"
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "spread":
|
|
||||||
result = "Spread";
|
|
||||||
break;
|
|
||||||
case "shares_volume":
|
|
||||||
result = "Shares vol.";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "open_interest":
|
|
||||||
result = "Interest";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "resolution_data":
|
|
||||||
result = null;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "liquidity":
|
|
||||||
result = "Liquidity";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "tradevolume":
|
|
||||||
result = "Volume";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Logical checks
|
|
||||||
|
|
||||||
let checkIfDisplayTimeStampAtBottom = (qualityIndicators) => {
|
|
||||||
let indicators = Object.keys(qualityIndicators);
|
|
||||||
if (indicators.length == 1 && indicators[0] == "stars") {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let getCurrencySymbolIfNeeded = ({
|
|
||||||
indicator,
|
|
||||||
platform,
|
|
||||||
}: {
|
|
||||||
indicator: any;
|
|
||||||
platform: string;
|
|
||||||
}) => {
|
|
||||||
let indicatorsWhichNeedCurrencySymbol = ["Volume", "Interest", "Liquidity"];
|
|
||||||
let dollarPlatforms = ["predictit", "kalshi", "polymarket"];
|
|
||||||
if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) {
|
|
||||||
if (dollarPlatforms.includes(platform)) {
|
|
||||||
return "$";
|
|
||||||
} else {
|
|
||||||
return "£";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let getPercentageSymbolIfNeeded = ({ indicator, platform }) => {
|
|
||||||
let indicatorsWhichNeedPercentageSymbol = ["Spread"];
|
|
||||||
if (indicatorsWhichNeedPercentageSymbol.includes(indicator)) {
|
|
||||||
return "%";
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Display functions*/
|
|
||||||
|
|
||||||
// Auxiliary
|
|
||||||
|
|
||||||
let displayMarkdown = (description) => {
|
|
||||||
let formatted = truncateText(250, cleanText(description)); //, description)//
|
|
||||||
// overflow-hidden overflow-ellipsis h-24
|
|
||||||
return formatted === "" ? (
|
|
||||||
""
|
|
||||||
) : (
|
|
||||||
<div className="overflow-clip">
|
|
||||||
<ReactMarkdown linkTarget="_blank" className="font-normal">
|
|
||||||
{formatted}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let generateOptionRow = (option) => {
|
|
||||||
let chooseColor = (probability) => {
|
|
||||||
if (probability < 0.1) {
|
|
||||||
return "bg-blue-50 text-blue-500";
|
|
||||||
} else if (probability < 0.3) {
|
|
||||||
return "bg-blue-100 text-blue-600";
|
|
||||||
} else if (probability < 0.7) {
|
|
||||||
return "bg-blue-200 text-blue-700";
|
|
||||||
} else {
|
|
||||||
return "bg-blue-300 text-blue-800";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="items-center flex">
|
|
||||||
<div
|
|
||||||
className={`${chooseColor(
|
|
||||||
option.probability
|
|
||||||
)} w-14 flex-none rounded-md py-0.5 my-1 text-sm text-center`}
|
|
||||||
>
|
|
||||||
{formatProbability(option.probability)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-auto text-gray-700 pl-3 leading-snug text-sm">
|
|
||||||
{option.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let formatForecastOptions = (options) => {
|
|
||||||
let optionsSorted = options.sort((a, b) => b.probability - a.probability);
|
|
||||||
let optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options.
|
|
||||||
let result = optionsMax5.map((option) => generateOptionRow(option));
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
let showFirstQualityIndicator = ({
|
|
||||||
numforecasts,
|
|
||||||
timestamp,
|
|
||||||
showTimeStamp,
|
|
||||||
qualityindicators,
|
|
||||||
}) => {
|
|
||||||
if (!!numforecasts) {
|
|
||||||
return (
|
|
||||||
<div className="flex col-span-1 row-span-1">
|
|
||||||
{/*<span>{` ${numforecasts == 1 ? "Forecast" : "Forecasts:"}`}</span> */}
|
|
||||||
<span>{"Forecasts:"}</span>
|
|
||||||
<span className="font-bold">{Number(numforecasts).toFixed(0)}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
showTimeStamp &&
|
|
||||||
checkIfDisplayTimeStampAtBottom(qualityindicators)
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={`hidden sm:flex items-center justify-center text-gray-600 mt-2`}
|
|
||||||
>
|
|
||||||
<svg className="ml-4 mr-1 mt-1" height="10" width="16">
|
|
||||||
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
|
|
||||||
</svg>
|
|
||||||
{`Last updated: ${
|
|
||||||
timestamp && !!timestamp.slice ? timestamp.slice(0, 10) : "unknown"
|
|
||||||
}`}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayQualityIndicators: React.FC<{
|
|
||||||
numforecasts: number;
|
|
||||||
timestamp: number;
|
|
||||||
showTimeStamp: boolean;
|
|
||||||
qualityindicators: any;
|
|
||||||
platform: string; // id string - e.g. "goodjudgment", not "Good Judgment"
|
|
||||||
}> = ({
|
|
||||||
numforecasts,
|
|
||||||
timestamp,
|
|
||||||
showTimeStamp,
|
|
||||||
qualityindicators,
|
|
||||||
platform,
|
|
||||||
}) => {
|
|
||||||
// grid grid-cols-1
|
|
||||||
return (
|
|
||||||
<div className="text-sm">
|
|
||||||
{showFirstQualityIndicator({
|
|
||||||
numforecasts,
|
|
||||||
timestamp,
|
|
||||||
showTimeStamp,
|
|
||||||
qualityindicators,
|
|
||||||
})}
|
|
||||||
{Object.entries(formatQualityIndicators(qualityindicators)).map(
|
|
||||||
(entry, i) => {
|
|
||||||
return (
|
|
||||||
<div className="col-span-1 row-span-1">
|
|
||||||
<span>{`${entry[0]}:`}</span>
|
|
||||||
<span className="font-bold">
|
|
||||||
{`${getCurrencySymbolIfNeeded({
|
|
||||||
indicator: entry[0],
|
|
||||||
platform,
|
|
||||||
})}${formatNumber(entry[1])}${getPercentageSymbolIfNeeded({
|
|
||||||
indicator: entry[0],
|
|
||||||
platform,
|
|
||||||
})}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main display functions
|
|
||||||
|
|
||||||
let forecastFooter = ({
|
|
||||||
stars,
|
|
||||||
platform,
|
|
||||||
platformLabel,
|
|
||||||
numforecasts,
|
|
||||||
qualityindicators,
|
|
||||||
timestamp,
|
|
||||||
showTimeStamp,
|
|
||||||
expandFooterToFullWidth,
|
|
||||||
}) => {
|
|
||||||
// I experimented with justify-evenly, justify-around, etc., here: https://tailwindcss.com/docs/justify-content
|
|
||||||
// I came to the conclusion that as long as the description isn't justified too, aligning the footer symmetrically doesn't make sense
|
|
||||||
// because the contrast is jarring.
|
|
||||||
let debuggingWithBackground = false;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`grid grid-cols-3 ${
|
|
||||||
expandFooterToFullWidth ? "justify-between" : ""
|
|
||||||
} text-gray-500 mb-2 mt-1`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`self-center col-span-1 ${getStarsColor(stars)} ${
|
|
||||||
debuggingWithBackground ? "bg-red-200" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getstars(stars)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
expandFooterToFullWidth ? "place-self-center" : "self-center"
|
|
||||||
} col-span-1 font-bold ${debuggingWithBackground ? "bg-red-100" : ""}`}
|
|
||||||
>
|
|
||||||
{platformLabel
|
|
||||||
.replace("Good Judgment Open", "GJOpen")
|
|
||||||
.replace(/ /g, "\u00a0")}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
expandFooterToFullWidth
|
|
||||||
? "justify-self-end mr-4"
|
|
||||||
: "justify-self-center"
|
|
||||||
} col-span-1 ${debuggingWithBackground ? "bg-red-100" : ""}`}
|
|
||||||
>
|
|
||||||
{displayQualityIndicators({
|
|
||||||
numforecasts,
|
|
||||||
timestamp,
|
|
||||||
showTimeStamp,
|
|
||||||
qualityindicators,
|
|
||||||
platform,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Body */
|
|
||||||
|
|
||||||
interface SingleProps {
|
|
||||||
forecast: FrontendForecast;
|
|
||||||
showTimeStamp: boolean;
|
|
||||||
expandFooterToFullWidth: boolean;
|
|
||||||
showIdToggle?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DisplayForecast: React.FC<SingleProps> = ({
|
|
||||||
forecast: {
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
url,
|
|
||||||
platform,
|
|
||||||
platformLabel,
|
|
||||||
description,
|
|
||||||
options,
|
|
||||||
qualityindicators,
|
|
||||||
timestamp,
|
|
||||||
visualization,
|
|
||||||
},
|
|
||||||
showTimeStamp,
|
|
||||||
expandFooterToFullWidth,
|
|
||||||
showIdToggle,
|
|
||||||
}) => {
|
|
||||||
// const [isJustCopiedSignalVisible, setIsJustCopiedSignalVisible] = useState(false)
|
|
||||||
const isJustCopiedSignalVisible = false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={`displayForecast-${id}`}
|
|
||||||
href={url}
|
|
||||||
className="hover:bg-gray-100 hover:no-underline cursor-pointer flex flex-col px-4 py-3 bg-white rounded-md shadow place-content-stretch flex-grow no-underline"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<div className="flex-grow">
|
|
||||||
<div
|
|
||||||
className={`text-gray-800 ${opacityFromScore(
|
|
||||||
0
|
|
||||||
)} text-lg mb-2 font-medium justify-self-start`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
showIdToggle ? "" : "hidden"
|
|
||||||
} flex items-center justify-center mt-2 mb-3 text-sm bg-transparent py-4 px-4 border rounded mt-5 p-10 text-center mb-2 mr-10 ml-10 hover:bg-blue-300 text-blue-400 text-blue-700 ${
|
|
||||||
isJustCopiedSignalVisible
|
|
||||||
? " hover:text-white hover:border-blue-700 border-blue-400 hover:bg-blue-700"
|
|
||||||
: "border-blue-400 hover:text-white hover:border-transparent"
|
|
||||||
}`}
|
|
||||||
id="toggle"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
navigator.clipboard.writeText(`"${id}"`);
|
|
||||||
// setIsJustCopiedSignalVisible(true);
|
|
||||||
// setTimeout(() => setIsJustCopiedSignalVisible(false), 1000);
|
|
||||||
// This is just personal preference.
|
|
||||||
// I prefer to not show the whole text area selected.
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className={``}>{`[${id}]`}</span>
|
|
||||||
<FaRegClipboard className={`ml-3`} />
|
|
||||||
</div>
|
|
||||||
{title.replace("</a>", "")}
|
|
||||||
</div>
|
|
||||||
{options.length == 2 &&
|
|
||||||
(options[0].name == "Yes" || options[0].name == "No") && (
|
|
||||||
<div>
|
|
||||||
<div className="grid mb-5 mt-4 mb-5 grid-cols-1 sm:grid-rows-1">
|
|
||||||
<div className="flex-grow col-span-1 w-full items-center justify-center">
|
|
||||||
<span
|
|
||||||
className={`${primaryForecastColor(
|
|
||||||
options[0].probability
|
|
||||||
)} text-white w-16 rounded-md px-1.5 py-0.5 font-bold `}
|
|
||||||
>
|
|
||||||
{formatProbability(options[0].probability)}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={`${textColor(
|
|
||||||
options[0].probability
|
|
||||||
)} ml-2 text-gray-500 inline-block`}
|
|
||||||
>
|
|
||||||
{primaryEstimateAsText(options[0].probability)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`hidden sm:${
|
|
||||||
showTimeStamp &&
|
|
||||||
!checkIfDisplayTimeStampAtBottom(qualityindicators)
|
|
||||||
? "flex"
|
|
||||||
: "hidden"
|
|
||||||
} ${opacityFromScore(
|
|
||||||
0
|
|
||||||
)} row-end-2 col-start-2 col-end-2 row-start-1 row-end-1 col-span-1 items-center justify-center text-gray-600 ml-3 mr-2 `}
|
|
||||||
>
|
|
||||||
<svg className="mt-1" height="10" width="16">
|
|
||||||
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
|
|
||||||
</svg>
|
|
||||||
{`Last updated: ${
|
|
||||||
timestamp ? timestamp.slice(0, 10) : "unknown"
|
|
||||||
}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(options.length != 2 ||
|
|
||||||
(options[0].name != "Yes" && options[0].name != "No")) && (
|
|
||||||
<>
|
|
||||||
<div className={`mb-2 mt-2 ${opacityFromScore(0)}`}>
|
|
||||||
{formatForecastOptions(options)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`hidden sm:${
|
|
||||||
showTimeStamp &&
|
|
||||||
!checkIfDisplayTimeStampAtBottom(qualityindicators)
|
|
||||||
? "flex"
|
|
||||||
: "hidden"
|
|
||||||
} ${opacityFromScore(
|
|
||||||
0
|
|
||||||
)} col-start-2 col-end-2 row-start-1 row-end-1 text-gray-600 mt-3 mb-3`}
|
|
||||||
>
|
|
||||||
<svg className="ml-6 mr-1 mt-2" height="10" width="16">
|
|
||||||
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
|
|
||||||
</svg>
|
|
||||||
{`Last updated: ${
|
|
||||||
timestamp ? timestamp.slice(0, 10) : "unknown"
|
|
||||||
}`}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{platform !== "guesstimate" && options.length < 3 && (
|
|
||||||
<div className={`text-gray-500 ${opacityFromScore(0)} mt-4`}>
|
|
||||||
{displayMarkdown(description)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{platform === "guesstimate" && (
|
|
||||||
<img
|
|
||||||
className="rounded-sm mb-1"
|
|
||||||
src={visualization}
|
|
||||||
alt="Guesstimate Screenshot"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`flex sm:hidden ${
|
|
||||||
!showTimeStamp ? "hidden" : ""
|
|
||||||
} items-center justify-center mt-2 mb-4 text-gray-600`}
|
|
||||||
>
|
|
||||||
{/* This one is exclusively for mobile*/}
|
|
||||||
<svg className="" height="10" width="16">
|
|
||||||
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
|
|
||||||
</svg>
|
|
||||||
{`Last updated: ${timestamp ? timestamp.slice(0, 10) : "unknown"}`}
|
|
||||||
</div>
|
|
||||||
<div className={`${opacityFromScore(0)} w-full`}>
|
|
||||||
{forecastFooter({
|
|
||||||
stars: qualityindicators.stars,
|
|
||||||
platform: platform,
|
|
||||||
platformLabel: platformLabel || platform, // author || platformLabel,
|
|
||||||
numforecasts: qualityindicators.numforecasts,
|
|
||||||
qualityindicators,
|
|
||||||
timestamp,
|
|
||||||
showTimeStamp,
|
|
||||||
expandFooterToFullWidth,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
results: FrontendForecast[];
|
|
||||||
numDisplay: number;
|
|
||||||
showIdToggle: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplayForecasts: React.FC<Props> = ({
|
|
||||||
results,
|
|
||||||
numDisplay,
|
|
||||||
showIdToggle,
|
|
||||||
}) => {
|
|
||||||
if (!results) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{results.slice(0, numDisplay).map((result) => (
|
|
||||||
/*let displayWithMetaculusCapture =
|
|
||||||
fuseSearchResult.item.platform == "Metaculus"
|
|
||||||
? metaculusEmbed(fuseSearchResult.item)
|
|
||||||
: displayForecast({ ...fuseSearchResult.item });
|
|
||||||
*/
|
|
||||||
<DisplayForecast
|
|
||||||
forecast={result}
|
|
||||||
showTimeStamp={false}
|
|
||||||
expandFooterToFullWidth={false}
|
|
||||||
showIdToggle={showIdToggle}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DisplayForecasts;
|
|
|
@ -1,5 +1,5 @@
|
||||||
import displayForecasts from "./displayForecasts";
|
import { DisplayForecasts } from "./DisplayForecasts";
|
||||||
import displayOneForecast from "./displayOneForecastForCapture";
|
import { DisplayOneForecastForCapture } from "./DisplayOneForecastForCapture";
|
||||||
|
|
||||||
export function displayForecastsWrapperForSearch({
|
export function displayForecastsWrapperForSearch({
|
||||||
results,
|
results,
|
||||||
|
@ -8,7 +8,11 @@ export function displayForecastsWrapperForSearch({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{displayForecasts({ results: results || [], numDisplay, showIdToggle })}
|
<DisplayForecasts
|
||||||
|
results={results || []}
|
||||||
|
numDisplay={numDisplay}
|
||||||
|
showIdToggle={showIdToggle}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,9 +23,9 @@ export function displayForecastsWrapperForCapture({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 w-full justify-center">
|
<div className="grid grid-cols-1 w-full justify-center">
|
||||||
{displayOneForecast({
|
<DisplayOneForecastForCapture
|
||||||
result: results[whichResultToDisplayAndCapture],
|
result={results[whichResultToDisplayAndCapture]}
|
||||||
})}
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
15
src/web/hooks.ts
Normal file
15
src/web/hooks.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import React, { DependencyList, EffectCallback, useEffect } from "react";
|
||||||
|
|
||||||
|
export const useNoInitialEffect = (
|
||||||
|
effect: EffectCallback,
|
||||||
|
deps: DependencyList
|
||||||
|
) => {
|
||||||
|
const initial = React.useRef(true);
|
||||||
|
useEffect(() => {
|
||||||
|
if (initial.current) {
|
||||||
|
initial.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return effect();
|
||||||
|
}, deps);
|
||||||
|
};
|
|
@ -1,25 +1,15 @@
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { DependencyList, EffectCallback, Fragment, useEffect, useState } from "react";
|
import React, { Fragment, useState } from "react";
|
||||||
|
|
||||||
import ButtonsForStars from "../display/buttonsForStars";
|
import { ButtonsForStars } from "../display/ButtonsForStars";
|
||||||
import Form from "../display/form";
|
import { MultiSelectPlatform } from "../display/MultiSelectPlatform";
|
||||||
import { MultiSelectPlatform } from "../display/multiSelectPlatforms";
|
import { QueryForm } from "../display/QueryForm";
|
||||||
import { SliderElement } from "../display/slider";
|
import { SliderElement } from "../display/SliderElement";
|
||||||
|
import { useNoInitialEffect } from "../hooks";
|
||||||
import { FrontendForecast } from "../platforms";
|
import { FrontendForecast } from "../platforms";
|
||||||
import searchAccordingToQueryData from "../worker/searchAccordingToQueryData";
|
import searchAccordingToQueryData from "../worker/searchAccordingToQueryData";
|
||||||
import { Props as AnySearchPageProps, QueryParameters } from "./anySearchPage";
|
import { Props as AnySearchPageProps, QueryParameters } from "./anySearchPage";
|
||||||
|
|
||||||
const useNoInitialEffect = (effect: EffectCallback, deps: DependencyList) => {
|
|
||||||
const initial = React.useRef(true);
|
|
||||||
useEffect(() => {
|
|
||||||
if (initial.current) {
|
|
||||||
initial.current = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return effect();
|
|
||||||
}, deps);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props extends AnySearchPageProps {
|
interface Props extends AnySearchPageProps {
|
||||||
hasSearchbar: boolean;
|
hasSearchbar: boolean;
|
||||||
hasCapture: boolean;
|
hasCapture: boolean;
|
||||||
|
@ -76,7 +66,7 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
numDisplay,
|
numDisplay,
|
||||||
};
|
};
|
||||||
|
|
||||||
let filterManually = (
|
const filterManually = (
|
||||||
queryData: QueryParameters,
|
queryData: QueryParameters,
|
||||||
results: FrontendForecast[]
|
results: FrontendForecast[]
|
||||||
) => {
|
) => {
|
||||||
|
@ -102,7 +92,7 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
const queryIsEmpty =
|
const queryIsEmpty =
|
||||||
!queryData || queryData.query == "" || queryData.query == undefined;
|
!queryData || queryData.query == "" || queryData.query == undefined;
|
||||||
|
|
||||||
let results = queryIsEmpty
|
const results = queryIsEmpty
|
||||||
? filterManually(queryData, defaultResults)
|
? filterManually(queryData, defaultResults)
|
||||||
: await searchAccordingToQueryData(queryData, numDisplay);
|
: await searchAccordingToQueryData(queryData, numDisplay);
|
||||||
|
|
||||||
|
@ -110,8 +100,8 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// I don't want the function which display forecasts (displayForecasts) to change with a change in queryParameters. But I want it to have access to the queryParameters, and in particular access to queryParameters.numDisplay. Hence why this function lives inside Home.
|
// I don't want the function which display forecasts (displayForecasts) to change with a change in queryParameters. But I want it to have access to the queryParameters, and in particular access to queryParameters.numDisplay. Hence why this function lives inside Home.
|
||||||
let getInfoToDisplayForecastsFunction = () => {
|
const getInfoToDisplayForecastsFunction = () => {
|
||||||
let numDisplayRounded =
|
const numDisplayRounded =
|
||||||
numDisplay % 3 != 0
|
numDisplay % 3 != 0
|
||||||
? numDisplay + (3 - (Math.round(numDisplay) % 3))
|
? numDisplay + (3 - (Math.round(numDisplay) % 3))
|
||||||
: numDisplay;
|
: numDisplay;
|
||||||
|
@ -156,7 +146,7 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
|
|
||||||
useNoInitialEffect(() => {
|
useNoInitialEffect(() => {
|
||||||
setResults([]);
|
setResults([]);
|
||||||
let newTimeoutId = setTimeout(() => {
|
const newTimeoutId = setTimeout(() => {
|
||||||
updateRoute();
|
updateRoute();
|
||||||
executeSearchOrAnswerWithDefaultResults();
|
executeSearchOrAnswerWithDefaultResults();
|
||||||
}, 500);
|
}, 500);
|
||||||
|
@ -170,7 +160,7 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
/* State controllers */
|
/* State controllers */
|
||||||
|
|
||||||
/* Change the stars threshold */
|
/* Change the stars threshold */
|
||||||
let onChangeStars = (value: number) => {
|
const onChangeStars = (value: number) => {
|
||||||
setQueryParameters({
|
setQueryParameters({
|
||||||
...queryParameters,
|
...queryParameters,
|
||||||
starsThreshold: value,
|
starsThreshold: value,
|
||||||
|
@ -178,7 +168,7 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Change the number of elements to display */
|
/* Change the number of elements to display */
|
||||||
let displayFunctionNumDisplaySlider = (value) => {
|
const displayFunctionNumDisplaySlider = (value: number) => {
|
||||||
return (
|
return (
|
||||||
"Show " +
|
"Show " +
|
||||||
Math.round(value) +
|
Math.round(value) +
|
||||||
|
@ -186,16 +176,16 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
(Math.round(value) === 1 ? "" : "s")
|
(Math.round(value) === 1 ? "" : "s")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
let onChangeSliderForNumDisplay = (event) => {
|
const onChangeSliderForNumDisplay = (event) => {
|
||||||
setNumDisplay(Math.round(event[0]));
|
setNumDisplay(Math.round(event[0]));
|
||||||
setForceSearch(forceSearch + 1); // FIXME - force new search iff numDisplay is greater than last search limit
|
setForceSearch(forceSearch + 1); // FIXME - force new search iff numDisplay is greater than last search limit
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Change the forecast threshold */
|
/* Change the forecast threshold */
|
||||||
let displayFunctionNumForecasts = (value: number) => {
|
const displayFunctionNumForecasts = (value: number) => {
|
||||||
return "# Forecasts > " + Math.round(value);
|
return "# Forecasts > " + Math.round(value);
|
||||||
};
|
};
|
||||||
let onChangeSliderForNumForecasts = (event) => {
|
const onChangeSliderForNumForecasts = (event) => {
|
||||||
setQueryParameters({
|
setQueryParameters({
|
||||||
...queryParameters,
|
...queryParameters,
|
||||||
forecastsThreshold: Math.round(event[0]),
|
forecastsThreshold: Math.round(event[0]),
|
||||||
|
@ -203,7 +193,7 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Change on the search bar */
|
/* Change on the search bar */
|
||||||
let onChangeSearchBar = (value: string) => {
|
const onChangeSearchBar = (value: string) => {
|
||||||
setQueryParameters({
|
setQueryParameters({
|
||||||
...queryParameters,
|
...queryParameters,
|
||||||
query: value,
|
query: value,
|
||||||
|
@ -211,7 +201,7 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Change selected platforms */
|
/* Change selected platforms */
|
||||||
let onChangeSelectedPlatforms = (value) => {
|
const onChangeSelectedPlatforms = (value) => {
|
||||||
setQueryParameters({
|
setQueryParameters({
|
||||||
...queryParameters,
|
...queryParameters,
|
||||||
forecastingPlatforms: value,
|
forecastingPlatforms: value,
|
||||||
|
@ -219,18 +209,18 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
// Change show id
|
// Change show id
|
||||||
let onChangeShowId = () => {
|
const onChangeShowId = () => {
|
||||||
setShowIdToggle(!showIdToggle);
|
setShowIdToggle(!showIdToggle);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Capture functionality
|
// Capture functionality
|
||||||
let onClickBack = () => {
|
const onClickBack = () => {
|
||||||
let decreaseUntil0 = (num: number) => (num - 1 > 0 ? num - 1 : 0);
|
const decreaseUntil0 = (num: number) => (num - 1 > 0 ? num - 1 : 0);
|
||||||
setWhichResultToDisplayAndCapture(
|
setWhichResultToDisplayAndCapture(
|
||||||
decreaseUntil0(whichResultToDisplayAndCapture)
|
decreaseUntil0(whichResultToDisplayAndCapture)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
let onClickForward = (whichResultToDisplayAndCapture: number) => {
|
const onClickForward = (whichResultToDisplayAndCapture: number) => {
|
||||||
setWhichResultToDisplayAndCapture(whichResultToDisplayAndCapture + 1);
|
setWhichResultToDisplayAndCapture(whichResultToDisplayAndCapture + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -240,7 +230,7 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
<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 ? (
|
{hasSearchbar ? (
|
||||||
<div className="w-10/12 mb-2">
|
<div className="w-10/12 mb-2">
|
||||||
<Form
|
<QueryForm
|
||||||
value={queryParameters.query}
|
value={queryParameters.query}
|
||||||
onChange={onChangeSearchBar}
|
onChange={onChangeSearchBar}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|
|
@ -58,10 +58,6 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
).split("|");
|
).split("|");
|
||||||
}
|
}
|
||||||
|
|
||||||
const platformNameToLabel = Object.fromEntries(
|
|
||||||
platforms.map((platform) => [platform.name, platform.label])
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultNumDisplay = 21;
|
const defaultNumDisplay = 21;
|
||||||
const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay;
|
const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay;
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { DashboardItem } from "../../backend/dashboards";
|
import { DashboardItem } from "../../backend/dashboards";
|
||||||
import { Forecast } from "../../backend/platforms";
|
import { Forecast, getPlatformsConfig } from "../../backend/platforms";
|
||||||
|
import { addLabelsToForecasts, FrontendForecast } from "../platforms";
|
||||||
|
|
||||||
export async function getDashboardForecastsByDashboardId({
|
export async function getDashboardForecastsByDashboardId({
|
||||||
dashboardId,
|
dashboardId,
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
dashboardForecasts: Forecast[];
|
dashboardForecasts: FrontendForecast[];
|
||||||
dashboardItem: DashboardItem;
|
dashboardItem: DashboardItem;
|
||||||
}> {
|
}> {
|
||||||
console.log("getDashboardForecastsByDashboardId: ");
|
console.log("getDashboardForecastsByDashboardId: ");
|
||||||
let dashboardContents: Forecast[] = [];
|
let dashboardForecasts: Forecast[] = [];
|
||||||
let dashboardItem: DashboardItem | any = null;
|
let dashboardItem: DashboardItem | null = null;
|
||||||
try {
|
try {
|
||||||
let { data } = await axios({
|
const { data } = await axios({
|
||||||
url: `${process.env.NEXT_PUBLIC_SITE_URL}/api/dashboard-by-id`,
|
url: `${process.env.NEXT_PUBLIC_SITE_URL}/api/dashboard-by-id`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: {
|
data: {
|
||||||
|
@ -21,13 +22,19 @@ export async function getDashboardForecastsByDashboardId({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
console.log(data);
|
console.log(data);
|
||||||
dashboardContents = data.dashboardContents;
|
|
||||||
|
dashboardForecasts = data.dashboardContents;
|
||||||
dashboardItem = data.dashboardItem as DashboardItem;
|
dashboardItem = data.dashboardItem as DashboardItem;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
const labeledDashboardForecasts = addLabelsToForecasts(
|
||||||
|
dashboardForecasts,
|
||||||
|
getPlatformsConfig({ withGuesstimate: false })
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dashboardForecasts: dashboardContents,
|
dashboardForecasts: labeledDashboardForecasts,
|
||||||
dashboardItem,
|
dashboardItem,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user