378 lines
12 KiB
TypeScript
378 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 { platformsWithLabels, PlatformWithLabel } from '../platforms';
|
|
import searchAccordingToQueryData from '../worker/searchAccordingToQueryData';
|
|
|
|
interface QueryParametersWithoutNum {
|
|
query: string;
|
|
starsThreshold: number;
|
|
forecastsThreshold: number;
|
|
forecastingPlatforms: PlatformWithLabel[];
|
|
}
|
|
|
|
export interface QueryParameters extends QueryParametersWithoutNum {
|
|
numDisplay: number;
|
|
}
|
|
|
|
interface Props {
|
|
defaultResults: any;
|
|
initialResults: 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;
|
|
}
|
|
|
|
export const defaultQueryParameters: QueryParametersWithoutNum = {
|
|
query: "",
|
|
starsThreshold: 2,
|
|
forecastsThreshold: 0,
|
|
forecastingPlatforms: platformsWithLabels, // weird key value format,
|
|
};
|
|
export const defaultNumDisplay = 21;
|
|
|
|
const useNoInitialEffect = (effect: EffectCallback, deps: DependencyList) => {
|
|
const initial = React.useRef(true);
|
|
useEffect(() => {
|
|
if (initial.current) {
|
|
initial.current = false;
|
|
return;
|
|
}
|
|
return effect();
|
|
}, deps);
|
|
};
|
|
|
|
/* Body */
|
|
const CommonDisplay: React.FC<Props> = ({
|
|
defaultResults,
|
|
initialResults,
|
|
initialQueryParameters,
|
|
hasSearchbar,
|
|
hasCapture,
|
|
hasAdvancedOptions,
|
|
placeholder,
|
|
displaySeeMoreHint,
|
|
displayForecastsWrapper,
|
|
}) => {
|
|
const router = useRouter();
|
|
/* States */
|
|
|
|
const [queryParameters, setQueryParameters] =
|
|
useState<QueryParametersWithoutNum>(initialQueryParameters);
|
|
|
|
const [numDisplay, setNumDisplay] = useState(
|
|
initialQueryParameters.numDisplay ?? defaultNumDisplay
|
|
);
|
|
|
|
// 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) => {
|
|
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)
|
|
: 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 =
|
|
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.map((x) => x.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,
|
|
});
|
|
};
|
|
|
|
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
|
|
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({
|
|
results,
|
|
whichResultToDisplayAndCapture,
|
|
showIdToggle,
|
|
})}
|
|
</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;
|