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 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).
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import { NextPage } from "next"; | |||
| import React from "react"; | ||||
| 
 | ||||
| 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 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 { displayForecastsWrapperForSearch } from "../web/display/displayForecastsWrappers"; | ||||
| import Layout from "../web/display/layout"; | ||||
| import { Layout } from "../web/display/Layout"; | ||||
| import { Props } from "../web/search/anySearchPage"; | ||||
| import CommonDisplay from "../web/search/CommonDisplay"; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { NextPage } from "next"; | ||||
| import React, { useEffect } from "react"; | ||||
| 
 | ||||
| function Recursion() { | ||||
| const Recursion: NextPage = () => { | ||||
|   useEffect(() => { | ||||
|     if (typeof window !== "undefined") { | ||||
|       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> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| }; | ||||
| 
 | ||||
| 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 { platforms } from "../backend/platforms"; | ||||
| import { DisplayForecast } from "../web/display/displayForecasts"; | ||||
| import { DisplayForecast } from "../web/display/DisplayForecast"; | ||||
| import { FrontendForecast } from "../web/platforms"; | ||||
| import searchAccordingToQueryData from "../web/worker/searchAccordingToQueryData"; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,85 +1,76 @@ | |||
| import Link from "next/link"; | ||||
| 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 */ | ||||
| function displayTool({ | ||||
|   sameWebpage, | ||||
|   title, | ||||
|   description, | ||||
|   link, | ||||
|   url, | ||||
|   img, | ||||
|   i, | ||||
| }: any) { | ||||
|   switch (sameWebpage) { | ||||
|     case true: | ||||
|       return ( | ||||
|         <Link href={link} passHref key={`tool-${i}`}> | ||||
|           <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"> | ||||
|             <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> | ||||
|       ); | ||||
|       break; | ||||
|     default: | ||||
|       return ( | ||||
|         <a | ||||
|           href={url} | ||||
|           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> | ||||
|       ); | ||||
|       break; | ||||
| const ToolCard: React.FC<Tool> = (tool) => { | ||||
|   const inner = ( | ||||
|     <Card> | ||||
|       <div className="grid content-start gap-3"> | ||||
|         <div className="text-gray-800 text-lg font-medium">{tool.title}</div> | ||||
|         <div className="text-gray-500">{tool.description}</div> | ||||
|         {tool.img && <img src={tool.img} className="text-gray-500" />} | ||||
|       </div> | ||||
|     </Card> | ||||
|   ); | ||||
| 
 | ||||
|   if ("innerLink" in tool) { | ||||
|     return ( | ||||
|       <Link href={tool.innerLink} passHref> | ||||
|         <a className="text‑inherit no-underline">{inner}</a> | ||||
|       </Link> | ||||
|     ); | ||||
|   } else if ("externalLink" in tool) { | ||||
|     return ( | ||||
|       <a href={tool.externalLink} className="text‑inherit no-underline"> | ||||
|         {inner} | ||||
|       </a> | ||||
|     ); | ||||
|   } else { | ||||
|     return inner; | ||||
|   } | ||||
| } | ||||
| }; | ||||
| 
 | ||||
| export default function Tools({ lastUpdated }) { | ||||
|   let tools = [ | ||||
|   let tools: Tool[] = [ | ||||
|     { | ||||
|       title: "Search", | ||||
|       description: "Find forecasting questions on many platforms", | ||||
|       link: "/", | ||||
|       sameWebpage: true, | ||||
|       description: "Find forecasting questions on many platforms.", | ||||
|       innerLink: "/", | ||||
|       img: "https://i.imgur.com/Q94gVqG.png", | ||||
|     }, | ||||
|     { | ||||
|       title: "[Beta] Present", | ||||
|       description: "Present forecasts in dashboards.", | ||||
|       sameWebpage: true, | ||||
|       link: "/dashboards", | ||||
|       innerLink: "/dashboards", | ||||
|       img: "https://i.imgur.com/x8qkuHQ.png", | ||||
|     }, | ||||
|     { | ||||
|       title: "Capture", | ||||
|       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.", | ||||
|       link: "/capture", | ||||
|       sameWebpage: true, | ||||
|       innerLink: "/capture", | ||||
|       img: "https://i.imgur.com/EXkFBzz.png", | ||||
|     }, | ||||
|     { | ||||
|       title: "Summon", | ||||
|       description: | ||||
|         "Summon metaforecast on Twitter by mentioning @metaforecast, or on Discord by using Fletcher and !metaforecast, followed by search terms", | ||||
|       url: "https://twitter.com/metaforecast", | ||||
|         "Summon metaforecast on Twitter by mentioning @metaforecast, or on Discord by using Fletcher and !metaforecast, followed by search terms.", | ||||
|       externalLink: "https://twitter.com/metaforecast", | ||||
|       img: "https://i.imgur.com/BQ4Zzjw.png", | ||||
|     }, | ||||
|     { | ||||
|  | @ -87,7 +78,6 @@ export default function Tools({ lastUpdated }) { | |||
|       description: | ||||
|         "Interact with metaforecast's API and fetch forecasts for your application. Currently possible but documentation is poor, get in touch.", | ||||
|     }, | ||||
| 
 | ||||
|     { | ||||
|       title: "[Upcoming] Record", | ||||
|       description: "Save your forecasts or bets.", | ||||
|  | @ -95,8 +85,10 @@ export default function Tools({ lastUpdated }) { | |||
|   ]; | ||||
|   return ( | ||||
|     <Layout page="tools"> | ||||
|       <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4 mb-8"> | ||||
|         {tools.map((tool, i) => displayTool({ ...tool, i }))} | ||||
|       <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) => ( | ||||
|           <ToolCard {...tool} key={`tool-${i}`} /> | ||||
|         ))} | ||||
|       </div> | ||||
|     </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; | ||||
| } | ||||
| 
 | ||||
| const ButtonsForStars: React.FC<Props> = ({ onChange, value }) => { | ||||
| export const ButtonsForStars: React.FC<Props> = ({ onChange, value }) => { | ||||
|   const onChangeInner = (buttonPressed: number) => { | ||||
|     onChange(buttonPressed); | ||||
|   }; | ||||
|   let setStyle = (buttonNumber: number) => | ||||
|   const setStyle = (buttonNumber: number) => | ||||
|     `flex row-span-1 col-start-${buttonNumber + 1} col-end-${ | ||||
|       buttonNumber + 2 | ||||
|     } items-center justify-center text-center${ | ||||
|  | @ -37,5 +37,3 @@ const ButtonsForStars: React.FC<Props> = ({ onChange, value }) => { | |||
|     </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 { uploadToImgur } from "../worker/uploadToImgur"; | ||||
| import { DisplayForecast } from "./displayForecasts"; | ||||
| import { DisplayForecast } from "./DisplayForecast"; | ||||
| 
 | ||||
| function displayOneForecastInner(result: FrontendForecast, containerRef) { | ||||
|   return ( | ||||
|  | @ -171,7 +171,7 @@ interface Props { | |||
|   result: FrontendForecast; | ||||
| } | ||||
| 
 | ||||
| const DisplayOneForecast: React.FC<Props> = ({ result }) => { | ||||
| export const DisplayOneForecastForCapture: React.FC<Props> = ({ result }) => { | ||||
|   const [hasDisplayBeenCaptured, setHasDisplayBeenCaptured] = useState(false); | ||||
| 
 | ||||
|   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
 | ||||
| // Note: https://stackoverflow.com/questions/66016033/can-no-longer-upload-images-to-imgur-from-localhost
 | ||||
| // 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 */ | ||||
| export default function Layout({ page, children }) { | ||||
| export const Layout = ({ page, children }) => { | ||||
|   let lastUpdated = calculateLastUpdate(); | ||||
|   // The correct way to do this would be by passing a prop to Layout,
 | ||||
|   // and to get the last updating using server side props.
 | ||||
|  | @ -150,4 +150,4 @@ export default function Layout({ page, children }) { | |||
|       </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"; | ||||
| 
 | ||||
| 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) => { | ||||
|     event.preventDefault(); | ||||
|     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> | ||||
|   ); | ||||
| } | ||||
| }; | ||||
|  | @ -76,9 +76,19 @@ function Track({ source, target, getTrackProps }) { | |||
|   ); | ||||
| } | ||||
| 
 | ||||
| interface Props { | ||||
|   value: number; | ||||
|   onChange: (event: any) => void; | ||||
|   displayFunction: (value: number) => string; | ||||
| } | ||||
| 
 | ||||
| /* Body */ | ||||
| // Two functions, essentially identical.
 | ||||
| export function SliderElement({ onChange, value, displayFunction }) { | ||||
| export const SliderElement: React.FC<Props> = ({ | ||||
|   onChange, | ||||
|   value, | ||||
|   displayFunction, | ||||
| }) => { | ||||
|   return ( | ||||
|     <Slider | ||||
|       rootStyle={ | ||||
|  | @ -122,4 +132,4 @@ export function SliderElement({ onChange, value, displayFunction }) { | |||
|       </Tracks> | ||||
|     </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 displayOneForecast from "./displayOneForecastForCapture"; | ||||
| import { DisplayForecasts } from "./DisplayForecasts"; | ||||
| import { DisplayOneForecastForCapture } from "./DisplayOneForecastForCapture"; | ||||
| 
 | ||||
| export function displayForecastsWrapperForSearch({ | ||||
|   results, | ||||
|  | @ -8,7 +8,11 @@ export function displayForecastsWrapperForSearch({ | |||
| }) { | ||||
|   return ( | ||||
|     <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> | ||||
|   ); | ||||
| } | ||||
|  | @ -19,9 +23,9 @@ export function displayForecastsWrapperForCapture({ | |||
| }) { | ||||
|   return ( | ||||
|     <div className="grid grid-cols-1 w-full justify-center"> | ||||
|       {displayOneForecast({ | ||||
|         result: results[whichResultToDisplayAndCapture], | ||||
|       })} | ||||
|       <DisplayOneForecastForCapture | ||||
|         result={results[whichResultToDisplayAndCapture]} | ||||
|       /> | ||||
|     </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 React, { DependencyList, EffectCallback, Fragment, useEffect, useState } from "react"; | ||||
| import React, { Fragment, useState } from "react"; | ||||
| 
 | ||||
| import ButtonsForStars from "../display/buttonsForStars"; | ||||
| import Form from "../display/form"; | ||||
| import { MultiSelectPlatform } from "../display/multiSelectPlatforms"; | ||||
| import { SliderElement } from "../display/slider"; | ||||
| import { ButtonsForStars } from "../display/ButtonsForStars"; | ||||
| import { MultiSelectPlatform } from "../display/MultiSelectPlatform"; | ||||
| import { QueryForm } from "../display/QueryForm"; | ||||
| import { SliderElement } from "../display/SliderElement"; | ||||
| import { useNoInitialEffect } from "../hooks"; | ||||
| import { FrontendForecast } from "../platforms"; | ||||
| import searchAccordingToQueryData from "../worker/searchAccordingToQueryData"; | ||||
| 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 { | ||||
|   hasSearchbar: boolean; | ||||
|   hasCapture: boolean; | ||||
|  | @ -76,7 +66,7 @@ const CommonDisplay: React.FC<Props> = ({ | |||
|       numDisplay, | ||||
|     }; | ||||
| 
 | ||||
|     let filterManually = ( | ||||
|     const filterManually = ( | ||||
|       queryData: QueryParameters, | ||||
|       results: FrontendForecast[] | ||||
|     ) => { | ||||
|  | @ -102,7 +92,7 @@ const CommonDisplay: React.FC<Props> = ({ | |||
|     const queryIsEmpty = | ||||
|       !queryData || queryData.query == "" || queryData.query == undefined; | ||||
| 
 | ||||
|     let results = queryIsEmpty | ||||
|     const results = queryIsEmpty | ||||
|       ? filterManually(queryData, defaultResults) | ||||
|       : 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.
 | ||||
|   let getInfoToDisplayForecastsFunction = () => { | ||||
|     let numDisplayRounded = | ||||
|   const getInfoToDisplayForecastsFunction = () => { | ||||
|     const numDisplayRounded = | ||||
|       numDisplay % 3 != 0 | ||||
|         ? numDisplay + (3 - (Math.round(numDisplay) % 3)) | ||||
|         : numDisplay; | ||||
|  | @ -156,7 +146,7 @@ const CommonDisplay: React.FC<Props> = ({ | |||
| 
 | ||||
|   useNoInitialEffect(() => { | ||||
|     setResults([]); | ||||
|     let newTimeoutId = setTimeout(() => { | ||||
|     const newTimeoutId = setTimeout(() => { | ||||
|       updateRoute(); | ||||
|       executeSearchOrAnswerWithDefaultResults(); | ||||
|     }, 500); | ||||
|  | @ -170,7 +160,7 @@ const CommonDisplay: React.FC<Props> = ({ | |||
|   /* State controllers */ | ||||
| 
 | ||||
|   /* Change the stars threshold */ | ||||
|   let onChangeStars = (value: number) => { | ||||
|   const onChangeStars = (value: number) => { | ||||
|     setQueryParameters({ | ||||
|       ...queryParameters, | ||||
|       starsThreshold: value, | ||||
|  | @ -178,7 +168,7 @@ const CommonDisplay: React.FC<Props> = ({ | |||
|   }; | ||||
| 
 | ||||
|   /* Change the number of elements to display  */ | ||||
|   let displayFunctionNumDisplaySlider = (value) => { | ||||
|   const displayFunctionNumDisplaySlider = (value: number) => { | ||||
|     return ( | ||||
|       "Show " + | ||||
|       Math.round(value) + | ||||
|  | @ -186,16 +176,16 @@ const CommonDisplay: React.FC<Props> = ({ | |||
|       (Math.round(value) === 1 ? "" : "s") | ||||
|     ); | ||||
|   }; | ||||
|   let onChangeSliderForNumDisplay = (event) => { | ||||
|   const onChangeSliderForNumDisplay = (event) => { | ||||
|     setNumDisplay(Math.round(event[0])); | ||||
|     setForceSearch(forceSearch + 1); // FIXME - force new search iff numDisplay is greater than last search limit
 | ||||
|   }; | ||||
| 
 | ||||
|   /* Change the forecast threshold */ | ||||
|   let displayFunctionNumForecasts = (value: number) => { | ||||
|   const displayFunctionNumForecasts = (value: number) => { | ||||
|     return "# Forecasts > " + Math.round(value); | ||||
|   }; | ||||
|   let onChangeSliderForNumForecasts = (event) => { | ||||
|   const onChangeSliderForNumForecasts = (event) => { | ||||
|     setQueryParameters({ | ||||
|       ...queryParameters, | ||||
|       forecastsThreshold: Math.round(event[0]), | ||||
|  | @ -203,7 +193,7 @@ const CommonDisplay: React.FC<Props> = ({ | |||
|   }; | ||||
| 
 | ||||
|   /* Change on the search bar */ | ||||
|   let onChangeSearchBar = (value: string) => { | ||||
|   const onChangeSearchBar = (value: string) => { | ||||
|     setQueryParameters({ | ||||
|       ...queryParameters, | ||||
|       query: value, | ||||
|  | @ -211,7 +201,7 @@ const CommonDisplay: React.FC<Props> = ({ | |||
|   }; | ||||
| 
 | ||||
|   /* Change selected platforms */ | ||||
|   let onChangeSelectedPlatforms = (value) => { | ||||
|   const onChangeSelectedPlatforms = (value) => { | ||||
|     setQueryParameters({ | ||||
|       ...queryParameters, | ||||
|       forecastingPlatforms: value, | ||||
|  | @ -219,18 +209,18 @@ const CommonDisplay: React.FC<Props> = ({ | |||
|   }; | ||||
| 
 | ||||
|   // Change show id
 | ||||
|   let onChangeShowId = () => { | ||||
|   const onChangeShowId = () => { | ||||
|     setShowIdToggle(!showIdToggle); | ||||
|   }; | ||||
| 
 | ||||
|   // Capture functionality
 | ||||
|   let onClickBack = () => { | ||||
|     let decreaseUntil0 = (num: number) => (num - 1 > 0 ? num - 1 : 0); | ||||
|   const onClickBack = () => { | ||||
|     const decreaseUntil0 = (num: number) => (num - 1 > 0 ? num - 1 : 0); | ||||
|     setWhichResultToDisplayAndCapture( | ||||
|       decreaseUntil0(whichResultToDisplayAndCapture) | ||||
|     ); | ||||
|   }; | ||||
|   let onClickForward = (whichResultToDisplayAndCapture: number) => { | ||||
|   const onClickForward = (whichResultToDisplayAndCapture: number) => { | ||||
|     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"> | ||||
|         {hasSearchbar ? ( | ||||
|           <div className="w-10/12 mb-2"> | ||||
|             <Form | ||||
|             <QueryForm | ||||
|               value={queryParameters.query} | ||||
|               onChange={onChangeSearchBar} | ||||
|               placeholder={placeholder} | ||||
|  |  | |||
|  | @ -58,10 +58,6 @@ export const getServerSideProps: GetServerSideProps<Props> = async ( | |||
|     ).split("|"); | ||||
|   } | ||||
| 
 | ||||
|   const platformNameToLabel = Object.fromEntries( | ||||
|     platforms.map((platform) => [platform.name, platform.label]) | ||||
|   ); | ||||
| 
 | ||||
|   const defaultNumDisplay = 21; | ||||
|   const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay; | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,19 +1,20 @@ | |||
| import axios from "axios"; | ||||
| 
 | ||||
| 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({ | ||||
|   dashboardId, | ||||
| }): Promise<{ | ||||
|   dashboardForecasts: Forecast[]; | ||||
|   dashboardForecasts: FrontendForecast[]; | ||||
|   dashboardItem: DashboardItem; | ||||
| }> { | ||||
|   console.log("getDashboardForecastsByDashboardId: "); | ||||
|   let dashboardContents: Forecast[] = []; | ||||
|   let dashboardItem: DashboardItem | any = null; | ||||
|   let dashboardForecasts: Forecast[] = []; | ||||
|   let dashboardItem: DashboardItem | null = null; | ||||
|   try { | ||||
|     let { data } = await axios({ | ||||
|     const { data } = await axios({ | ||||
|       url: `${process.env.NEXT_PUBLIC_SITE_URL}/api/dashboard-by-id`, | ||||
|       method: "post", | ||||
|       data: { | ||||
|  | @ -21,13 +22,19 @@ export async function getDashboardForecastsByDashboardId({ | |||
|       }, | ||||
|     }); | ||||
|     console.log(data); | ||||
|     dashboardContents = data.dashboardContents; | ||||
| 
 | ||||
|     dashboardForecasts = data.dashboardContents; | ||||
|     dashboardItem = data.dashboardItem as DashboardItem; | ||||
|   } catch (error) { | ||||
|     console.log(error); | ||||
|   } finally { | ||||
|     const labeledDashboardForecasts = addLabelsToForecasts( | ||||
|       dashboardForecasts, | ||||
|       getPlatformsConfig({ withGuesstimate: false }) | ||||
|     ); | ||||
| 
 | ||||
|     return { | ||||
|       dashboardForecasts: dashboardContents, | ||||
|       dashboardForecasts: labeledDashboardForecasts, | ||||
|       dashboardItem, | ||||
|     }; | ||||
|   } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user