355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
import { useRouter } from "next/router";
|
|
import React, { DependencyList, EffectCallback, Fragment, useEffect, useState } from "react";
|
|
|
|
import ButtonsForStars from "../display/buttonsForStars";
|
|
import Form from "../display/form";
|
|
import { MultiSelectPlatform } from "../display/multiSelectPlatforms";
|
|
import { SliderElement } from "../display/slider";
|
|
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;
|
|
hasAdvancedOptions: boolean;
|
|
placeholder: string;
|
|
displaySeeMoreHint: boolean;
|
|
displayForecastsWrapper: (opts: {
|
|
results: FrontendForecast[];
|
|
numDisplay: number;
|
|
whichResultToDisplayAndCapture: number;
|
|
showIdToggle: boolean;
|
|
}) => React.ReactNode;
|
|
}
|
|
|
|
/* Body */
|
|
const CommonDisplay: React.FC<Props> = ({
|
|
defaultResults,
|
|
initialResults,
|
|
initialQueryParameters,
|
|
defaultQueryParameters,
|
|
initialNumDisplay,
|
|
defaultNumDisplay,
|
|
platformsConfig,
|
|
hasSearchbar,
|
|
hasCapture,
|
|
hasAdvancedOptions,
|
|
placeholder,
|
|
displaySeeMoreHint,
|
|
displayForecastsWrapper,
|
|
}) => {
|
|
const router = useRouter();
|
|
/* States */
|
|
|
|
const [queryParameters, setQueryParameters] = useState<QueryParameters>(
|
|
initialQueryParameters
|
|
);
|
|
|
|
const [numDisplay, setNumDisplay] = useState(initialNumDisplay);
|
|
|
|
// used to distinguish numDisplay updates which force search and don't force search, see effects below
|
|
const [forceSearch, setForceSearch] = useState(0);
|
|
|
|
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() {
|
|
const queryData = {
|
|
...queryParameters,
|
|
numDisplay,
|
|
};
|
|
|
|
let filterManually = (
|
|
queryData: QueryParameters,
|
|
results: FrontendForecast[]
|
|
) => {
|
|
if (
|
|
queryData.forecastingPlatforms &&
|
|
queryData.forecastingPlatforms.length > 0
|
|
) {
|
|
results = results.filter((result) =>
|
|
queryData.forecastingPlatforms.includes(result.platform)
|
|
);
|
|
}
|
|
if (queryData.starsThreshold === 4) {
|
|
results = results.filter(
|
|
(result) => result.qualityindicators.stars >= 4
|
|
);
|
|
}
|
|
if (queryData.forecastsThreshold) {
|
|
// results = results.filter(result => (result.qualityindicators && result.qualityindicators.numforecasts > forecastsThreshold))
|
|
}
|
|
return results;
|
|
};
|
|
|
|
const queryIsEmpty =
|
|
!queryData || queryData.query == "" || queryData.query == undefined;
|
|
|
|
let results = queryIsEmpty
|
|
? filterManually(queryData, defaultResults)
|
|
: await searchAccordingToQueryData(queryData, numDisplay);
|
|
|
|
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 = () => {
|
|
let numDisplayRounded =
|
|
numDisplay % 3 != 0
|
|
? numDisplay + (3 - (Math.round(numDisplay) % 3))
|
|
: numDisplay;
|
|
return displayForecastsWrapper({
|
|
results,
|
|
numDisplay: numDisplayRounded,
|
|
whichResultToDisplayAndCapture,
|
|
showIdToggle,
|
|
});
|
|
};
|
|
|
|
const updateRoute = () => {
|
|
const stringify = (key: string, value: any) => {
|
|
if (key === "forecastingPlatforms") {
|
|
return value.join("|");
|
|
} else {
|
|
return String(value);
|
|
}
|
|
};
|
|
|
|
const query = {};
|
|
for (const key of Object.keys(defaultQueryParameters)) {
|
|
const value = stringify(key, queryParameters[key]);
|
|
const defaultValue = stringify(key, defaultQueryParameters[key]);
|
|
if (value === defaultValue) continue;
|
|
query[key] = value;
|
|
}
|
|
|
|
if (numDisplay !== defaultNumDisplay) query["numDisplay"] = numDisplay;
|
|
|
|
router.replace(
|
|
{
|
|
pathname: router.pathname,
|
|
query,
|
|
},
|
|
undefined,
|
|
{ shallow: true }
|
|
);
|
|
};
|
|
|
|
useNoInitialEffect(updateRoute, [numDisplay]);
|
|
|
|
useNoInitialEffect(() => {
|
|
setResults([]);
|
|
let newTimeoutId = setTimeout(() => {
|
|
updateRoute();
|
|
executeSearchOrAnswerWithDefaultResults();
|
|
}, 500);
|
|
|
|
// avoid sending results if user has not stopped typing.
|
|
return () => {
|
|
clearTimeout(newTimeoutId);
|
|
};
|
|
}, [queryParameters, forceSearch]);
|
|
|
|
/* State controllers */
|
|
|
|
/* Change the stars threshold */
|
|
let onChangeStars = (value: number) => {
|
|
setQueryParameters({
|
|
...queryParameters,
|
|
starsThreshold: value,
|
|
});
|
|
};
|
|
|
|
/* Change the number of elements to display */
|
|
let displayFunctionNumDisplaySlider = (value) => {
|
|
return (
|
|
"Show " +
|
|
Math.round(value) +
|
|
" result" +
|
|
(Math.round(value) === 1 ? "" : "s")
|
|
);
|
|
};
|
|
let 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) => {
|
|
return "# Forecasts > " + Math.round(value);
|
|
};
|
|
let onChangeSliderForNumForecasts = (event) => {
|
|
setQueryParameters({
|
|
...queryParameters,
|
|
forecastsThreshold: Math.round(event[0]),
|
|
});
|
|
};
|
|
|
|
/* Change on the search bar */
|
|
let onChangeSearchBar = (value: string) => {
|
|
setQueryParameters({
|
|
...queryParameters,
|
|
query: value,
|
|
});
|
|
};
|
|
|
|
/* Change selected platforms */
|
|
let onChangeSelectedPlatforms = (value) => {
|
|
setQueryParameters({
|
|
...queryParameters,
|
|
forecastingPlatforms: value,
|
|
});
|
|
};
|
|
|
|
// 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);
|
|
};
|
|
|
|
/* Final return */
|
|
return (
|
|
<Fragment>
|
|
<label className="mb-4 mt-4 flex flex-row justify-center items-center">
|
|
{hasSearchbar ? (
|
|
<div className="w-10/12 mb-2">
|
|
<Form
|
|
value={queryParameters.query}
|
|
onChange={onChangeSearchBar}
|
|
placeholder={placeholder}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
|
|
{hasAdvancedOptions ? (
|
|
<div className="w-2/12 flex justify-center ml-4 md:ml-2 lg:ml-0">
|
|
<button
|
|
className="text-gray-500 text-sm mb-2"
|
|
onClick={() => showAdvancedOptions(!advancedOptions)}
|
|
>
|
|
Advanced options ▼
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
|
|
{hasCapture ? (
|
|
<div className="w-2/12 flex justify-center ml-4 md:ml-2 gap-1 lg:ml-0">
|
|
<button
|
|
className="text-blue-500 cursor-pointer text-xl mb-3 pr-3 hover:text-blue-600"
|
|
onClick={() => onClickBack()}
|
|
>
|
|
◀
|
|
</button>
|
|
<button
|
|
className="text-blue-500 cursor-pointer text-xl mb-3 pl-3 hover:text-blue-600"
|
|
onClick={() => onClickForward(whichResultToDisplayAndCapture)}
|
|
>
|
|
▶
|
|
</button>
|
|
</div>
|
|
) : null}
|
|
</label>
|
|
|
|
{hasAdvancedOptions && advancedOptions ? (
|
|
<div className="flex-1 flex-col mx-auto justify-center items-center w-full">
|
|
<div className="grid sm:grid-rows-4 sm:grid-cols-1 md:grid-rows-2 lg:grid-rows-2 grid-cols-1 md:grid-cols-3 lg:grid-cols-3 items-center content-center bg-gray-50 rounded-md px-8 pt-4 pb-1 shadow mb-4">
|
|
<div className="flex row-start-1 row-end-1 col-start-1 col-end-4 md:row-span-1 md:col-start-1 md:col-end-1 md:row-start-1 md:row-end-1 lg:row-span-1 lg:col-start-1 lg:col-end-1 lg:row-start-1 lg:row-end-1 items-center justify-center mb-4">
|
|
<SliderElement
|
|
onChange={onChangeSliderForNumForecasts}
|
|
value={queryParameters.forecastsThreshold}
|
|
displayFunction={displayFunctionNumForecasts}
|
|
/>
|
|
</div>
|
|
<div className="flex row-start-2 row-end-2 col-start-1 col-end-4 md:row-start-1 md:row-end-1 md:col-start-2 md:col-end-2 lg:row-start-1 lg:row-end-1 lg:col-start-2 items-center justify-center mb-4">
|
|
<ButtonsForStars
|
|
onChange={onChangeStars}
|
|
value={queryParameters.starsThreshold}
|
|
/>
|
|
</div>
|
|
<div className="flex row-start-3 row-end-3 col-start-1 col-end-4 md:col-start-3 md:col-end-3 md:row-start-1 md:row-end-1 lg:col-start-3 lg:col-end-3 lg:row-start-1 lg:row-end-1 items-center justify-center mb-4">
|
|
<SliderElement
|
|
value={numDisplay}
|
|
onChange={onChangeSliderForNumDisplay}
|
|
displayFunction={displayFunctionNumDisplaySlider}
|
|
/>
|
|
</div>
|
|
<div className="flex col-span-3 items-center justify-center">
|
|
<MultiSelectPlatform
|
|
platformsConfig={platformsConfig}
|
|
value={queryParameters.forecastingPlatforms}
|
|
onChange={onChangeSelectedPlatforms}
|
|
/>
|
|
</div>
|
|
<button
|
|
className="block col-start-1 col-end-4 md:col-start-2 md:col-end-3 md:row-start-4 md:row-end-4 lg:col-start-2 lg:col-end-3 lg:row-start-4 lg:row-end-4 bg-transparent hover:bg-blue-300 text-blue-400 hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded mt-5 p-10 text-center mb-2 mr-10 ml-10 items-center justify-center"
|
|
onClick={onChangeShowId}
|
|
>
|
|
Toggle show id
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
|
|
<div>{getInfoToDisplayForecastsFunction()}</div>
|
|
|
|
{displaySeeMoreHint &&
|
|
(!results || (results.length != 0 && numDisplay < results.length)) ? (
|
|
<div>
|
|
<p className="mt-4 mb-4">
|
|
{"Can't find what you were looking for?"}
|
|
<span
|
|
className={`cursor-pointer text-blue-800 ${
|
|
!results ? "hidden" : ""
|
|
}`}
|
|
onClick={() => {
|
|
setNumDisplay(numDisplay * 2);
|
|
}}
|
|
>
|
|
{" Show more,"}
|
|
</span>
|
|
{" or "}
|
|
<a
|
|
href="https://www.metaculus.com/questions/create/"
|
|
className="cursor-pointer text-blue-800 no-underline"
|
|
target="_blank"
|
|
>
|
|
suggest a question on Metaculus
|
|
</a>
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
|
|
<br></br>
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
export default CommonDisplay;
|