diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx
new file mode 100644
index 00000000..980f8d97
--- /dev/null
+++ b/web/components/contract-search.tsx
@@ -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 (
+
+
+
+
+
+
+
+ )
+}
+
+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 (
+
+
+
+ {hasLoaded && (
+
+ )}
+
+ )
+}
+
+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])
+}
diff --git a/web/components/contract/contracts-list.tsx b/web/components/contract/contracts-list.tsx
index dab8613d..dbbb9a10 100644
--- a/web/components/contract/contracts-list.tsx
+++ b/web/components/contract/contracts-list.tsx
@@ -1,41 +1,24 @@
import _ from 'lodash'
-import Link from 'next/link'
-import clsx from 'clsx'
-import { useEffect, useState } from 'react'
-import {
- contractMetrics,
- Contract,
- listContracts,
- getBinaryProb,
-} from '../../lib/firebase/contracts'
+import { Contract } from '../../lib/firebase/contracts'
import { User } from '../../lib/firebase/users'
import { Col } from '../layout/col'
import { SiteLink } from '../site-link'
import { ContractCard } from './contract-card'
-import {
- Sort,
- useQueryAndSortParams,
-} from '../../hooks/use-sort-and-query-params'
-import { Answer } from '../../../common/answer'
-import { LoadingIndicator } from '../loading-indicator'
+import { ContractSearch } from '../contract-search'
export function ContractsGrid(props: {
contracts: Contract[]
- showHotVolume?: boolean
+ loadMore: () => void
+ hasMore: boolean
showCloseTime?: boolean
}) {
- const { showCloseTime } = props
- const PAGE_SIZE = 100
- const [page, setPage] = useState(1)
-
+ const { showCloseTime, hasMore, loadMore } = props
const [resolvedContracts, activeContracts] = _.partition(
props.contracts,
(c) => c.isResolved
)
- const allContracts = [...activeContracts, ...resolvedContracts]
- const showMore = allContracts.length > PAGE_SIZE * page
- const contracts = allContracts.slice(0, PAGE_SIZE * page)
+ const contracts = [...activeContracts, ...resolvedContracts]
if (contracts.length === 0) {
return (
@@ -49,317 +32,38 @@ export function ContractsGrid(props: {
}
return (
- <>
+
{contracts.map((contract) => (
))}
- {/* Show a link that increases the page num when clicked */}
- {showMore && (
+ {hasMore && (
setPage(page + 1)}
+ className="btn btn-primary self-center normal-case"
+ onClick={loadMore}
>
- Show more...
+ Show more
)}
- >
- )
-}
-
-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 (
-
- {creatorIdsSubset.map((creatorId) => {
- const { creatorUsername, creatorName } = byCreator[creatorId][0]
-
- return (
-
-
- {creatorName}
-
-
-
- {byCreator[creatorId]
- .slice(0, MAX_GROUPED_CONTRACTS_DISPLAYED)
- .map((contract) => (
-
- ))}
-
-
- {byCreator[creatorId].length > MAX_GROUPED_CONTRACTS_DISPLAYED ? (
-
- e.stopPropagation()}
- >
- See all
-
-
- ) : (
-
- )}
-
- )
- })}
)
}
-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 (
-
- {tagsSubset.map((tag) => {
- return (
-
-
- #{tag}
-
-
-
- {byTag[tag]
- .slice(0, MAX_GROUPED_CONTRACTS_DISPLAYED)
- .map((contract) => (
-
- ))}
-
-
- {byTag[tag].length > MAX_GROUPED_CONTRACTS_DISPLAYED ? (
-
- e.stopPropagation()}
- >
- See all
-
-
- ) : (
-
- )}
-
- )
- })}
-
- )
-}
-
-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 (
-
- {/* Show a search input next to a sort dropdown */}
-
- setQuery(e.target.value)}
- placeholder="Search markets"
- className="input input-bordered w-full"
- />
- setSort(e.target.value as Sort)}
- >
- Most traded
- 24h volume
- Closing soon
- Closed
- Newest
- Oldest
- Most likely
- Least likely
-
- By tag
- {!byOneCreator && By creator }
- Resolved
- {byOneCreator && All markets }
-
-
-
- {contracts === undefined ? (
-
- ) : sort === 'tag' ? (
-
- ) : !byOneCreator && sort === 'creator' ? (
-
- ) : (
-
- )}
-
- )
-}
-
export function CreatorContractsList(props: { creator: User }) {
const { creator } = props
- const [contracts, setContracts] = useState('loading')
-
- useEffect(() => {
- if (creator?.id) {
- // TODO: stream changes from firestore
- listContracts(creator.id).then(setContracts)
- }
- }, [creator])
-
- if (contracts === 'loading') return <>>
return (
-
diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx
index 642a4e90..72c899ad 100644
--- a/web/hooks/use-sort-and-query-params.tsx
+++ b/web/hooks/use-sort-and-query-params.tsx
@@ -1,6 +1,7 @@
import _ from 'lodash'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
+import { useSearchBox } from 'react-instantsearch-hooks-web'
const MARKETS_SORT = 'markets_sort'
@@ -11,12 +12,12 @@ export type Sort =
| 'oldest'
| 'most-traded'
| '24-hour-vol'
- | 'close-date'
+ | 'closing-soon'
| 'closed'
| 'resolved'
| 'all'
-export function useQueryAndSortParams(options?: {
+export function useInitialQueryAndSort(options?: {
defaultSort: Sort
shouldLoadFromStorage?: boolean
}) {
@@ -26,24 +27,58 @@ export function useQueryAndSortParams(options?: {
})
const router = useRouter()
- const { s: sort, q: query } = router.query as {
- q?: string
- s?: Sort
+ const [initialSort, setInitialSort] = useState(undefined)
+ const [initialQuery, setInitialQuery] = useState('')
+
+ 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) => {
- router.query.s = sort
- router.push(router, undefined, { shallow: true })
- if (shouldLoadFromStorage) {
- localStorage.setItem(MARKETS_SORT, sort || '')
+ if (sort !== router.query.s) {
+ router.query.s = sort
+ router.push(router, undefined, { shallow: true })
+ if (shouldLoadFromStorage) {
+ localStorage.setItem(MARKETS_SORT, sort || '')
+ }
}
}
- const [queryState, setQueryState] = useState(query)
-
- useEffect(() => {
- setQueryState(query)
- }, [query])
+ const { query, refine } = useSearchBox()
// Debounce router query update.
const pushQuery = useMemo(
@@ -60,26 +95,13 @@ export function useQueryAndSortParams(options?: {
)
const setQuery = (query: string | undefined) => {
- setQueryState(query)
+ refine(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 {
- sort: sort ?? defaultSort,
- query: queryState ?? '',
setSort,
setQuery,
+ query,
}
}
diff --git a/web/pages/_document.tsx b/web/pages/_document.tsx
index 18329b12..1c0c2b9b 100644
--- a/web/pages/_document.tsx
+++ b/web/pages/_document.tsx
@@ -18,6 +18,13 @@ export default function Document() {
rel="stylesheet"
/>
+
+
,
+ content: This view is deprecated.
,
href: foldPath(fold, 'markets'),
},
{
diff --git a/web/pages/markets.tsx b/web/pages/markets.tsx
index 23ff2adf..185c021a 100644
--- a/web/pages/markets.tsx
+++ b/web/pages/markets.tsx
@@ -1,17 +1,8 @@
-import {
- ContractsGrid,
- SearchableGrid,
-} from '../components/contract/contracts-list'
+import { ContractSearch } from '../components/contract-search'
import { Page } from '../components/page'
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"
export default function Markets() {
- const contracts = useContracts()
-
return (
-
+
)
}
-
-export const HotMarkets = (props: { contracts: Contract[] }) => {
- const { contracts } = props
- if (contracts.length === 0) return <>>
-
- return (
-
-
-
-
- )
-}
-
-export const ClosingSoonMarkets = (props: { contracts: Contract[] }) => {
- const { contracts } = props
- if (contracts.length === 0) return <>>
-
- return (
-
-
-
-
- )
-}
diff --git a/web/pages/tag/[tag].tsx b/web/pages/tag/[tag].tsx
index cabc6c80..171a3ab2 100644
--- a/web/pages/tag/[tag].tsx
+++ b/web/pages/tag/[tag].tsx
@@ -1,30 +1,18 @@
-import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
-import { SearchableGrid } from '../../components/contract/contracts-list'
import { Page } from '../../components/page'
import { Title } from '../../components/title'
-import {
- Contract,
- listTaggedContractsCaseInsensitive,
-} from '../../lib/firebase/contracts'
+import { ContractSearch } from '../../components/contract-search'
export default function TagPage() {
const router = useRouter()
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()
- useEffect(() => {
- if (tag != null) {
- listTaggedContractsCaseInsensitive(tag).then(setContracts)
- }
- }, [tag])
-
return (
-
+
)
}