Implement algolia search!
This commit is contained in:
parent
39afe5461f
commit
7744199eac
191
web/components/contract-search.tsx
Normal file
191
web/components/contract-search.tsx
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import algoliasearch from 'algoliasearch/lite'
|
||||||
|
import {
|
||||||
|
InstantSearch,
|
||||||
|
SearchBox,
|
||||||
|
SortBy,
|
||||||
|
useInfiniteHits,
|
||||||
|
useRange,
|
||||||
|
useRefinementList,
|
||||||
|
useSortBy,
|
||||||
|
useToggleRefinement,
|
||||||
|
} from 'react-instantsearch-hooks-web'
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
|
import {
|
||||||
|
Sort,
|
||||||
|
useInitialQueryAndSort,
|
||||||
|
useUpdateQueryAndSort,
|
||||||
|
} from '../hooks/use-sort-and-query-params'
|
||||||
|
import { ContractsGrid } from './contract/contracts-list'
|
||||||
|
import { Row } from './layout/row'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Spacer } from './layout/spacer'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
const searchClient = algoliasearch(
|
||||||
|
'GJQPAYENIF',
|
||||||
|
'75c28fc084a80e1129d427d470cf41a3'
|
||||||
|
)
|
||||||
|
|
||||||
|
const sortIndexes = [
|
||||||
|
{ label: 'Newest', value: 'contracts-newest' },
|
||||||
|
{ label: 'Oldest', value: 'contracts-oldest' },
|
||||||
|
{ label: 'Most traded', value: 'contracts-most-traded' },
|
||||||
|
{ label: '24h volume', value: 'contracts-24-hour-vol' },
|
||||||
|
{ label: 'Closing soon', value: 'contracts-closing-soon' },
|
||||||
|
{ label: 'Resolved', value: 'contracts-resolved' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function ContractSearch(props: {
|
||||||
|
querySortOptions?: {
|
||||||
|
defaultSort: Sort
|
||||||
|
filter?: {
|
||||||
|
creatorId?: string
|
||||||
|
tag?: string
|
||||||
|
}
|
||||||
|
shouldLoadFromStorage?: boolean
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
const { querySortOptions } = props
|
||||||
|
|
||||||
|
const { initialSort } = useInitialQueryAndSort(querySortOptions)
|
||||||
|
|
||||||
|
const sort = sortIndexes
|
||||||
|
.map(({ value }) => value)
|
||||||
|
.includes(`contracts-${initialSort ?? ''}`)
|
||||||
|
? initialSort
|
||||||
|
: querySortOptions?.defaultSort
|
||||||
|
|
||||||
|
console.log('sort', sort)
|
||||||
|
if (!sort) return <></>
|
||||||
|
return (
|
||||||
|
<InstantSearch searchClient={searchClient} indexName={`contracts-${sort}`}>
|
||||||
|
<Row className="gap-2">
|
||||||
|
<SearchBox
|
||||||
|
className="flex-1"
|
||||||
|
classNames={{
|
||||||
|
form: 'before:top-5',
|
||||||
|
input: 'pl-10 input input-bordered h-[40px]',
|
||||||
|
}}
|
||||||
|
placeholder="Search markets"
|
||||||
|
/>
|
||||||
|
<SortBy items={sortIndexes} />
|
||||||
|
</Row>
|
||||||
|
<ContractSearchInner querySortOptions={querySortOptions} />
|
||||||
|
</InstantSearch>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ContractSearchInner(props: {
|
||||||
|
querySortOptions?: {
|
||||||
|
defaultSort: Sort
|
||||||
|
filter?: {
|
||||||
|
creatorId?: string
|
||||||
|
tag?: string
|
||||||
|
}
|
||||||
|
shouldLoadFromStorage?: boolean
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
const { querySortOptions } = props
|
||||||
|
const { initialQuery } = useInitialQueryAndSort(querySortOptions)
|
||||||
|
|
||||||
|
const { query, setQuery, setSort } = useUpdateQueryAndSort({
|
||||||
|
shouldLoadFromStorage: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('initial query', initialQuery)
|
||||||
|
setQuery(initialQuery)
|
||||||
|
}, [initialQuery])
|
||||||
|
|
||||||
|
const { currentRefinement: index } = useSortBy({
|
||||||
|
items: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('setting query', query)
|
||||||
|
setQuery(query)
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('effect sort', 'curr', index)
|
||||||
|
const sort = index.split('contracts-')[1] as Sort
|
||||||
|
if (sort) {
|
||||||
|
setSort(sort)
|
||||||
|
}
|
||||||
|
}, [index])
|
||||||
|
|
||||||
|
const creatorId = querySortOptions?.filter?.creatorId
|
||||||
|
useFilterCreator(creatorId)
|
||||||
|
|
||||||
|
const tag = querySortOptions?.filter?.tag
|
||||||
|
useFilterTag(tag)
|
||||||
|
|
||||||
|
if (!creatorId) {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
useFilterClosed(index)
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
useFilterResolved(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { showMore, hits, isLastPage } = useInfiniteHits()
|
||||||
|
const contracts = hits as any as Contract[]
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const hasLoaded = contracts.length > 0 || router.isReady
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Spacer h={8} />
|
||||||
|
|
||||||
|
{hasLoaded && (
|
||||||
|
<ContractsGrid
|
||||||
|
contracts={contracts}
|
||||||
|
loadMore={showMore}
|
||||||
|
hasMore={!isLastPage}
|
||||||
|
showCloseTime={index === 'contracts-closing-soon'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFilterCreator = (creatorId: string | undefined) => {
|
||||||
|
const { refine } = useRefinementList({ attribute: 'creatorId' })
|
||||||
|
useEffect(() => {
|
||||||
|
if (creatorId) refine(creatorId)
|
||||||
|
}, [creatorId, refine])
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFilterTag = (tag: string | undefined) => {
|
||||||
|
const { refine } = useRefinementList({ attribute: 'lowercaseTags' })
|
||||||
|
useEffect(() => {
|
||||||
|
if (tag) refine(`${tag.toLowerCase()} OR ManifoldMarkets`)
|
||||||
|
}, [tag, refine])
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFilterClosed = (index: string) => {
|
||||||
|
const [now] = useState(Date.now())
|
||||||
|
useRange({
|
||||||
|
attribute: 'closeTime',
|
||||||
|
min: index === 'contracts-resolved' ? 0 : now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const useFilterResolved = (index: string) => {
|
||||||
|
const { refine: refineResolved } = useToggleRefinement({
|
||||||
|
attribute: 'isResolved',
|
||||||
|
on: true,
|
||||||
|
off: false,
|
||||||
|
})
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(
|
||||||
|
'effect',
|
||||||
|
'curr',
|
||||||
|
index,
|
||||||
|
'update',
|
||||||
|
index === 'contracts-resolved'
|
||||||
|
)
|
||||||
|
refineResolved({ isRefined: index !== 'contracts-resolved' })
|
||||||
|
}, [index])
|
||||||
|
}
|
|
@ -1,41 +1,24 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import Link from 'next/link'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import {
|
import { Contract } from '../../lib/firebase/contracts'
|
||||||
contractMetrics,
|
|
||||||
Contract,
|
|
||||||
listContracts,
|
|
||||||
getBinaryProb,
|
|
||||||
} from '../../lib/firebase/contracts'
|
|
||||||
import { User } from '../../lib/firebase/users'
|
import { User } from '../../lib/firebase/users'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { SiteLink } from '../site-link'
|
import { SiteLink } from '../site-link'
|
||||||
import { ContractCard } from './contract-card'
|
import { ContractCard } from './contract-card'
|
||||||
import {
|
import { ContractSearch } from '../contract-search'
|
||||||
Sort,
|
|
||||||
useQueryAndSortParams,
|
|
||||||
} from '../../hooks/use-sort-and-query-params'
|
|
||||||
import { Answer } from '../../../common/answer'
|
|
||||||
import { LoadingIndicator } from '../loading-indicator'
|
|
||||||
|
|
||||||
export function ContractsGrid(props: {
|
export function ContractsGrid(props: {
|
||||||
contracts: Contract[]
|
contracts: Contract[]
|
||||||
showHotVolume?: boolean
|
loadMore: () => void
|
||||||
|
hasMore: boolean
|
||||||
showCloseTime?: boolean
|
showCloseTime?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { showCloseTime } = props
|
const { showCloseTime, hasMore, loadMore } = props
|
||||||
const PAGE_SIZE = 100
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
|
|
||||||
const [resolvedContracts, activeContracts] = _.partition(
|
const [resolvedContracts, activeContracts] = _.partition(
|
||||||
props.contracts,
|
props.contracts,
|
||||||
(c) => c.isResolved
|
(c) => c.isResolved
|
||||||
)
|
)
|
||||||
const allContracts = [...activeContracts, ...resolvedContracts]
|
const contracts = [...activeContracts, ...resolvedContracts]
|
||||||
const showMore = allContracts.length > PAGE_SIZE * page
|
|
||||||
const contracts = allContracts.slice(0, PAGE_SIZE * page)
|
|
||||||
|
|
||||||
if (contracts.length === 0) {
|
if (contracts.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
@ -49,317 +32,38 @@ export function ContractsGrid(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Col className="gap-8">
|
||||||
<ul className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
|
<ul className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
{contracts.map((contract) => (
|
{contracts.map((contract) => (
|
||||||
<ContractCard
|
<ContractCard
|
||||||
contract={contract}
|
contract={contract}
|
||||||
key={contract.id}
|
key={contract.id}
|
||||||
// showHotVolume={showHotVolume}
|
|
||||||
showCloseTime={showCloseTime}
|
showCloseTime={showCloseTime}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{/* Show a link that increases the page num when clicked */}
|
{hasMore && (
|
||||||
{showMore && (
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-link float-right normal-case"
|
className="btn btn-primary self-center normal-case"
|
||||||
onClick={() => setPage(page + 1)}
|
onClick={loadMore}
|
||||||
>
|
>
|
||||||
Show more...
|
Show more
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_GROUPED_CONTRACTS_DISPLAYED = 6
|
|
||||||
|
|
||||||
function CreatorContractsGrid(props: { contracts: Contract[] }) {
|
|
||||||
const { contracts } = props
|
|
||||||
|
|
||||||
const byCreator = _.groupBy(contracts, (contract) => contract.creatorId)
|
|
||||||
const creator7DayVol = _.mapValues(byCreator, (contracts) =>
|
|
||||||
_.sumBy(contracts, (contract) => contract.volume7Days)
|
|
||||||
)
|
|
||||||
const creatorIds = _.sortBy(
|
|
||||||
Object.keys(byCreator),
|
|
||||||
(creatorId) => -1 * creator7DayVol[creatorId]
|
|
||||||
)
|
|
||||||
|
|
||||||
let numContracts = 0
|
|
||||||
let maxIndex = 0
|
|
||||||
for (; maxIndex < creatorIds.length; maxIndex++) {
|
|
||||||
numContracts += Math.min(
|
|
||||||
MAX_GROUPED_CONTRACTS_DISPLAYED,
|
|
||||||
byCreator[creatorIds[maxIndex]].length
|
|
||||||
)
|
|
||||||
if (numContracts > MAX_CONTRACTS_DISPLAYED) break
|
|
||||||
}
|
|
||||||
|
|
||||||
const creatorIdsSubset = creatorIds.slice(0, maxIndex)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col className="gap-6">
|
|
||||||
{creatorIdsSubset.map((creatorId) => {
|
|
||||||
const { creatorUsername, creatorName } = byCreator[creatorId][0]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col className="gap-4" key={creatorUsername}>
|
|
||||||
<SiteLink className="text-lg" href={`/${creatorUsername}`}>
|
|
||||||
{creatorName}
|
|
||||||
</SiteLink>
|
|
||||||
|
|
||||||
<ul role="list" className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
||||||
{byCreator[creatorId]
|
|
||||||
.slice(0, MAX_GROUPED_CONTRACTS_DISPLAYED)
|
|
||||||
.map((contract) => (
|
|
||||||
<ContractCard contract={contract} key={contract.id} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{byCreator[creatorId].length > MAX_GROUPED_CONTRACTS_DISPLAYED ? (
|
|
||||||
<Link href={`/${creatorUsername}`}>
|
|
||||||
<a
|
|
||||||
className={clsx(
|
|
||||||
'self-end hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
|
||||||
)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
See all
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<div />
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TagContractsGrid(props: { contracts: Contract[] }) {
|
|
||||||
const { contracts } = props
|
|
||||||
|
|
||||||
const contractTags = _.flatMap(contracts, (contract) => {
|
|
||||||
const { tags } = contract
|
|
||||||
return tags.map((tag) => ({
|
|
||||||
tag,
|
|
||||||
contract,
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
const groupedByTag = _.groupBy(contractTags, ({ tag }) => tag)
|
|
||||||
const byTag = _.mapValues(groupedByTag, (contractTags) =>
|
|
||||||
contractTags.map(({ contract }) => contract)
|
|
||||||
)
|
|
||||||
const tag7DayVol = _.mapValues(byTag, (contracts) =>
|
|
||||||
_.sumBy(contracts, (contract) => contract.volume7Days)
|
|
||||||
)
|
|
||||||
const tags = _.sortBy(
|
|
||||||
Object.keys(byTag),
|
|
||||||
(creatorId) => -1 * tag7DayVol[creatorId]
|
|
||||||
)
|
|
||||||
|
|
||||||
let numContracts = 0
|
|
||||||
let maxIndex = 0
|
|
||||||
for (; maxIndex < tags.length; maxIndex++) {
|
|
||||||
numContracts += Math.min(
|
|
||||||
MAX_GROUPED_CONTRACTS_DISPLAYED,
|
|
||||||
byTag[tags[maxIndex]].length
|
|
||||||
)
|
|
||||||
if (numContracts > MAX_CONTRACTS_DISPLAYED) break
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagsSubset = tags.slice(0, maxIndex)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Col className="gap-6">
|
|
||||||
{tagsSubset.map((tag) => {
|
|
||||||
return (
|
|
||||||
<Col className="gap-4" key={tag}>
|
|
||||||
<SiteLink className="text-lg" href={`/tag/${tag}`}>
|
|
||||||
#{tag}
|
|
||||||
</SiteLink>
|
|
||||||
|
|
||||||
<ul role="list" className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
|
||||||
{byTag[tag]
|
|
||||||
.slice(0, MAX_GROUPED_CONTRACTS_DISPLAYED)
|
|
||||||
.map((contract) => (
|
|
||||||
<ContractCard contract={contract} key={contract.id} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{byTag[tag].length > MAX_GROUPED_CONTRACTS_DISPLAYED ? (
|
|
||||||
<Link href={`/tag/${tag}`}>
|
|
||||||
<a
|
|
||||||
className={clsx(
|
|
||||||
'self-end hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
|
||||||
)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
See all
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<div />
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Col>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_CONTRACTS_DISPLAYED = 99
|
|
||||||
|
|
||||||
export function SearchableGrid(props: {
|
|
||||||
contracts: Contract[] | undefined
|
|
||||||
byOneCreator?: boolean
|
|
||||||
querySortOptions?: {
|
|
||||||
defaultSort: Sort
|
|
||||||
shouldLoadFromStorage?: boolean
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
const { contracts, byOneCreator, querySortOptions } = props
|
|
||||||
|
|
||||||
const { query, setQuery, sort, setSort } =
|
|
||||||
useQueryAndSortParams(querySortOptions)
|
|
||||||
|
|
||||||
const queryWords = query.toLowerCase().split(' ')
|
|
||||||
function check(corpus: String) {
|
|
||||||
return queryWords.every((word) => corpus.toLowerCase().includes(word))
|
|
||||||
}
|
|
||||||
|
|
||||||
let matches = (contracts ?? []).filter(
|
|
||||||
(c) =>
|
|
||||||
check(c.question) ||
|
|
||||||
check(c.description) ||
|
|
||||||
check(c.creatorName) ||
|
|
||||||
check(c.creatorUsername) ||
|
|
||||||
check(c.lowercaseTags.map((tag) => `#${tag}`).join(' ')) ||
|
|
||||||
check(
|
|
||||||
((c as any).answers ?? [])
|
|
||||||
.map((answer: Answer) => answer.text)
|
|
||||||
.join(' ')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (sort === 'newest' || sort === 'all') {
|
|
||||||
matches.sort((a, b) => b.createdTime - a.createdTime)
|
|
||||||
} else if (sort === 'resolved') {
|
|
||||||
matches = _.sortBy(
|
|
||||||
matches,
|
|
||||||
(contract) => -1 * (contract.resolutionTime ?? 0)
|
|
||||||
)
|
|
||||||
} else if (sort === 'oldest') {
|
|
||||||
matches.sort((a, b) => a.createdTime - b.createdTime)
|
|
||||||
} else if (sort === 'close-date' || sort === 'closed') {
|
|
||||||
matches = _.sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
|
|
||||||
matches = _.sortBy(
|
|
||||||
matches,
|
|
||||||
(contract) =>
|
|
||||||
(sort === 'closed' ? -1 : 1) * (contract.closeTime ?? Infinity)
|
|
||||||
)
|
|
||||||
const hideClosed = sort === 'closed'
|
|
||||||
matches = matches.filter(
|
|
||||||
({ closeTime }) => closeTime && closeTime > Date.now() !== hideClosed
|
|
||||||
)
|
|
||||||
} else if (sort === 'most-traded') {
|
|
||||||
matches.sort((a, b) => b.volume - a.volume)
|
|
||||||
} else if (sort === '24-hour-vol') {
|
|
||||||
// Use lodash for stable sort, so previous sort breaks all ties.
|
|
||||||
matches = _.sortBy(matches, ({ volume7Days }) => -1 * volume7Days)
|
|
||||||
matches = _.sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
|
|
||||||
} else if (sort === 'creator' || sort === 'tag') {
|
|
||||||
matches.sort((a, b) => b.volume7Days - a.volume7Days)
|
|
||||||
} else if (sort === 'most-likely') {
|
|
||||||
matches = _.sortBy(matches, (contract) => -getBinaryProb(contract))
|
|
||||||
} else if (sort === 'least-likely') {
|
|
||||||
// Exclude non-binary contracts
|
|
||||||
matches = matches.filter((contract) => getBinaryProb(contract) !== 0)
|
|
||||||
matches = _.sortBy(matches, (contract) => getBinaryProb(contract))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sort !== 'all') {
|
|
||||||
// Filter for (or filter out) resolved contracts
|
|
||||||
matches = matches.filter((c) =>
|
|
||||||
sort === 'resolved' ? c.resolution : !c.resolution
|
|
||||||
)
|
|
||||||
|
|
||||||
// Filter out closed contracts.
|
|
||||||
if (sort !== 'closed' && sort !== 'resolved') {
|
|
||||||
matches = matches.filter((c) => !c.closeTime || c.closeTime > Date.now())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Show a search input next to a sort dropdown */}
|
|
||||||
<div className="mt-2 mb-8 flex justify-between gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={query}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
placeholder="Search markets"
|
|
||||||
className="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
className="select select-bordered"
|
|
||||||
value={sort}
|
|
||||||
onChange={(e) => setSort(e.target.value as Sort)}
|
|
||||||
>
|
|
||||||
<option value="most-traded">Most traded</option>
|
|
||||||
<option value="24-hour-vol">24h volume</option>
|
|
||||||
<option value="close-date">Closing soon</option>
|
|
||||||
<option value="closed">Closed</option>
|
|
||||||
<option value="newest">Newest</option>
|
|
||||||
<option value="oldest">Oldest</option>
|
|
||||||
<option value="most-likely">Most likely</option>
|
|
||||||
<option value="least-likely">Least likely</option>
|
|
||||||
|
|
||||||
<option value="tag">By tag</option>
|
|
||||||
{!byOneCreator && <option value="creator">By creator</option>}
|
|
||||||
<option value="resolved">Resolved</option>
|
|
||||||
{byOneCreator && <option value="all">All markets</option>}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{contracts === undefined ? (
|
|
||||||
<LoadingIndicator />
|
|
||||||
) : sort === 'tag' ? (
|
|
||||||
<TagContractsGrid contracts={matches} />
|
|
||||||
) : !byOneCreator && sort === 'creator' ? (
|
|
||||||
<CreatorContractsGrid contracts={matches} />
|
|
||||||
) : (
|
|
||||||
<ContractsGrid
|
|
||||||
contracts={matches}
|
|
||||||
showCloseTime={['close-date', 'closed'].includes(sort)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CreatorContractsList(props: { creator: User }) {
|
export function CreatorContractsList(props: { creator: User }) {
|
||||||
const { creator } = props
|
const { creator } = props
|
||||||
const [contracts, setContracts] = useState<Contract[] | 'loading'>('loading')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (creator?.id) {
|
|
||||||
// TODO: stream changes from firestore
|
|
||||||
listContracts(creator.id).then(setContracts)
|
|
||||||
}
|
|
||||||
}, [creator])
|
|
||||||
|
|
||||||
if (contracts === 'loading') return <></>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchableGrid
|
<ContractSearch
|
||||||
contracts={contracts}
|
|
||||||
byOneCreator
|
|
||||||
querySortOptions={{
|
querySortOptions={{
|
||||||
defaultSort: 'all',
|
filter: {
|
||||||
|
creatorId: creator.id,
|
||||||
|
},
|
||||||
|
defaultSort: 'newest',
|
||||||
shouldLoadFromStorage: false,
|
shouldLoadFromStorage: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useSearchBox } from 'react-instantsearch-hooks-web'
|
||||||
|
|
||||||
const MARKETS_SORT = 'markets_sort'
|
const MARKETS_SORT = 'markets_sort'
|
||||||
|
|
||||||
|
@ -11,12 +12,12 @@ export type Sort =
|
||||||
| 'oldest'
|
| 'oldest'
|
||||||
| 'most-traded'
|
| 'most-traded'
|
||||||
| '24-hour-vol'
|
| '24-hour-vol'
|
||||||
| 'close-date'
|
| 'closing-soon'
|
||||||
| 'closed'
|
| 'closed'
|
||||||
| 'resolved'
|
| 'resolved'
|
||||||
| 'all'
|
| 'all'
|
||||||
|
|
||||||
export function useQueryAndSortParams(options?: {
|
export function useInitialQueryAndSort(options?: {
|
||||||
defaultSort: Sort
|
defaultSort: Sort
|
||||||
shouldLoadFromStorage?: boolean
|
shouldLoadFromStorage?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
@ -26,24 +27,58 @@ export function useQueryAndSortParams(options?: {
|
||||||
})
|
})
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const { s: sort, q: query } = router.query as {
|
const [initialSort, setInitialSort] = useState<Sort | undefined>(undefined)
|
||||||
q?: string
|
const [initialQuery, setInitialQuery] = useState('')
|
||||||
s?: Sort
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If there's no sort option, then set the one from localstorage
|
||||||
|
if (router.isReady) {
|
||||||
|
const { s: sort, q: query } = router.query as {
|
||||||
|
q?: string
|
||||||
|
s?: Sort
|
||||||
|
}
|
||||||
|
|
||||||
|
setInitialQuery(query ?? '')
|
||||||
|
|
||||||
|
if (!sort && shouldLoadFromStorage) {
|
||||||
|
console.log('ready loading from storage ', sort ?? defaultSort)
|
||||||
|
const localSort = localStorage.getItem(MARKETS_SORT) as Sort
|
||||||
|
if (localSort) {
|
||||||
|
router.query.s = localSort
|
||||||
|
// Use replace to not break navigating back.
|
||||||
|
router.replace(router, undefined, { shallow: true })
|
||||||
|
}
|
||||||
|
setInitialSort(localSort ?? defaultSort)
|
||||||
|
} else {
|
||||||
|
console.log('ready setting to ', sort ?? defaultSort)
|
||||||
|
setInitialSort(sort ?? defaultSort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [defaultSort, router.isReady, shouldLoadFromStorage])
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialSort,
|
||||||
|
initialQuery,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateQueryAndSort(props: {
|
||||||
|
shouldLoadFromStorage: boolean
|
||||||
|
}) {
|
||||||
|
const { shouldLoadFromStorage } = props
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
const setSort = (sort: Sort | undefined) => {
|
const setSort = (sort: Sort | undefined) => {
|
||||||
router.query.s = sort
|
if (sort !== router.query.s) {
|
||||||
router.push(router, undefined, { shallow: true })
|
router.query.s = sort
|
||||||
if (shouldLoadFromStorage) {
|
router.push(router, undefined, { shallow: true })
|
||||||
localStorage.setItem(MARKETS_SORT, sort || '')
|
if (shouldLoadFromStorage) {
|
||||||
|
localStorage.setItem(MARKETS_SORT, sort || '')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [queryState, setQueryState] = useState(query)
|
const { query, refine } = useSearchBox()
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setQueryState(query)
|
|
||||||
}, [query])
|
|
||||||
|
|
||||||
// Debounce router query update.
|
// Debounce router query update.
|
||||||
const pushQuery = useMemo(
|
const pushQuery = useMemo(
|
||||||
|
@ -60,26 +95,13 @@ export function useQueryAndSortParams(options?: {
|
||||||
)
|
)
|
||||||
|
|
||||||
const setQuery = (query: string | undefined) => {
|
const setQuery = (query: string | undefined) => {
|
||||||
setQueryState(query)
|
refine(query ?? '')
|
||||||
pushQuery(query)
|
pushQuery(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// If there's no sort option, then set the one from localstorage
|
|
||||||
if (router.isReady && !sort && shouldLoadFromStorage) {
|
|
||||||
const localSort = localStorage.getItem(MARKETS_SORT) as Sort
|
|
||||||
if (localSort) {
|
|
||||||
router.query.s = localSort
|
|
||||||
// Use replace to not break navigating back.
|
|
||||||
router.replace(router, undefined, { shallow: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sort: sort ?? defaultSort,
|
|
||||||
query: queryState ?? '',
|
|
||||||
setSort,
|
setSort,
|
||||||
setQuery,
|
setQuery,
|
||||||
|
query,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,13 @@ export default function Document() {
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css"
|
||||||
|
integrity="sha256-TehzF/2QvNKhGQrrNpoOb2Ck4iGZ1J/DI4pkd2oUsBc="
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
/>
|
||||||
|
|
||||||
<script
|
<script
|
||||||
async
|
async
|
||||||
src="https://www.googletagmanager.com/gtag/js?id=G-SSFK1Q138D"
|
src="https://www.googletagmanager.com/gtag/js?id=G-SSFK1Q138D"
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
getFoldBySlug,
|
getFoldBySlug,
|
||||||
getFoldContracts,
|
getFoldContracts,
|
||||||
} from '../../../lib/firebase/folds'
|
} from '../../../lib/firebase/folds'
|
||||||
import { ActivityFeed } from '../../../components/feed/activity-feed'
|
|
||||||
import { TagsList } from '../../../components/tags-list'
|
import { TagsList } from '../../../components/tags-list'
|
||||||
import { Row } from '../../../components/layout/row'
|
import { Row } from '../../../components/layout/row'
|
||||||
import { UserLink } from '../../../components/user-page'
|
import { UserLink } from '../../../components/user-page'
|
||||||
|
@ -20,7 +19,6 @@ import { Spacer } from '../../../components/layout/spacer'
|
||||||
import { Col } from '../../../components/layout/col'
|
import { Col } from '../../../components/layout/col'
|
||||||
import { useUser } from '../../../hooks/use-user'
|
import { useUser } from '../../../hooks/use-user'
|
||||||
import { useFold } from '../../../hooks/use-fold'
|
import { useFold } from '../../../hooks/use-fold'
|
||||||
import { SearchableGrid } from '../../../components/contract/contracts-list'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { scoreCreators, scoreTraders } from '../../../../common/scoring'
|
import { scoreCreators, scoreTraders } from '../../../../common/scoring'
|
||||||
import { Leaderboard } from '../../../components/leaderboard'
|
import { Leaderboard } from '../../../components/leaderboard'
|
||||||
|
@ -209,7 +207,7 @@ export default function FoldPage(props: {
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
title: 'Markets',
|
title: 'Markets',
|
||||||
content: <SearchableGrid contracts={contracts} />,
|
content: <div>This view is deprecated.</div>,
|
||||||
href: foldPath(fold, 'markets'),
|
href: foldPath(fold, 'markets'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,17 +1,8 @@
|
||||||
import {
|
import { ContractSearch } from '../components/contract-search'
|
||||||
ContractsGrid,
|
|
||||||
SearchableGrid,
|
|
||||||
} from '../components/contract/contracts-list'
|
|
||||||
import { Page } from '../components/page'
|
import { Page } from '../components/page'
|
||||||
import { SEO } from '../components/SEO'
|
import { SEO } from '../components/SEO'
|
||||||
import { Title } from '../components/title'
|
|
||||||
import { useContracts } from '../hooks/use-contracts'
|
|
||||||
import { Contract } from '../lib/firebase/contracts'
|
|
||||||
|
|
||||||
// TODO: Rename endpoint to "Explore"
|
// TODO: Rename endpoint to "Explore"
|
||||||
export default function Markets() {
|
export default function Markets() {
|
||||||
const contracts = useContracts()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<SEO
|
<SEO
|
||||||
|
@ -19,31 +10,7 @@ export default function Markets() {
|
||||||
description="Discover what's new, trending, or soon-to-close. Or search among our hundreds of markets."
|
description="Discover what's new, trending, or soon-to-close. Or search among our hundreds of markets."
|
||||||
url="/markets"
|
url="/markets"
|
||||||
/>
|
/>
|
||||||
<SearchableGrid contracts={contracts} />
|
<ContractSearch />
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HotMarkets = (props: { contracts: Contract[] }) => {
|
|
||||||
const { contracts } = props
|
|
||||||
if (contracts.length === 0) return <></>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full rounded-lg border-2 border-indigo-100 bg-indigo-50 p-6 shadow-md">
|
|
||||||
<Title className="!mt-0" text="🔥 Markets" />
|
|
||||||
<ContractsGrid contracts={contracts} showHotVolume />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ClosingSoonMarkets = (props: { contracts: Contract[] }) => {
|
|
||||||
const { contracts } = props
|
|
||||||
if (contracts.length === 0) return <></>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full rounded-lg border-2 border-green-100 bg-green-50 p-6 shadow-md">
|
|
||||||
<Title className="!mt-0" text="⏰ Closing soon" />
|
|
||||||
<ContractsGrid contracts={contracts} showCloseTime />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,30 +1,18 @@
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { SearchableGrid } from '../../components/contract/contracts-list'
|
|
||||||
import { Page } from '../../components/page'
|
import { Page } from '../../components/page'
|
||||||
import { Title } from '../../components/title'
|
import { Title } from '../../components/title'
|
||||||
import {
|
import { ContractSearch } from '../../components/contract-search'
|
||||||
Contract,
|
|
||||||
listTaggedContractsCaseInsensitive,
|
|
||||||
} from '../../lib/firebase/contracts'
|
|
||||||
|
|
||||||
export default function TagPage() {
|
export default function TagPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { tag } = router.query as { tag: string }
|
const { tag } = router.query as { tag: string }
|
||||||
|
|
||||||
// mqp: i wrote this in a panic to make the page literally work at all so if you
|
|
||||||
// want to e.g. listen for new contracts you may want to fix it up
|
|
||||||
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
|
||||||
useEffect(() => {
|
|
||||||
if (tag != null) {
|
|
||||||
listTaggedContractsCaseInsensitive(tag).then(setContracts)
|
|
||||||
}
|
|
||||||
}, [tag])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Title text={`#${tag}`} />
|
<Title text={`#${tag}`} />
|
||||||
<SearchableGrid contracts={contracts} />
|
<ContractSearch
|
||||||
|
querySortOptions={{ filter: { tag }, defaultSort: 'newest' }}
|
||||||
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user