2022-05-20 02:13:54 +00:00
|
|
|
/* eslint-disable react-hooks/exhaustive-deps */
|
2022-05-09 17:38:33 +00:00
|
|
|
import algoliasearch from 'algoliasearch/lite'
|
2022-06-05 19:06:08 +00:00
|
|
|
|
2022-07-01 22:37:30 +00:00
|
|
|
import { Contract } from 'common/contract'
|
2022-07-30 00:08:51 +00:00
|
|
|
import { Sort, useQueryAndSortParams } from '../hooks/use-sort-and-query-params'
|
2022-07-26 01:27:43 +00:00
|
|
|
import {
|
|
|
|
ContractHighlightOptions,
|
|
|
|
ContractsGrid,
|
|
|
|
} from './contract/contracts-list'
|
2022-05-09 17:38:33 +00:00
|
|
|
import { Row } from './layout/row'
|
2022-07-30 00:08:51 +00:00
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
2022-05-09 17:38:33 +00:00
|
|
|
import { Spacer } from './layout/spacer'
|
2022-06-14 09:35:51 +00:00
|
|
|
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
2022-07-13 21:11:22 +00:00
|
|
|
import { useUser } from 'web/hooks/use-user'
|
|
|
|
import { useFollows } from 'web/hooks/use-follows'
|
2022-07-21 17:45:47 +00:00
|
|
|
import { track, trackCallback } from 'web/lib/service/analytics'
|
2022-06-22 23:45:48 +00:00
|
|
|
import ContractSearchFirestore from 'web/pages/contract-search-firestore'
|
2022-07-13 21:11:22 +00:00
|
|
|
import { useMemberGroups } from 'web/hooks/use-group'
|
2022-07-20 05:23:00 +00:00
|
|
|
import { Group, NEW_USER_GROUP_SLUGS } from 'common/group'
|
2022-07-15 20:06:33 +00:00
|
|
|
import { PillButton } from './buttons/pill-button'
|
2022-07-30 00:08:51 +00:00
|
|
|
import { range, sortBy } from 'lodash'
|
2022-07-20 05:23:00 +00:00
|
|
|
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
|
2022-07-30 00:08:51 +00:00
|
|
|
import { Col } from './layout/col'
|
2022-05-09 17:38:33 +00:00
|
|
|
|
|
|
|
const searchClient = algoliasearch(
|
|
|
|
'GJQPAYENIF',
|
|
|
|
'75c28fc084a80e1129d427d470cf41a3'
|
|
|
|
)
|
|
|
|
|
|
|
|
const indexPrefix = ENV === 'DEV' ? 'dev-' : ''
|
2022-07-30 01:13:53 +00:00
|
|
|
const searchIndexName = ENV === 'DEV' ? 'dev-contracts' : 'contractsIndex'
|
2022-05-09 17:38:33 +00:00
|
|
|
|
2022-07-30 00:08:51 +00:00
|
|
|
const sortOptions = [
|
|
|
|
{ label: 'Newest', value: 'newest' },
|
|
|
|
{ label: 'Most popular', value: 'score' },
|
|
|
|
{ label: 'Most traded', value: 'most-traded' },
|
|
|
|
{ label: '24h volume', value: '24-hour-vol' },
|
|
|
|
{ label: 'Last updated', value: 'last-updated' },
|
|
|
|
{ label: 'Subsidy', value: 'liquidity' },
|
|
|
|
{ label: 'Close date', value: 'close-date' },
|
|
|
|
{ label: 'Resolve date', value: 'resolve-date' },
|
2022-05-09 17:38:33 +00:00
|
|
|
]
|
2022-07-19 22:29:41 +00:00
|
|
|
export const DEFAULT_SORT = 'score'
|
2022-05-09 17:38:33 +00:00
|
|
|
|
2022-07-13 21:11:22 +00:00
|
|
|
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
|
2022-05-09 17:38:33 +00:00
|
|
|
|
|
|
|
export function ContractSearch(props: {
|
|
|
|
querySortOptions?: {
|
|
|
|
defaultSort: Sort
|
2022-05-11 04:19:13 +00:00
|
|
|
defaultFilter?: filter
|
2022-05-09 17:38:33 +00:00
|
|
|
shouldLoadFromStorage?: boolean
|
|
|
|
}
|
2022-05-17 17:56:10 +00:00
|
|
|
additionalFilter?: {
|
|
|
|
creatorId?: string
|
|
|
|
tag?: string
|
2022-07-01 22:37:30 +00:00
|
|
|
excludeContractIds?: string[]
|
2022-07-13 22:20:56 +00:00
|
|
|
groupSlug?: string
|
2022-05-17 17:56:10 +00:00
|
|
|
}
|
2022-07-26 01:27:43 +00:00
|
|
|
highlightOptions?: ContractHighlightOptions
|
2022-06-05 00:00:13 +00:00
|
|
|
onContractClick?: (contract: Contract) => void
|
2022-07-01 22:37:30 +00:00
|
|
|
showPlaceHolder?: boolean
|
|
|
|
hideOrderSelector?: boolean
|
|
|
|
overrideGridClassName?: string
|
2022-07-26 01:27:43 +00:00
|
|
|
cardHideOptions?: {
|
|
|
|
hideGroupLink?: boolean
|
|
|
|
hideQuickBet?: boolean
|
|
|
|
}
|
2022-05-09 17:38:33 +00:00
|
|
|
}) {
|
2022-06-05 00:14:14 +00:00
|
|
|
const {
|
|
|
|
querySortOptions,
|
|
|
|
additionalFilter,
|
|
|
|
onContractClick,
|
2022-07-01 22:37:30 +00:00
|
|
|
overrideGridClassName,
|
|
|
|
hideOrderSelector,
|
|
|
|
showPlaceHolder,
|
2022-07-26 01:27:43 +00:00
|
|
|
cardHideOptions,
|
|
|
|
highlightOptions,
|
2022-06-05 00:14:14 +00:00
|
|
|
} = props
|
2022-05-09 17:38:33 +00:00
|
|
|
|
2022-07-13 21:11:22 +00:00
|
|
|
const user = useUser()
|
2022-07-19 23:22:55 +00:00
|
|
|
const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
|
|
|
|
(group) => !NEW_USER_GROUP_SLUGS.includes(group.slug)
|
|
|
|
)
|
2022-07-20 05:23:00 +00:00
|
|
|
const memberGroupSlugs =
|
|
|
|
memberGroups.length > 0
|
|
|
|
? memberGroups.map((g) => g.slug)
|
|
|
|
: DEFAULT_CATEGORY_GROUPS.map((g) => g.slug)
|
|
|
|
|
|
|
|
const memberPillGroups = sortBy(
|
2022-07-19 23:22:55 +00:00
|
|
|
memberGroups.filter((group) => group.contractIds.length > 0),
|
|
|
|
(group) => group.contractIds.length
|
|
|
|
).reverse()
|
2022-07-20 05:23:00 +00:00
|
|
|
|
|
|
|
const defaultPillGroups = DEFAULT_CATEGORY_GROUPS as Group[]
|
|
|
|
|
|
|
|
const pillGroups =
|
|
|
|
memberPillGroups.length > 0 ? memberPillGroups : defaultPillGroups
|
|
|
|
|
2022-07-13 21:11:22 +00:00
|
|
|
const follows = useFollows(user?.id)
|
2022-05-09 17:38:33 +00:00
|
|
|
|
2022-07-30 00:08:51 +00:00
|
|
|
const { shouldLoadFromStorage, defaultSort } = querySortOptions ?? {}
|
|
|
|
const { query, setQuery, sort, setSort } = useQueryAndSortParams({
|
|
|
|
defaultSort,
|
|
|
|
shouldLoadFromStorage,
|
|
|
|
})
|
2022-05-09 17:38:33 +00:00
|
|
|
|
2022-05-11 04:19:13 +00:00
|
|
|
const [filter, setFilter] = useState<filter>(
|
|
|
|
querySortOptions?.defaultFilter ?? 'open'
|
|
|
|
)
|
2022-07-20 19:49:14 +00:00
|
|
|
const pillsEnabled = !additionalFilter
|
2022-07-21 17:45:47 +00:00
|
|
|
|
2022-07-20 20:05:48 +00:00
|
|
|
const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
|
2022-05-09 17:38:33 +00:00
|
|
|
|
2022-07-30 00:08:51 +00:00
|
|
|
const selectPill = (pill: string | undefined) => () => {
|
2022-07-21 17:45:47 +00:00
|
|
|
setPillFilter(pill)
|
2022-07-30 00:08:51 +00:00
|
|
|
setPage(0)
|
2022-07-21 17:45:47 +00:00
|
|
|
track('select search category', { category: pill ?? 'all' })
|
|
|
|
}
|
|
|
|
|
2022-07-30 01:13:53 +00:00
|
|
|
const additionalFilters = [
|
|
|
|
additionalFilter?.creatorId
|
|
|
|
? `creatorId:${additionalFilter.creatorId}`
|
|
|
|
: '',
|
|
|
|
additionalFilter?.tag ? `lowercaseTags:${additionalFilter.tag}` : '',
|
|
|
|
additionalFilter?.groupSlug
|
|
|
|
? `groupLinks.slug:${additionalFilter.groupSlug}`
|
|
|
|
: '',
|
|
|
|
]
|
2022-07-30 00:37:53 +00:00
|
|
|
let facetFilters = query
|
2022-07-30 01:13:53 +00:00
|
|
|
? additionalFilters
|
2022-07-30 00:37:53 +00:00
|
|
|
: [
|
2022-07-30 01:13:53 +00:00
|
|
|
...additionalFilters,
|
2022-07-30 00:37:53 +00:00
|
|
|
filter === 'open' ? 'isResolved:false' : '',
|
|
|
|
filter === 'closed' ? 'isResolved:false' : '',
|
|
|
|
filter === 'resolved' ? 'isResolved:true' : '',
|
|
|
|
pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets'
|
|
|
|
? `groupLinks.slug:${pillFilter}`
|
|
|
|
: '',
|
|
|
|
pillFilter === 'personal'
|
|
|
|
? // Show contracts in groups that the user is a member of
|
|
|
|
memberGroupSlugs
|
|
|
|
.map((slug) => `groupLinks.slug:${slug}`)
|
|
|
|
// Show contracts created by users the user follows
|
|
|
|
.concat(follows?.map((followId) => `creatorId:${followId}`) ?? [])
|
|
|
|
// Show contracts bet on by users the user follows
|
|
|
|
.concat(
|
|
|
|
follows?.map((followId) => `uniqueBettorIds:${followId}`) ?? []
|
|
|
|
)
|
|
|
|
: '',
|
|
|
|
// Subtract contracts you bet on from For you.
|
|
|
|
pillFilter === 'personal' && user ? `uniqueBettorIds:-${user.id}` : '',
|
|
|
|
pillFilter === 'your-bets' && user
|
|
|
|
? // Show contracts bet on by the user
|
|
|
|
`uniqueBettorIds:${user.id}`
|
|
|
|
: '',
|
|
|
|
].filter((f) => f)
|
2022-07-30 00:08:51 +00:00
|
|
|
// Hack to make Algolia work.
|
|
|
|
facetFilters = ['', ...facetFilters]
|
|
|
|
|
2022-07-30 00:37:53 +00:00
|
|
|
const numericFilters = query
|
|
|
|
? []
|
|
|
|
: [
|
|
|
|
filter === 'open' ? `closeTime > ${Date.now()}` : '',
|
|
|
|
filter === 'closed' ? `closeTime <= ${Date.now()}` : '',
|
|
|
|
].filter((f) => f)
|
2022-05-17 17:56:10 +00:00
|
|
|
|
2022-06-03 04:52:14 +00:00
|
|
|
const indexName = `${indexPrefix}contracts-${sort}`
|
2022-07-30 00:08:51 +00:00
|
|
|
const index = useMemo(() => searchClient.initIndex(indexName), [indexName])
|
2022-07-30 01:13:53 +00:00
|
|
|
const searchIndex = useMemo(
|
|
|
|
() => searchClient.initIndex(searchIndexName),
|
|
|
|
[searchIndexName]
|
|
|
|
)
|
2022-07-30 00:08:51 +00:00
|
|
|
|
|
|
|
const [page, setPage] = useState(0)
|
|
|
|
const [numPages, setNumPages] = useState(1)
|
|
|
|
const [hitsByPage, setHitsByPage] = useState<{ [page: string]: Contract[] }>(
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
let wasMostRecentQuery = true
|
2022-07-30 01:13:53 +00:00
|
|
|
const algoliaIndex = query ? searchIndex : index
|
|
|
|
|
|
|
|
algoliaIndex
|
2022-07-30 00:08:51 +00:00
|
|
|
.search(query, {
|
|
|
|
facetFilters,
|
|
|
|
numericFilters,
|
|
|
|
page,
|
|
|
|
hitsPerPage: 20,
|
|
|
|
})
|
|
|
|
.then((results) => {
|
|
|
|
if (!wasMostRecentQuery) return
|
|
|
|
|
|
|
|
if (page === 0) {
|
|
|
|
setHitsByPage({
|
|
|
|
[0]: results.hits as any as Contract[],
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
setHitsByPage((hitsByPage) => ({
|
|
|
|
...hitsByPage,
|
|
|
|
[page]: results.hits,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
setNumPages(results.nbPages)
|
|
|
|
})
|
|
|
|
return () => {
|
|
|
|
wasMostRecentQuery = false
|
|
|
|
}
|
|
|
|
// Note numeric filters are unique based on current time, so can't compare
|
|
|
|
// them by value.
|
2022-07-30 01:13:53 +00:00
|
|
|
}, [query, page, index, searchIndex, JSON.stringify(facetFilters), filter])
|
2022-07-30 00:08:51 +00:00
|
|
|
|
|
|
|
const loadMore = () => {
|
|
|
|
if (page >= numPages - 1) return
|
|
|
|
|
|
|
|
const haveLoadedCurrentPage = hitsByPage[page]
|
|
|
|
if (haveLoadedCurrentPage) setPage(page + 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
const hits = range(0, page + 1)
|
|
|
|
.map((p) => hitsByPage[p] ?? [])
|
|
|
|
.flat()
|
|
|
|
|
|
|
|
const contracts = hits.filter(
|
|
|
|
(c) => !additionalFilter?.excludeContractIds?.includes(c.id)
|
|
|
|
)
|
|
|
|
|
|
|
|
const showTime =
|
|
|
|
sort === 'close-date' || sort === 'resolve-date' ? sort : undefined
|
|
|
|
|
|
|
|
const updateQuery = (newQuery: string) => {
|
|
|
|
setQuery(newQuery)
|
|
|
|
setPage(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
const selectFilter = (newFilter: filter) => {
|
|
|
|
if (newFilter === filter) return
|
|
|
|
setFilter(newFilter)
|
|
|
|
setPage(0)
|
|
|
|
trackCallback('select search filter', { filter: newFilter })
|
|
|
|
}
|
|
|
|
|
|
|
|
const selectSort = (newSort: Sort) => {
|
|
|
|
if (newSort === sort) return
|
|
|
|
|
|
|
|
setPage(0)
|
|
|
|
setSort(newSort)
|
|
|
|
track('select sort', { sort: newSort })
|
|
|
|
}
|
2022-06-03 04:52:14 +00:00
|
|
|
|
2022-07-01 16:26:45 +00:00
|
|
|
if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
|
2022-06-24 17:27:01 +00:00
|
|
|
return (
|
|
|
|
<ContractSearchFirestore
|
|
|
|
querySortOptions={querySortOptions}
|
|
|
|
additionalFilter={additionalFilter}
|
|
|
|
/>
|
|
|
|
)
|
2022-06-14 09:35:51 +00:00
|
|
|
}
|
|
|
|
|
2022-05-09 17:38:33 +00:00
|
|
|
return (
|
2022-07-30 00:08:51 +00:00
|
|
|
<Col>
|
2022-05-18 14:42:52 +00:00
|
|
|
<Row className="gap-1 sm:gap-2">
|
2022-07-30 00:08:51 +00:00
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
value={query}
|
|
|
|
onChange={(e) => updateQuery(e.target.value)}
|
|
|
|
placeholder={showPlaceHolder ? `Search ${filter} markets` : ''}
|
|
|
|
className="input input-bordered w-full"
|
2022-05-18 14:42:52 +00:00
|
|
|
/>
|
2022-07-30 00:37:53 +00:00
|
|
|
{!query && (
|
|
|
|
<select
|
|
|
|
className="select select-bordered"
|
|
|
|
value={filter}
|
|
|
|
onChange={(e) => selectFilter(e.target.value as filter)}
|
|
|
|
>
|
|
|
|
<option value="open">Open</option>
|
|
|
|
<option value="closed">Closed</option>
|
|
|
|
<option value="resolved">Resolved</option>
|
|
|
|
<option value="all">All</option>
|
|
|
|
</select>
|
|
|
|
)}
|
2022-07-30 01:13:53 +00:00
|
|
|
{!hideOrderSelector && !query && (
|
2022-07-30 00:08:51 +00:00
|
|
|
<select
|
|
|
|
className="select select-bordered"
|
|
|
|
value={sort}
|
|
|
|
onChange={(e) => selectSort(e.target.value as Sort)}
|
|
|
|
>
|
|
|
|
{sortOptions.map((option) => (
|
|
|
|
<option key={option.value} value={option.value}>
|
|
|
|
{option.label}
|
|
|
|
</option>
|
|
|
|
))}
|
|
|
|
</select>
|
2022-07-01 22:37:30 +00:00
|
|
|
)}
|
2022-05-09 17:38:33 +00:00
|
|
|
</Row>
|
2022-05-18 14:42:52 +00:00
|
|
|
|
|
|
|
<Spacer h={3} />
|
|
|
|
|
2022-07-20 19:49:14 +00:00
|
|
|
{pillsEnabled && (
|
2022-07-19 23:22:55 +00:00
|
|
|
<Row className="scrollbar-hide items-start gap-2 overflow-x-auto">
|
|
|
|
<PillButton
|
|
|
|
key={'all'}
|
|
|
|
selected={pillFilter === undefined}
|
2022-07-30 00:08:51 +00:00
|
|
|
onSelect={selectPill(undefined)}
|
2022-07-19 23:22:55 +00:00
|
|
|
>
|
|
|
|
All
|
|
|
|
</PillButton>
|
|
|
|
<PillButton
|
|
|
|
key={'personal'}
|
|
|
|
selected={pillFilter === 'personal'}
|
2022-07-30 00:08:51 +00:00
|
|
|
onSelect={selectPill('personal')}
|
2022-07-19 23:22:55 +00:00
|
|
|
>
|
2022-07-22 21:30:07 +00:00
|
|
|
{user ? 'For you' : 'Featured'}
|
2022-07-19 23:22:55 +00:00
|
|
|
</PillButton>
|
2022-07-20 05:23:00 +00:00
|
|
|
|
2022-07-22 19:50:29 +00:00
|
|
|
{user && (
|
|
|
|
<PillButton
|
|
|
|
key={'your-bets'}
|
|
|
|
selected={pillFilter === 'your-bets'}
|
2022-07-30 00:08:51 +00:00
|
|
|
onSelect={selectPill('your-bets')}
|
2022-07-22 19:50:29 +00:00
|
|
|
>
|
|
|
|
Your bets
|
|
|
|
</PillButton>
|
|
|
|
)}
|
2022-07-20 20:30:07 +00:00
|
|
|
|
2022-07-19 23:22:55 +00:00
|
|
|
{pillGroups.map(({ name, slug }) => {
|
|
|
|
return (
|
|
|
|
<PillButton
|
|
|
|
key={slug}
|
|
|
|
selected={pillFilter === slug}
|
2022-07-30 00:08:51 +00:00
|
|
|
onSelect={selectPill(slug)}
|
2022-07-19 23:22:55 +00:00
|
|
|
>
|
|
|
|
{name}
|
|
|
|
</PillButton>
|
|
|
|
)
|
|
|
|
})}
|
|
|
|
</Row>
|
|
|
|
)}
|
2022-07-15 20:06:33 +00:00
|
|
|
|
|
|
|
<Spacer h={3} />
|
2022-07-13 21:11:22 +00:00
|
|
|
|
|
|
|
{filter === 'personal' &&
|
|
|
|
(follows ?? []).length === 0 &&
|
2022-07-19 23:22:55 +00:00
|
|
|
memberGroupSlugs.length === 0 ? (
|
2022-07-13 21:11:22 +00:00
|
|
|
<>You're not following anyone, nor in any of your own groups yet.</>
|
|
|
|
) : (
|
2022-07-30 00:08:51 +00:00
|
|
|
<ContractsGrid
|
|
|
|
contracts={contracts}
|
|
|
|
loadMore={loadMore}
|
|
|
|
hasMore={true}
|
|
|
|
showTime={showTime}
|
2022-07-13 21:11:22 +00:00
|
|
|
onContractClick={onContractClick}
|
|
|
|
overrideGridClassName={overrideGridClassName}
|
2022-07-26 01:27:43 +00:00
|
|
|
highlightOptions={highlightOptions}
|
|
|
|
cardHideOptions={cardHideOptions}
|
2022-07-13 21:11:22 +00:00
|
|
|
/>
|
|
|
|
)}
|
2022-07-30 00:08:51 +00:00
|
|
|
</Col>
|
2022-05-09 17:38:33 +00:00
|
|
|
)
|
|
|
|
}
|