2022-03-16 21:02:34 +00:00
import React , { Fragment , useState } from 'react' ;
2022-03-24 21:29:01 +00:00
import { platformNames , PlatformWithLabel } from '../platforms' ;
2022-03-16 21:02:34 +00:00
import searchAccordingToQueryData from '../worker/searchAccordingToQueryData' ;
2022-03-24 21:29:01 +00:00
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 [ ] ;
}
2022-03-16 21:02:34 +00:00
2022-03-24 21:29:01 +00:00
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 ;
}
2022-03-16 21:02:34 +00:00
/* Helper functions */
// URL slugs
2022-03-24 21:29:01 +00:00
let transformObjectIntoUrlSlug = ( obj : QueryParameters ) = > {
2022-03-16 21:02:34 +00:00
let results = [ ] ;
for ( let key in obj ) {
2022-03-24 21:29:01 +00:00
if ( typeof obj [ key ] === "number" || typeof obj [ key ] === "string" ) {
2022-03-16 21:02:34 +00:00
results . push ( ` ${ key } = ${ obj [ key ] } ` ) ;
2022-03-24 21:29:01 +00:00
} else if ( key === "forecastingPlatforms" ) {
2022-03-16 21:02:34 +00:00
let arr = obj [ key ] . map ( ( x ) = > x . value ) ;
let arrstring = arr . join ( "|" ) ;
results . push ( ` ${ key } = ${ arrstring } ` ) ;
}
}
let string = "?" + results . join ( "&" ) ;
return string ;
} ;
/* Body */
2022-03-24 21:29:01 +00:00
const CommonDisplay : React.FC < Props > = ( {
2022-03-16 21:02:34 +00:00
initialResults ,
defaultResults ,
initialQueryParameters ,
hasSearchbar ,
hasCapture ,
hasAdvancedOptions ,
placeholder ,
displaySeeMoreHint ,
displayForecastsWrapper ,
2022-03-24 21:29:01 +00:00
} ) = > {
2022-03-16 21:02:34 +00:00
/* 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.
2022-03-24 21:29:01 +00:00
async function executeSearchOrAnswerWithDefaultResults (
queryData : QueryParameters
) {
2022-03-16 21:02:34 +00:00
// the queryData object has the same contents as queryParameters.
// but I wanted to spare myself having to think about namespace conflicts.
2022-03-24 21:29:01 +00:00
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 )
) ;
}
2022-03-24 21:29:01 +00:00
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 ;
} ;
2022-03-24 21:29:01 +00:00
const queryIsEmpty =
! queryData || queryData . query == "" || queryData . query == undefined ;
let results = queryIsEmpty
? filterManually ( queryData , defaultResults || initialResults )
: await searchAccordingToQueryData ( queryData ) ;
setResults ( results ) ;
2022-03-16 21:02:34 +00:00
}
2022-03-24 21:29:01 +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 =
queryParameters . numDisplay % 3 != 0
? queryParameters . numDisplay +
( 3 - ( Math . round ( queryParameters . numDisplay ) % 3 ) )
: queryParameters . numDisplay ;
2022-03-24 21:29:01 +00:00
return displayForecastsWrapper ( {
2022-03-16 21:02:34 +00:00
results ,
numDisplay : numDisplayRounded ,
whichResultToDisplayAndCapture ,
showIdToggle ,
} ) ;
} ;
/* State controllers */
2022-03-24 21:29:01 +00:00
let onChangeSearchInputs = ( newQueryParameters : QueryParameters ) = > {
2022-03-16 21:02:34 +00:00
setQueryParameters ( newQueryParameters ) ; // ({ ...newQueryParameters, processedUrlYet: true });
setResults ( [ ] ) ;
clearTimeout ( searchSpeedSettings . timeoutId ) ;
let newtimeoutId = setTimeout ( async ( ) = > {
let urlSlug = transformObjectIntoUrlSlug ( newQueryParameters ) ;
let urlWithoutDefaultParameters = urlSlug
2022-03-24 21:29:01 +00:00
. replace ( "?query=&" , "?" )
2022-03-16 21:02:34 +00:00
. 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 */
2022-03-24 21:29:01 +00:00
let onChangeStars = ( value : number ) = > {
let newQueryParameters : QueryParameters = {
. . . queryParameters ,
starsThreshold : value ,
} ;
2022-03-16 21:02:34 +00:00
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 ) = > {
2022-03-24 21:29:01 +00:00
let newQueryParameters : QueryParameters = {
2022-03-16 21:02:34 +00:00
. . . 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 */
2022-03-24 21:29:01 +00:00
let displayFunctionNumForecasts = ( value : number ) = > {
2022-03-16 21:02:34 +00:00
return "# Forecasts > " + Math . round ( value ) ;
} ;
let onChangeSliderForNumForecasts = ( event ) = > {
let newQueryParameters = {
. . . queryParameters ,
forecastsThreshold : Math.round ( event [ 0 ] ) ,
} ;
onChangeSearchInputs ( newQueryParameters ) ;
} ;
/* Change on the search bar */
2022-03-24 21:29:01 +00:00
let onChangeSearchBar = ( value : string ) = > {
2022-03-16 21:02:34 +00:00
let newQueryParameters = { . . . queryParameters , query : value } ;
onChangeSearchInputs ( newQueryParameters ) ;
} ;
2022-03-24 21:29:01 +00:00
/* Change selected platforms */
2022-03-16 21:02:34 +00:00
let onChangeSelectedPlatforms = ( value ) = > {
let newQueryParameters = {
. . . queryParameters ,
forecastingPlatforms : value ,
} ;
onChangeSearchInputs ( newQueryParameters ) ;
} ;
// Change show id
let onChangeShowId = ( ) = > {
setShowIdToggle ( ! showIdToggle ) ;
} ;
// Capture functionality
let onClickBack = ( ) = > {
2022-03-24 21:29:01 +00:00
let decreaseUntil0 = ( num : number ) = > ( num - 1 > 0 ? num - 1 : 0 ) ;
2022-03-16 21:02:34 +00:00
setWhichResultToDisplayAndCapture (
decreaseUntil0 ( whichResultToDisplayAndCapture )
) ;
} ;
2022-03-24 21:29:01 +00:00
let onClickForward = ( whichResultToDisplayAndCapture : number ) = > {
2022-03-16 21:02:34 +00:00
setWhichResultToDisplayAndCapture ( whichResultToDisplayAndCapture + 1 ) ;
// setTimeout(()=> {onClickForward(whichResultToDisplayAndCapture+1)}, 5000)
} ;
/* Final return */
return (
2022-03-24 21:29:01 +00:00
< 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 >
2022-03-24 21:29:01 +00:00
) : 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 >
2022-03-24 21:29:01 +00:00
) : 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 >
2022-03-24 21:29:01 +00:00
) : 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 = { queryParameters . 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 >
2022-03-24 21:29:01 +00:00
) : null }
2022-03-16 21:02:34 +00:00
2022-03-24 21:29:01 +00:00
< div >
{ getInfoToDisplayForecastsFunction ( {
2022-03-16 21:02:34 +00:00
results ,
whichResultToDisplayAndCapture ,
showIdToggle ,
} ) }
< / div >
2022-03-24 21:29:01 +00:00
{ displaySeeMoreHint ? (
< div >
< p
className = { ` mt-4 mb-4 ${
! results ||
( results . length != 0 &&
queryParameters . numDisplay < results . length )
? ""
: "hidden"
2022-03-16 21:02:34 +00:00
} ` }
>
2022-03-24 21:29:01 +00:00
{ "Can't find what you were looking for?" }
< span
className = { ` cursor-pointer text-blue-800 ${
! results ? "hidden" : ""
} ` }
onClick = { ( ) = > {
setQueryParameters ( {
. . . queryParameters ,
numDisplay : queryParameters.numDisplay * 2 ,
} ) ;
} }
>
{ " Show more, or" }
< / span > { " " }
< 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 >
) ;
2022-03-24 21:29:01 +00:00
} ;
export default CommonDisplay ;