import React, { Fragment, useState } from 'react'; import { platformNames, PlatformWithLabel } from '../platforms'; import searchAccordingToQueryData from '../worker/searchAccordingToQueryData'; import ButtonsForStars from './buttonsForStars'; import Form from './form'; import MultiSelectPlatform from './multiSelectPlatforms'; import { SliderElement } from './slider'; export interface QueryParameters { query: string; numDisplay: number; starsThreshold: number; forecastsThreshold: number; forecastingPlatforms: PlatformWithLabel[]; } interface Props { initialResults: any; defaultResults: any; initialQueryParameters: QueryParameters; hasSearchbar: boolean; hasCapture: boolean; hasAdvancedOptions: boolean; placeholder: string; displaySeeMoreHint: boolean; displayForecastsWrapper: (opts: { results: any; numDisplay: number; whichResultToDisplayAndCapture: number; showIdToggle: boolean; }) => React.ReactNode; } /* Helper functions */ // URL slugs let transformObjectIntoUrlSlug = (obj: QueryParameters) => { let results = []; for (let key in obj) { if (typeof obj[key] === "number" || typeof obj[key] === "string") { results.push(`${key}=${obj[key]}`); } else if (key === "forecastingPlatforms") { let arr = obj[key].map((x) => x.value); let arrstring = arr.join("|"); results.push(`${key}=${arrstring}`); } } let string = "?" + results.join("&"); return string; }; /* Body */ const CommonDisplay: React.FC = ({ initialResults, defaultResults, initialQueryParameters, hasSearchbar, hasCapture, hasAdvancedOptions, placeholder, displaySeeMoreHint, displayForecastsWrapper, }) => { /* States */ const [queryParameters, setQueryParameters] = useState( initialQueryParameters ); let initialSearchSpeedSettings = { timeoutId: null, awaitEndTyping: 500, time: Date.now(), }; const [searchSpeedSettings, setSearchSpeedSettings] = useState( initialSearchSpeedSettings ); const [results, setResults] = useState(initialResults); const [advancedOptions, showAdvancedOptions] = useState(false); const [whichResultToDisplayAndCapture, setWhichResultToDisplayAndCapture] = useState(0); const [showIdToggle, setShowIdToggle] = useState(false); /* Functions which I want to have access to the Home namespace */ // I don't want to create an "defaultResults" object for each search. async function executeSearchOrAnswerWithDefaultResults( queryData: QueryParameters ) { // the queryData object has the same contents as queryParameters. // but I wanted to spare myself having to think about namespace conflicts. let filterManually = (queryData: QueryParameters, results) => { if ( queryData.forecastingPlatforms && queryData.forecastingPlatforms.length > 0 ) { let forecastingPlatforms = queryData.forecastingPlatforms.map( (platformObj) => platformObj.value ); results = results.filter((result) => forecastingPlatforms.includes(result.item.platform) ); } if (queryData.starsThreshold === 4) { results = results.filter( (result) => result.item.qualityindicators.stars >= 4 ); } if (queryData.forecastsThreshold) { // results = results.filter(result => (result.qualityindicators && result.item.qualityindicators.numforecasts > forecastsThreshold)) } return results; }; const queryIsEmpty = !queryData || queryData.query == "" || queryData.query == undefined; let results = queryIsEmpty ? filterManually(queryData, defaultResults || initialResults) : await searchAccordingToQueryData(queryData); setResults(results); } // 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 = ({ results, whichResultToDisplayAndCapture, showIdToggle, }) => { let numDisplayRounded = queryParameters.numDisplay % 3 != 0 ? queryParameters.numDisplay + (3 - (Math.round(queryParameters.numDisplay) % 3)) : queryParameters.numDisplay; return displayForecastsWrapper({ results, numDisplay: numDisplayRounded, whichResultToDisplayAndCapture, showIdToggle, }); }; /* State controllers */ let onChangeSearchInputs = (newQueryParameters: QueryParameters) => { setQueryParameters(newQueryParameters); // ({ ...newQueryParameters, processedUrlYet: true }); setResults([]); clearTimeout(searchSpeedSettings.timeoutId); let newtimeoutId = setTimeout(async () => { let urlSlug = transformObjectIntoUrlSlug(newQueryParameters); let urlWithoutDefaultParameters = urlSlug .replace("?query=&", "?") .replace("&starsThreshold=2", "") .replace("&numDisplay=21", "") .replace("&forecastsThreshold=0", "") .replace(`&forecastingPlatforms=${platformNames.join("|")}`, ""); if (urlWithoutDefaultParameters != "?query=") { if (typeof window !== "undefined") { if (!window.location.href.includes(urlWithoutDefaultParameters)) { window.history.replaceState( null, "Metaforecast", urlWithoutDefaultParameters ); } } } executeSearchOrAnswerWithDefaultResults(newQueryParameters); setSearchSpeedSettings({ ...searchSpeedSettings, timeoutId: null }); }, searchSpeedSettings.awaitEndTyping); setSearchSpeedSettings({ ...searchSpeedSettings, timeoutId: newtimeoutId }); // avoid sending results if user has not stopped typing. }; /* Change the stars threshold */ let onChangeStars = (value: number) => { let newQueryParameters: QueryParameters = { ...queryParameters, starsThreshold: value, }; onChangeSearchInputs(newQueryParameters); }; /* Change the number of elements to display */ let displayFunctionNumDisplaySlider = (value) => { return Math.round(value) != 1 ? "Show " + Math.round(value) + " results" : "Show " + Math.round(value) + " result"; }; let onChangeSliderForNumDisplay = (event) => { let newQueryParameters: QueryParameters = { ...queryParameters, numDisplay: Math.round(event[0]), }; onChangeSearchInputs(newQueryParameters); // Slightly inefficient because it recomputes the search in time, but it makes my logic easier. }; /* Change the forecast threshold */ let displayFunctionNumForecasts = (value: number) => { return "# Forecasts > " + Math.round(value); }; let onChangeSliderForNumForecasts = (event) => { let newQueryParameters = { ...queryParameters, forecastsThreshold: Math.round(event[0]), }; onChangeSearchInputs(newQueryParameters); }; /* Change on the search bar */ let onChangeSearchBar = (value: string) => { let newQueryParameters = { ...queryParameters, query: value }; onChangeSearchInputs(newQueryParameters); }; /* Change selected platforms */ let onChangeSelectedPlatforms = (value) => { let newQueryParameters = { ...queryParameters, forecastingPlatforms: value, }; onChangeSearchInputs(newQueryParameters); }; // Change show id let onChangeShowId = () => { setShowIdToggle(!showIdToggle); }; // Capture functionality let onClickBack = () => { let decreaseUntil0 = (num: number) => (num - 1 > 0 ? num - 1 : 0); setWhichResultToDisplayAndCapture( decreaseUntil0(whichResultToDisplayAndCapture) ); }; let onClickForward = (whichResultToDisplayAndCapture: number) => { setWhichResultToDisplayAndCapture(whichResultToDisplayAndCapture + 1); // setTimeout(()=> {onClickForward(whichResultToDisplayAndCapture+1)}, 5000) }; /* Final return */ return ( {hasAdvancedOptions && advancedOptions ? (
) : null}
{getInfoToDisplayForecastsFunction({ results, whichResultToDisplayAndCapture, showIdToggle, })}
{displaySeeMoreHint ? (

{"Can't find what you were looking for?"} { setQueryParameters({ ...queryParameters, numDisplay: queryParameters.numDisplay * 2, }); }} > {" Show more, or"} {" "} suggest a question on Metaculus

) : null}

); }; export default CommonDisplay;