metaforecast/src/web/search/CommonDisplay.tsx

378 lines
12 KiB
TypeScript
Raw Normal View History

2022-03-24 23:45:21 +00:00
import { useRouter } from 'next/router';
import React, { DependencyList, EffectCallback, Fragment, useEffect, useState } from 'react';
2022-03-16 21:02:34 +00:00
2022-03-25 23:51:11 +00:00
import ButtonsForStars from '../display/buttonsForStars';
import Form from '../display/form';
import MultiSelectPlatform from '../display/multiSelectPlatforms';
import { SliderElement } from '../display/slider';
2022-03-24 23:45:21 +00:00
import { platformsWithLabels, PlatformWithLabel } from '../platforms';
2022-03-16 21:02:34 +00:00
import searchAccordingToQueryData from '../worker/searchAccordingToQueryData';
2022-03-24 22:23:07 +00:00
interface QueryParametersWithoutNum {
query: string;
starsThreshold: number;
forecastsThreshold: number;
forecastingPlatforms: PlatformWithLabel[];
}
2022-03-16 21:02:34 +00:00
2022-03-24 22:23:07 +00:00
export interface QueryParameters extends QueryParametersWithoutNum {
numDisplay: number;
}
interface Props {
defaultResults: any;
2022-03-25 23:51:11 +00:00
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;
}
2022-03-16 21:02:34 +00:00
2022-03-25 23:51:11 +00:00
export const defaultQueryParameters: QueryParametersWithoutNum = {
2022-03-24 23:45:21 +00:00
query: "",
starsThreshold: 2,
forecastsThreshold: 0,
forecastingPlatforms: platformsWithLabels, // weird key value format,
2022-03-16 21:02:34 +00:00
};
2022-03-25 23:51:11 +00:00
export const defaultNumDisplay = 21;
2022-03-16 21:02:34 +00:00
const useNoInitialEffect = (effect: EffectCallback, deps: DependencyList) => {
const initial = React.useRef(true);
useEffect(() => {
if (initial.current) {
initial.current = false;
return;
}
return effect();
}, deps);
};
2022-03-16 21:02:34 +00:00
/* Body */
const CommonDisplay: React.FC<Props> = ({
2022-03-16 21:02:34 +00:00
defaultResults,
2022-03-25 23:51:11 +00:00
initialResults,
initialQueryParameters,
2022-03-16 21:02:34 +00:00
hasSearchbar,
hasCapture,
hasAdvancedOptions,
placeholder,
displaySeeMoreHint,
displayForecastsWrapper,
}) => {
2022-03-24 23:45:21 +00:00
const router = useRouter();
2022-03-16 21:02:34 +00:00
/* States */
2022-03-24 22:23:07 +00:00
const [queryParameters, setQueryParameters] =
2022-03-25 23:51:11 +00:00
useState<QueryParametersWithoutNum>(initialQueryParameters);
2022-03-24 23:45:21 +00:00
2022-03-25 23:51:11 +00:00
const [numDisplay, setNumDisplay] = useState(
initialQueryParameters.numDisplay ?? defaultNumDisplay
);
2022-03-24 22:23:07 +00:00
2022-03-24 23:45:21 +00:00
// used to distinguish numDisplay updates which force search and don't force search, see effects below
const [forceSearch, setForceSearch] = useState(0);
2022-03-24 22:23:07 +00:00
2022-03-25 23:51:11 +00:00
const [results, setResults] = useState(initialResults);
2022-03-16 21:02:34 +00:00
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.
2022-03-24 23:45:21 +00:00
async function executeSearchOrAnswerWithDefaultResults() {
const queryData = {
...queryParameters,
numDisplay,
};
let filterManually = (queryData: QueryParameters, results) => {
2022-03-16 21:02:34 +00:00
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) {
2022-03-16 21:02:34 +00:00
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
2022-03-24 22:51:20 +00:00
? filterManually(queryData, defaultResults)
: await searchAccordingToQueryData(queryData);
setResults(results);
2022-03-16 21:02:34 +00:00
}
// 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,
}) => {
2022-03-16 21:02:34 +00:00
let numDisplayRounded =
2022-03-24 22:23:07 +00:00
numDisplay % 3 != 0
? numDisplay + (3 - (Math.round(numDisplay) % 3))
: numDisplay;
return displayForecastsWrapper({
2022-03-16 21:02:34 +00:00
results,
numDisplay: numDisplayRounded,
whichResultToDisplayAndCapture,
showIdToggle,
});
};
2022-03-24 23:45:21 +00:00
const updateRoute = () => {
const stringify = (key: string, value: any) => {
if (key === "forecastingPlatforms") {
return value.map((x) => x.value).join("|");
} else {
return String(value);
2022-03-16 21:02:34 +00:00
}
2022-03-24 23:45:21 +00:00
};
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;
2022-03-16 21:02:34 +00:00
2022-03-24 23:45:21 +00:00
router.replace({
pathname: router.pathname,
query,
});
};
useNoInitialEffect(updateRoute, [numDisplay]);
2022-03-24 23:45:21 +00:00
useNoInitialEffect(() => {
2022-03-24 23:45:21 +00:00
setResults([]);
let newTimeoutId = setTimeout(() => {
updateRoute();
executeSearchOrAnswerWithDefaultResults();
2022-03-24 22:23:07 +00:00
}, 500);
2022-03-16 21:02:34 +00:00
// avoid sending results if user has not stopped typing.
2022-03-24 22:23:07 +00:00
return () => {
clearTimeout(newTimeoutId);
};
2022-03-25 23:51:11 +00:00
}, [queryParameters, forceSearch]);
2022-03-24 22:23:07 +00:00
/* State controllers */
2022-03-16 21:02:34 +00:00
/* Change the stars threshold */
let onChangeStars = (value: number) => {
2022-03-24 22:23:07 +00:00
setQueryParameters({
...queryParameters,
starsThreshold: value,
2022-03-24 22:23:07 +00:00
});
2022-03-16 21:02:34 +00:00
};
/* Change the number of elements to display */
let displayFunctionNumDisplaySlider = (value) => {
2022-03-24 22:23:07 +00:00
return (
"Show " +
Math.round(value) +
" result" +
(Math.round(value) === 1 ? "" : "s")
);
2022-03-16 21:02:34 +00:00
};
let onChangeSliderForNumDisplay = (event) => {
2022-03-24 22:23:07 +00:00
setNumDisplay(Math.round(event[0]));
2022-03-24 23:45:21 +00:00
setForceSearch(forceSearch + 1); // FIXME - force new search iff numDisplay is greater than last search limit
2022-03-16 21:02:34 +00:00
};
/* Change the forecast threshold */
let displayFunctionNumForecasts = (value: number) => {
2022-03-16 21:02:34 +00:00
return "# Forecasts > " + Math.round(value);
};
let onChangeSliderForNumForecasts = (event) => {
2022-03-24 22:23:07 +00:00
setQueryParameters({
2022-03-16 21:02:34 +00:00
...queryParameters,
forecastsThreshold: Math.round(event[0]),
2022-03-24 22:23:07 +00:00
});
2022-03-16 21:02:34 +00:00
};
/* Change on the search bar */
let onChangeSearchBar = (value: string) => {
2022-03-24 22:23:07 +00:00
setQueryParameters({
...queryParameters,
query: value,
});
2022-03-16 21:02:34 +00:00
};
/* Change selected platforms */
2022-03-16 21:02:34 +00:00
let onChangeSelectedPlatforms = (value) => {
2022-03-24 22:23:07 +00:00
setQueryParameters({
2022-03-16 21:02:34 +00:00
...queryParameters,
forecastingPlatforms: value,
2022-03-24 22:23:07 +00:00
});
2022-03-16 21:02:34 +00:00
};
// Change show id
let onChangeShowId = () => {
setShowIdToggle(!showIdToggle);
};
// Capture functionality
let onClickBack = () => {
let decreaseUntil0 = (num: number) => (num - 1 > 0 ? num - 1 : 0);
2022-03-16 21:02:34 +00:00
setWhichResultToDisplayAndCapture(
decreaseUntil0(whichResultToDisplayAndCapture)
);
};
let onClickForward = (whichResultToDisplayAndCapture: number) => {
2022-03-16 21:02:34 +00:00
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}
2022-03-16 21:02:34 +00:00
/>
</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>
2022-03-16 21:02:34 +00:00
</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>
2022-03-16 21:02:34 +00:00
</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
2022-03-24 22:23:07 +00:00
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>
2022-03-16 21:02:34 +00:00
</div>
</div>
) : null}
2022-03-16 21:02:34 +00:00
<div>
{getInfoToDisplayForecastsFunction({
2022-03-16 21:02:34 +00:00
results,
whichResultToDisplayAndCapture,
showIdToggle,
})}
</div>
2022-03-24 22:23:07 +00:00
{displaySeeMoreHint &&
(!results || (results.length != 0 && numDisplay < results.length)) ? (
<div>
2022-03-24 22:23:07 +00:00
<p className="mt-4 mb-4">
{"Can't find what you were looking for?"}
<span
className={`cursor-pointer text-blue-800 ${
!results ? "hidden" : ""
}`}
onClick={() => {
2022-03-24 22:23:07 +00:00
setNumDisplay(numDisplay * 2);
}}
>
2022-03-24 22:23:07 +00:00
{" 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}
2022-03-16 21:02:34 +00:00
<br></br>
</Fragment>
);
};
export default CommonDisplay;