WIP persistence work (#762)
* WIP persistence work * Fix up close date filter, kill custom scroll restoration * Use built-in Next.js scroll restoration machinery * Tweaking stuff * Implement 'history state' idea * Clean up and unify persistent state stores * Respect options for persisting contract search * Fix typing in common lib * Clean up console logging
This commit is contained in:
parent
1d948821ca
commit
1369f3b967
|
@ -1,6 +1,6 @@
|
||||||
import { union } from 'lodash'
|
import { union } from 'lodash'
|
||||||
|
|
||||||
export const removeUndefinedProps = <T>(obj: T): T => {
|
export const removeUndefinedProps = <T extends object>(obj: T): T => {
|
||||||
const newObj: any = {}
|
const newObj: any = {}
|
||||||
|
|
||||||
for (const key of Object.keys(obj)) {
|
for (const key of Object.keys(obj)) {
|
||||||
|
@ -37,4 +37,3 @@ export const subtractObjects = <T extends { [key: string]: number }>(
|
||||||
|
|
||||||
return newObj as T
|
return newObj as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,44 +1,35 @@
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import algoliasearch, { SearchIndex } from 'algoliasearch/lite'
|
import algoliasearch from 'algoliasearch/lite'
|
||||||
import { SearchOptions } from '@algolia/client-search'
|
import { SearchOptions } from '@algolia/client-search'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { Sort, useQuery, useSort } from '../hooks/use-sort-and-query-params'
|
|
||||||
import {
|
import {
|
||||||
ContractHighlightOptions,
|
ContractHighlightOptions,
|
||||||
ContractsGrid,
|
ContractsGrid,
|
||||||
} from './contract/contracts-grid'
|
} from './contract/contracts-grid'
|
||||||
import { ShowTime } from './contract/contract-details'
|
import { ShowTime } from './contract/contract-details'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { useEffect, useRef, useMemo, useState } from 'react'
|
import { useEffect, useLayoutEffect, useRef, useMemo } from 'react'
|
||||||
import { unstable_batchedUpdates } from 'react-dom'
|
|
||||||
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
|
||||||
import { useFollows } from 'web/hooks/use-follows'
|
import { useFollows } from 'web/hooks/use-follows'
|
||||||
|
import {
|
||||||
|
storageStore,
|
||||||
|
historyStore,
|
||||||
|
urlParamStore,
|
||||||
|
usePersistentState,
|
||||||
|
} from 'web/hooks/use-persistent-state'
|
||||||
|
import { safeLocalStorage } from 'web/lib/util/local'
|
||||||
import { track, trackCallback } from 'web/lib/service/analytics'
|
import { track, trackCallback } from 'web/lib/service/analytics'
|
||||||
import ContractSearchFirestore from 'web/pages/contract-search-firestore'
|
import ContractSearchFirestore from 'web/pages/contract-search-firestore'
|
||||||
import { useMemberGroups } from 'web/hooks/use-group'
|
import { useMemberGroups } from 'web/hooks/use-group'
|
||||||
import { NEW_USER_GROUP_SLUGS } from 'common/group'
|
import { NEW_USER_GROUP_SLUGS } from 'common/group'
|
||||||
import { PillButton } from './buttons/pill-button'
|
import { PillButton } from './buttons/pill-button'
|
||||||
import { debounce, sortBy } from 'lodash'
|
import { debounce, isEqual, sortBy } from 'lodash'
|
||||||
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
|
import { DEFAULT_CATEGORY_GROUPS } from 'common/categories'
|
||||||
import { Col } from './layout/col'
|
import { Col } from './layout/col'
|
||||||
import { safeLocalStorage } from 'web/lib/util/local'
|
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
|
||||||
// TODO: this obviously doesn't work with SSR, common sense would suggest
|
|
||||||
// that we should save things like this in cookies so the server has them
|
|
||||||
|
|
||||||
const MARKETS_SORT = 'markets_sort'
|
|
||||||
|
|
||||||
function setSavedSort(s: Sort) {
|
|
||||||
safeLocalStorage()?.setItem(MARKETS_SORT, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSavedSort() {
|
|
||||||
return safeLocalStorage()?.getItem(MARKETS_SORT) as Sort | null | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchClient = algoliasearch(
|
const searchClient = algoliasearch(
|
||||||
'GJQPAYENIF',
|
'GJQPAYENIF',
|
||||||
'75c28fc084a80e1129d427d470cf41a3'
|
'75c28fc084a80e1129d427d470cf41a3'
|
||||||
|
@ -47,7 +38,7 @@ const searchClient = algoliasearch(
|
||||||
const indexPrefix = ENV === 'DEV' ? 'dev-' : ''
|
const indexPrefix = ENV === 'DEV' ? 'dev-' : ''
|
||||||
const searchIndexName = ENV === 'DEV' ? 'dev-contracts' : 'contractsIndex'
|
const searchIndexName = ENV === 'DEV' ? 'dev-contracts' : 'contractsIndex'
|
||||||
|
|
||||||
const sortOptions = [
|
const SORTS = [
|
||||||
{ label: 'Newest', value: 'newest' },
|
{ label: 'Newest', value: 'newest' },
|
||||||
{ label: 'Trending', value: 'score' },
|
{ label: 'Trending', value: 'score' },
|
||||||
{ label: 'Most traded', value: 'most-traded' },
|
{ label: 'Most traded', value: 'most-traded' },
|
||||||
|
@ -56,16 +47,17 @@ const sortOptions = [
|
||||||
{ label: 'Subsidy', value: 'liquidity' },
|
{ label: 'Subsidy', value: 'liquidity' },
|
||||||
{ label: 'Close date', value: 'close-date' },
|
{ label: 'Close date', value: 'close-date' },
|
||||||
{ label: 'Resolve date', value: 'resolve-date' },
|
{ label: 'Resolve date', value: 'resolve-date' },
|
||||||
]
|
] as const
|
||||||
|
|
||||||
|
export type Sort = typeof SORTS[number]['value']
|
||||||
|
|
||||||
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
|
type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all'
|
||||||
|
|
||||||
type SearchParameters = {
|
type SearchParameters = {
|
||||||
index: SearchIndex
|
|
||||||
query: string
|
query: string
|
||||||
numericFilters: SearchOptions['numericFilters']
|
sort: Sort
|
||||||
|
openClosedFilter: 'open' | 'closed' | undefined
|
||||||
facetFilters: SearchOptions['facetFilters']
|
facetFilters: SearchOptions['facetFilters']
|
||||||
showTime?: ShowTime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdditionalFilter = {
|
type AdditionalFilter = {
|
||||||
|
@ -88,8 +80,8 @@ export function ContractSearch(props: {
|
||||||
hideQuickBet?: boolean
|
hideQuickBet?: boolean
|
||||||
}
|
}
|
||||||
headerClassName?: string
|
headerClassName?: string
|
||||||
useQuerySortLocalStorage?: boolean
|
persistPrefix?: string
|
||||||
useQuerySortUrlParams?: boolean
|
useQueryUrlParam?: boolean
|
||||||
isWholePage?: boolean
|
isWholePage?: boolean
|
||||||
maxItems?: number
|
maxItems?: number
|
||||||
noControls?: boolean
|
noControls?: boolean
|
||||||
|
@ -104,66 +96,94 @@ export function ContractSearch(props: {
|
||||||
cardHideOptions,
|
cardHideOptions,
|
||||||
highlightOptions,
|
highlightOptions,
|
||||||
headerClassName,
|
headerClassName,
|
||||||
useQuerySortLocalStorage,
|
persistPrefix,
|
||||||
useQuerySortUrlParams,
|
useQueryUrlParam,
|
||||||
isWholePage,
|
isWholePage,
|
||||||
maxItems,
|
maxItems,
|
||||||
noControls,
|
noControls,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [numPages, setNumPages] = useState(1)
|
const [state, setState] = usePersistentState(
|
||||||
const [pages, setPages] = useState<Contract[][]>([])
|
{
|
||||||
const [showTime, setShowTime] = useState<ShowTime | undefined>()
|
numPages: 1,
|
||||||
|
pages: [] as Contract[][],
|
||||||
|
showTime: null as ShowTime | null,
|
||||||
|
},
|
||||||
|
!persistPrefix
|
||||||
|
? undefined
|
||||||
|
: { key: `${persistPrefix}-search`, store: historyStore() }
|
||||||
|
)
|
||||||
|
|
||||||
const searchParameters = useRef<SearchParameters | undefined>()
|
const searchParams = useRef<SearchParameters | null>(null)
|
||||||
|
const searchParamsStore = historyStore<SearchParameters>()
|
||||||
const requestId = useRef(0)
|
const requestId = useRef(0)
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (persistPrefix) {
|
||||||
|
const params = searchParamsStore.get(`${persistPrefix}-params`)
|
||||||
|
if (params !== undefined) {
|
||||||
|
searchParams.current = params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const searchIndex = useMemo(
|
||||||
|
() => searchClient.initIndex(searchIndexName),
|
||||||
|
[searchIndexName]
|
||||||
|
)
|
||||||
|
|
||||||
const performQuery = async (freshQuery?: boolean) => {
|
const performQuery = async (freshQuery?: boolean) => {
|
||||||
if (searchParameters.current === undefined) {
|
if (searchParams.current == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const params = searchParameters.current
|
const { query, sort, openClosedFilter, facetFilters } = searchParams.current
|
||||||
const id = ++requestId.current
|
const id = ++requestId.current
|
||||||
const requestedPage = freshQuery ? 0 : pages.length
|
const requestedPage = freshQuery ? 0 : state.pages.length
|
||||||
if (freshQuery || requestedPage < numPages) {
|
if (freshQuery || requestedPage < state.numPages) {
|
||||||
const results = await params.index.search(params.query, {
|
const index = query
|
||||||
facetFilters: params.facetFilters,
|
? searchIndex
|
||||||
numericFilters: params.numericFilters,
|
: searchClient.initIndex(`${indexPrefix}contracts-${sort}`)
|
||||||
|
const numericFilters = query
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
openClosedFilter === 'open' ? `closeTime > ${Date.now()}` : '',
|
||||||
|
openClosedFilter === 'closed' ? `closeTime <= ${Date.now()}` : '',
|
||||||
|
].filter((f) => f)
|
||||||
|
const results = await index.search(query, {
|
||||||
|
facetFilters,
|
||||||
|
numericFilters,
|
||||||
page: requestedPage,
|
page: requestedPage,
|
||||||
hitsPerPage: 20,
|
hitsPerPage: 20,
|
||||||
})
|
})
|
||||||
// if there's a more recent request, forget about this one
|
// if there's a more recent request, forget about this one
|
||||||
if (id === requestId.current) {
|
if (id === requestId.current) {
|
||||||
const newPage = results.hits as any as Contract[]
|
const newPage = results.hits as any as Contract[]
|
||||||
// this spooky looking function is the easiest way to get react to
|
const showTime =
|
||||||
// batch this and not do multiple renders. we can throw it out in react 18.
|
sort === 'close-date' || sort === 'resolve-date' ? sort : null
|
||||||
// see https://github.com/reactwg/react-18/discussions/21
|
const pages = freshQuery ? [newPage] : [...state.pages, newPage]
|
||||||
unstable_batchedUpdates(() => {
|
setState({ numPages: results.nbPages, pages, showTime })
|
||||||
setShowTime(params.showTime)
|
if (freshQuery && isWholePage) window.scrollTo(0, 0)
|
||||||
setNumPages(results.nbPages)
|
|
||||||
if (freshQuery) {
|
|
||||||
setPages([newPage])
|
|
||||||
if (isWholePage) window.scrollTo(0, 0)
|
|
||||||
} else {
|
|
||||||
setPages((pages) => [...pages, newPage])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSearchParametersChanged = useRef(
|
const onSearchParametersChanged = useRef(
|
||||||
debounce((params) => {
|
debounce((params) => {
|
||||||
searchParameters.current = params
|
if (!isEqual(searchParams.current, params)) {
|
||||||
performQuery(true)
|
if (persistPrefix) {
|
||||||
|
searchParamsStore.set(`${persistPrefix}-params`, params)
|
||||||
|
}
|
||||||
|
searchParams.current = params
|
||||||
|
performQuery(true)
|
||||||
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
).current
|
).current
|
||||||
|
|
||||||
const contracts = pages
|
const contracts = state.pages
|
||||||
.flat()
|
.flat()
|
||||||
.filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id))
|
.filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id))
|
||||||
const renderedContracts =
|
const renderedContracts =
|
||||||
pages.length === 0 ? undefined : contracts.slice(0, maxItems)
|
state.pages.length === 0 ? undefined : contracts.slice(0, maxItems)
|
||||||
|
|
||||||
if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
|
if (IS_PRIVATE_MANIFOLD || process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
|
||||||
return <ContractSearchFirestore additionalFilter={additionalFilter} />
|
return <ContractSearchFirestore additionalFilter={additionalFilter} />
|
||||||
|
@ -177,8 +197,8 @@ export function ContractSearch(props: {
|
||||||
defaultFilter={defaultFilter}
|
defaultFilter={defaultFilter}
|
||||||
additionalFilter={additionalFilter}
|
additionalFilter={additionalFilter}
|
||||||
hideOrderSelector={hideOrderSelector}
|
hideOrderSelector={hideOrderSelector}
|
||||||
useQuerySortLocalStorage={useQuerySortLocalStorage}
|
persistPrefix={persistPrefix ? `${persistPrefix}-controls` : undefined}
|
||||||
useQuerySortUrlParams={useQuerySortUrlParams}
|
useQueryUrlParam={useQueryUrlParam}
|
||||||
user={user}
|
user={user}
|
||||||
onSearchParametersChanged={onSearchParametersChanged}
|
onSearchParametersChanged={onSearchParametersChanged}
|
||||||
noControls={noControls}
|
noControls={noControls}
|
||||||
|
@ -186,7 +206,7 @@ export function ContractSearch(props: {
|
||||||
<ContractsGrid
|
<ContractsGrid
|
||||||
contracts={renderedContracts}
|
contracts={renderedContracts}
|
||||||
loadMore={noControls ? undefined : performQuery}
|
loadMore={noControls ? undefined : performQuery}
|
||||||
showTime={showTime}
|
showTime={state.showTime ?? undefined}
|
||||||
onContractClick={onContractClick}
|
onContractClick={onContractClick}
|
||||||
highlightOptions={highlightOptions}
|
highlightOptions={highlightOptions}
|
||||||
cardHideOptions={cardHideOptions}
|
cardHideOptions={cardHideOptions}
|
||||||
|
@ -202,8 +222,8 @@ function ContractSearchControls(props: {
|
||||||
additionalFilter?: AdditionalFilter
|
additionalFilter?: AdditionalFilter
|
||||||
hideOrderSelector?: boolean
|
hideOrderSelector?: boolean
|
||||||
onSearchParametersChanged: (params: SearchParameters) => void
|
onSearchParametersChanged: (params: SearchParameters) => void
|
||||||
useQuerySortLocalStorage?: boolean
|
persistPrefix?: string
|
||||||
useQuerySortUrlParams?: boolean
|
useQueryUrlParam?: boolean
|
||||||
user?: User | null
|
user?: User | null
|
||||||
noControls?: boolean
|
noControls?: boolean
|
||||||
}) {
|
}) {
|
||||||
|
@ -214,25 +234,36 @@ function ContractSearchControls(props: {
|
||||||
additionalFilter,
|
additionalFilter,
|
||||||
hideOrderSelector,
|
hideOrderSelector,
|
||||||
onSearchParametersChanged,
|
onSearchParametersChanged,
|
||||||
useQuerySortLocalStorage,
|
persistPrefix,
|
||||||
useQuerySortUrlParams,
|
useQueryUrlParam,
|
||||||
user,
|
user,
|
||||||
noControls,
|
noControls,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const savedSort = useQuerySortLocalStorage ? getSavedSort() : null
|
const router = useRouter()
|
||||||
const initialSort = savedSort ?? defaultSort ?? 'score'
|
const [query, setQuery] = usePersistentState(
|
||||||
const querySortOpts = { useUrl: !!useQuerySortUrlParams }
|
'',
|
||||||
const [sort, setSort] = useSort(initialSort, querySortOpts)
|
!useQueryUrlParam
|
||||||
const [query, setQuery] = useQuery('', querySortOpts)
|
? undefined
|
||||||
const [filter, setFilter] = useState<filter>(defaultFilter ?? 'open')
|
: {
|
||||||
const [pillFilter, setPillFilter] = useState<string | undefined>(undefined)
|
key: 'q',
|
||||||
|
store: urlParamStore(router),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const [state, setState] = usePersistentState(
|
||||||
if (useQuerySortLocalStorage) {
|
{
|
||||||
setSavedSort(sort)
|
sort: defaultSort ?? 'score',
|
||||||
}
|
filter: defaultFilter ?? 'open',
|
||||||
}, [sort])
|
pillFilter: null as string | null,
|
||||||
|
},
|
||||||
|
!persistPrefix
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
key: `${persistPrefix}-params`,
|
||||||
|
store: storageStore(safeLocalStorage()),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const follows = useFollows(user?.id)
|
const follows = useFollows(user?.id)
|
||||||
const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
|
const memberGroups = (useMemberGroups(user?.id) ?? []).filter(
|
||||||
|
@ -266,14 +297,16 @@ function ContractSearchControls(props: {
|
||||||
...additionalFilters,
|
...additionalFilters,
|
||||||
additionalFilter ? '' : 'visibility:public',
|
additionalFilter ? '' : 'visibility:public',
|
||||||
|
|
||||||
filter === 'open' ? 'isResolved:false' : '',
|
state.filter === 'open' ? 'isResolved:false' : '',
|
||||||
filter === 'closed' ? 'isResolved:false' : '',
|
state.filter === 'closed' ? 'isResolved:false' : '',
|
||||||
filter === 'resolved' ? 'isResolved:true' : '',
|
state.filter === 'resolved' ? 'isResolved:true' : '',
|
||||||
|
|
||||||
pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets'
|
state.pillFilter &&
|
||||||
? `groupLinks.slug:${pillFilter}`
|
state.pillFilter !== 'personal' &&
|
||||||
|
state.pillFilter !== 'your-bets'
|
||||||
|
? `groupLinks.slug:${state.pillFilter}`
|
||||||
: '',
|
: '',
|
||||||
pillFilter === 'personal'
|
state.pillFilter === 'personal'
|
||||||
? // Show contracts in groups that the user is a member of
|
? // Show contracts in groups that the user is a member of
|
||||||
memberGroupSlugs
|
memberGroupSlugs
|
||||||
.map((slug) => `groupLinks.slug:${slug}`)
|
.map((slug) => `groupLinks.slug:${slug}`)
|
||||||
|
@ -285,22 +318,24 @@ function ContractSearchControls(props: {
|
||||||
)
|
)
|
||||||
: '',
|
: '',
|
||||||
// Subtract contracts you bet on from For you.
|
// Subtract contracts you bet on from For you.
|
||||||
pillFilter === 'personal' && user ? `uniqueBettorIds:-${user.id}` : '',
|
state.pillFilter === 'personal' && user
|
||||||
pillFilter === 'your-bets' && user
|
? `uniqueBettorIds:-${user.id}`
|
||||||
|
: '',
|
||||||
|
state.pillFilter === 'your-bets' && user
|
||||||
? // Show contracts bet on by the user
|
? // Show contracts bet on by the user
|
||||||
`uniqueBettorIds:${user.id}`
|
`uniqueBettorIds:${user.id}`
|
||||||
: '',
|
: '',
|
||||||
].filter((f) => f)
|
].filter((f) => f)
|
||||||
|
|
||||||
const numericFilters = query
|
const openClosedFilter =
|
||||||
? []
|
state.filter === 'open'
|
||||||
: [
|
? 'open'
|
||||||
filter === 'open' ? `closeTime > ${Date.now()}` : '',
|
: state.filter === 'closed'
|
||||||
filter === 'closed' ? `closeTime <= ${Date.now()}` : '',
|
? 'closed'
|
||||||
].filter((f) => f)
|
: undefined
|
||||||
|
|
||||||
const selectPill = (pill: string | undefined) => () => {
|
const selectPill = (pill: string | null) => () => {
|
||||||
setPillFilter(pill)
|
setState({ ...state, pillFilter: pill })
|
||||||
track('select search category', { category: pill ?? 'all' })
|
track('select search category', { category: pill ?? 'all' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,34 +344,25 @@ function ContractSearchControls(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectFilter = (newFilter: filter) => {
|
const selectFilter = (newFilter: filter) => {
|
||||||
if (newFilter === filter) return
|
if (newFilter === state.filter) return
|
||||||
setFilter(newFilter)
|
setState({ ...state, filter: newFilter })
|
||||||
track('select search filter', { filter: newFilter })
|
track('select search filter', { filter: newFilter })
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectSort = (newSort: Sort) => {
|
const selectSort = (newSort: Sort) => {
|
||||||
if (newSort === sort) return
|
if (newSort === state.sort) return
|
||||||
setSort(newSort)
|
setState({ ...state, sort: newSort })
|
||||||
track('select search sort', { sort: newSort })
|
track('select search sort', { sort: newSort })
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexName = `${indexPrefix}contracts-${sort}`
|
|
||||||
const index = useMemo(() => searchClient.initIndex(indexName), [indexName])
|
|
||||||
const searchIndex = useMemo(
|
|
||||||
() => searchClient.initIndex(searchIndexName),
|
|
||||||
[searchIndexName]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onSearchParametersChanged({
|
onSearchParametersChanged({
|
||||||
index: query ? searchIndex : index,
|
|
||||||
query: query,
|
query: query,
|
||||||
numericFilters: numericFilters,
|
sort: state.sort,
|
||||||
|
openClosedFilter: openClosedFilter,
|
||||||
facetFilters: facetFilters,
|
facetFilters: facetFilters,
|
||||||
showTime:
|
|
||||||
sort === 'close-date' || sort === 'resolve-date' ? sort : undefined,
|
|
||||||
})
|
})
|
||||||
}, [query, index, searchIndex, filter, JSON.stringify(facetFilters)])
|
}, [query, state.sort, openClosedFilter, JSON.stringify(facetFilters)])
|
||||||
|
|
||||||
if (noControls) {
|
if (noControls) {
|
||||||
return <></>
|
return <></>
|
||||||
|
@ -351,14 +377,14 @@ function ContractSearchControls(props: {
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => updateQuery(e.target.value)}
|
onChange={(e) => updateQuery(e.target.value)}
|
||||||
onBlur={trackCallback('search', { query })}
|
onBlur={trackCallback('search', { query: query })}
|
||||||
placeholder={'Search'}
|
placeholder={'Search'}
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
/>
|
/>
|
||||||
{!query && (
|
{!query && (
|
||||||
<select
|
<select
|
||||||
className="select select-bordered"
|
className="select select-bordered"
|
||||||
value={filter}
|
value={state.filter}
|
||||||
onChange={(e) => selectFilter(e.target.value as filter)}
|
onChange={(e) => selectFilter(e.target.value as filter)}
|
||||||
>
|
>
|
||||||
<option value="open">Open</option>
|
<option value="open">Open</option>
|
||||||
|
@ -370,10 +396,10 @@ function ContractSearchControls(props: {
|
||||||
{!hideOrderSelector && !query && (
|
{!hideOrderSelector && !query && (
|
||||||
<select
|
<select
|
||||||
className="select select-bordered"
|
className="select select-bordered"
|
||||||
value={sort}
|
value={state.sort}
|
||||||
onChange={(e) => selectSort(e.target.value as Sort)}
|
onChange={(e) => selectSort(e.target.value as Sort)}
|
||||||
>
|
>
|
||||||
{sortOptions.map((option) => (
|
{SORTS.map((option) => (
|
||||||
<option key={option.value} value={option.value}>
|
<option key={option.value} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
</option>
|
</option>
|
||||||
|
@ -386,14 +412,14 @@ function ContractSearchControls(props: {
|
||||||
<Row className="scrollbar-hide items-start gap-2 overflow-x-auto">
|
<Row className="scrollbar-hide items-start gap-2 overflow-x-auto">
|
||||||
<PillButton
|
<PillButton
|
||||||
key={'all'}
|
key={'all'}
|
||||||
selected={pillFilter === undefined}
|
selected={state.pillFilter === undefined}
|
||||||
onSelect={selectPill(undefined)}
|
onSelect={selectPill(null)}
|
||||||
>
|
>
|
||||||
All
|
All
|
||||||
</PillButton>
|
</PillButton>
|
||||||
<PillButton
|
<PillButton
|
||||||
key={'personal'}
|
key={'personal'}
|
||||||
selected={pillFilter === 'personal'}
|
selected={state.pillFilter === 'personal'}
|
||||||
onSelect={selectPill('personal')}
|
onSelect={selectPill('personal')}
|
||||||
>
|
>
|
||||||
{user ? 'For you' : 'Featured'}
|
{user ? 'For you' : 'Featured'}
|
||||||
|
@ -402,7 +428,7 @@ function ContractSearchControls(props: {
|
||||||
{user && (
|
{user && (
|
||||||
<PillButton
|
<PillButton
|
||||||
key={'your-bets'}
|
key={'your-bets'}
|
||||||
selected={pillFilter === 'your-bets'}
|
selected={state.pillFilter === 'your-bets'}
|
||||||
onSelect={selectPill('your-bets')}
|
onSelect={selectPill('your-bets')}
|
||||||
>
|
>
|
||||||
Your bets
|
Your bets
|
||||||
|
@ -413,7 +439,7 @@ function ContractSearchControls(props: {
|
||||||
return (
|
return (
|
||||||
<PillButton
|
<PillButton
|
||||||
key={slug}
|
key={slug}
|
||||||
selected={pillFilter === slug}
|
selected={state.pillFilter === slug}
|
||||||
onSelect={selectPill(slug)}
|
onSelect={selectPill(slug)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
|
|
106
web/hooks/use-persistent-state.ts
Normal file
106
web/hooks/use-persistent-state.ts
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useStateCheckEquality } from './use-state-check-equality'
|
||||||
|
import { NextRouter } from 'next/router'
|
||||||
|
|
||||||
|
export type PersistenceOptions<T> = { key: string; store: PersistentStore<T> }
|
||||||
|
|
||||||
|
export interface PersistentStore<T> {
|
||||||
|
get: (k: string) => T | undefined
|
||||||
|
set: (k: string, v: T | undefined) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const withURLParam = (location: Location, k: string, v?: string) => {
|
||||||
|
const newParams = new URLSearchParams(location.search)
|
||||||
|
if (!v) {
|
||||||
|
newParams.delete(k)
|
||||||
|
} else {
|
||||||
|
newParams.set(k, v)
|
||||||
|
}
|
||||||
|
const newUrl = new URL(location.href)
|
||||||
|
newUrl.search = newParams.toString()
|
||||||
|
return newUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
export const storageStore = <T>(storage?: Storage): PersistentStore<T> => ({
|
||||||
|
get: (k: string) => {
|
||||||
|
if (!storage) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const saved = storage.getItem(k)
|
||||||
|
if (typeof saved === 'string') {
|
||||||
|
try {
|
||||||
|
return JSON.parse(saved) as T
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: (k: string, v: T | undefined) => {
|
||||||
|
if (storage) {
|
||||||
|
if (v === undefined) {
|
||||||
|
storage.removeItem(k)
|
||||||
|
} else {
|
||||||
|
storage.setItem(k, JSON.stringify(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const urlParamStore = (router: NextRouter): PersistentStore<string> => ({
|
||||||
|
get: (k: string) => {
|
||||||
|
const v = router.query[k]
|
||||||
|
return typeof v === 'string' ? v : undefined
|
||||||
|
},
|
||||||
|
set: (k: string, v: string | undefined) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// see relevant discussion here https://github.com/vercel/next.js/discussions/18072
|
||||||
|
const url = withURLParam(window.location, k, v).toString()
|
||||||
|
const updatedState = { ...window.history.state, as: url, url }
|
||||||
|
window.history.replaceState(updatedState, '', url)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const historyStore = <T>(prefix = '__manifold'): PersistentStore<T> => ({
|
||||||
|
get: (k: string) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return window.history.state?.options?.[prefix]?.[k] as T | undefined
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set: (k: string, v: T | undefined) => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const state = window.history.state ?? {}
|
||||||
|
const options = state.options ?? {}
|
||||||
|
const inner = options[prefix] ?? {}
|
||||||
|
window.history.replaceState(
|
||||||
|
{
|
||||||
|
...state,
|
||||||
|
options: { ...options, [prefix]: { ...inner, [k]: v } },
|
||||||
|
},
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const usePersistentState = <T>(
|
||||||
|
initial: T,
|
||||||
|
persist?: PersistenceOptions<T>
|
||||||
|
) => {
|
||||||
|
const store = persist?.store
|
||||||
|
const key = persist?.key
|
||||||
|
// note that it's important in some cases to get the state correct during the
|
||||||
|
// first render, or scroll restoration won't take into account the saved state
|
||||||
|
const savedValue = key != null && store != null ? store.get(key) : undefined
|
||||||
|
const [state, setState] = useStateCheckEquality(savedValue ?? initial)
|
||||||
|
useEffect(() => {
|
||||||
|
if (key != null && store != null) {
|
||||||
|
store.set(key, state)
|
||||||
|
}
|
||||||
|
}, [key, state])
|
||||||
|
return [state, setState] as const
|
||||||
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { useEffect, useRef } from 'react'
|
|
||||||
|
|
||||||
// From: https://jak-ch-ll.medium.com/next-js-preserve-scroll-history-334cf699802a
|
|
||||||
export const usePreserveScroll = () => {
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const scrollPositions = useRef<{ [url: string]: number }>({})
|
|
||||||
const isBack = useRef(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
router.beforePopState(() => {
|
|
||||||
isBack.current = true
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
const onRouteChangeStart = () => {
|
|
||||||
const url = router.pathname
|
|
||||||
scrollPositions.current[url] = window.scrollY
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRouteChangeComplete = (url: any) => {
|
|
||||||
if (isBack.current && scrollPositions.current[url]) {
|
|
||||||
window.scroll({
|
|
||||||
top: scrollPositions.current[url],
|
|
||||||
behavior: 'auto',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
isBack.current = false
|
|
||||||
}
|
|
||||||
|
|
||||||
router.events.on('routeChangeStart', onRouteChangeStart)
|
|
||||||
router.events.on('routeChangeComplete', onRouteChangeComplete)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
router.events.off('routeChangeStart', onRouteChangeStart)
|
|
||||||
router.events.off('routeChangeComplete', onRouteChangeComplete)
|
|
||||||
}
|
|
||||||
}, [router])
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
import { useState } from 'react'
|
|
||||||
import { NextRouter, useRouter } from 'next/router'
|
|
||||||
|
|
||||||
export type Sort =
|
|
||||||
| 'newest'
|
|
||||||
| 'oldest'
|
|
||||||
| 'most-traded'
|
|
||||||
| '24-hour-vol'
|
|
||||||
| 'close-date'
|
|
||||||
| 'resolve-date'
|
|
||||||
| 'last-updated'
|
|
||||||
| 'score'
|
|
||||||
|
|
||||||
type UpdatedQueryParams = { [k: string]: string }
|
|
||||||
type QuerySortOpts = { useUrl: boolean }
|
|
||||||
|
|
||||||
function withURLParams(location: Location, params: UpdatedQueryParams) {
|
|
||||||
const newParams = new URLSearchParams(location.search)
|
|
||||||
for (const [k, v] of Object.entries(params)) {
|
|
||||||
if (!v) {
|
|
||||||
newParams.delete(k)
|
|
||||||
} else {
|
|
||||||
newParams.set(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const newUrl = new URL(location.href)
|
|
||||||
newUrl.search = newParams.toString()
|
|
||||||
return newUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateURL(params: UpdatedQueryParams) {
|
|
||||||
// see relevant discussion here https://github.com/vercel/next.js/discussions/18072
|
|
||||||
const url = withURLParams(window.location, params).toString()
|
|
||||||
const updatedState = { ...window.history.state, as: url, url }
|
|
||||||
window.history.replaceState(updatedState, '', url)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStringURLParam(router: NextRouter, k: string) {
|
|
||||||
const v = router.query[k]
|
|
||||||
return typeof v === 'string' ? v : null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useQuery(defaultQuery: string, opts?: QuerySortOpts) {
|
|
||||||
const useUrl = opts?.useUrl ?? false
|
|
||||||
const router = useRouter()
|
|
||||||
const initialQuery = useUrl ? getStringURLParam(router, 'q') : null
|
|
||||||
const [query, setQuery] = useState(initialQuery ?? defaultQuery)
|
|
||||||
if (!useUrl) {
|
|
||||||
return [query, setQuery] as const
|
|
||||||
} else {
|
|
||||||
return [query, (q: string) => (setQuery(q), updateURL({ q }))] as const
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSort(defaultSort: Sort, opts?: QuerySortOpts) {
|
|
||||||
const useUrl = opts?.useUrl ?? false
|
|
||||||
const router = useRouter()
|
|
||||||
const initialSort = useUrl ? (getStringURLParam(router, 's') as Sort) : null
|
|
||||||
const [sort, setSort] = useState(initialSort ?? defaultSort)
|
|
||||||
if (!useUrl) {
|
|
||||||
return [sort, setSort] as const
|
|
||||||
} else {
|
|
||||||
return [sort, (s: Sort) => (setSort(s), updateURL({ s }))] as const
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,7 @@
|
||||||
export const safeLocalStorage = () => (isLocalStorage() ? localStorage : null)
|
export const safeLocalStorage = () =>
|
||||||
|
isLocalStorage() ? localStorage : undefined
|
||||||
|
export const safeSessionStorage = () =>
|
||||||
|
isSessionStorage() ? sessionStorage : undefined
|
||||||
|
|
||||||
const isLocalStorage = () => {
|
const isLocalStorage = () => {
|
||||||
try {
|
try {
|
||||||
|
@ -9,3 +12,13 @@ const isLocalStorage = () => {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSessionStorage = () => {
|
||||||
|
try {
|
||||||
|
sessionStorage.getItem('test')
|
||||||
|
sessionStorage.setItem('hi', 'mom')
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ module.exports = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
optimizeFonts: false,
|
optimizeFonts: false,
|
||||||
experimental: {
|
experimental: {
|
||||||
|
scrollRestoration: true,
|
||||||
externalDir: true,
|
externalDir: true,
|
||||||
modularizeImports: {
|
modularizeImports: {
|
||||||
'@heroicons/react/solid/?(((\\w*)?/?)*)': {
|
'@heroicons/react/solid/?(((\\w*)?/?)*)': {
|
||||||
|
|
|
@ -3,7 +3,6 @@ import type { AppProps } from 'next/app'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import Script from 'next/script'
|
import Script from 'next/script'
|
||||||
import { usePreserveScroll } from 'web/hooks/use-preserve-scroll'
|
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||||
import { AuthProvider } from 'web/components/auth-context'
|
import { AuthProvider } from 'web/components/auth-context'
|
||||||
import Welcome from 'web/components/onboarding/welcome'
|
import Welcome from 'web/components/onboarding/welcome'
|
||||||
|
@ -26,8 +25,6 @@ function printBuildInfo() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
usePreserveScroll()
|
|
||||||
|
|
||||||
useEffect(printBuildInfo, [])
|
useEffect(printBuildInfo, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
import { Answer } from 'common/answer'
|
import { Answer } from 'common/answer'
|
||||||
import { searchInAny } from 'common/util/parse'
|
import { searchInAny } from 'common/util/parse'
|
||||||
import { sortBy } from 'lodash'
|
import { sortBy } from 'lodash'
|
||||||
import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
||||||
import { useContracts } from 'web/hooks/use-contracts'
|
import { useContracts } from 'web/hooks/use-contracts'
|
||||||
import { Sort, useQuery, useSort } from 'web/hooks/use-sort-and-query-params'
|
import {
|
||||||
|
usePersistentState,
|
||||||
|
urlParamStore,
|
||||||
|
} from 'web/hooks/use-persistent-state'
|
||||||
|
|
||||||
const MAX_CONTRACTS_RENDERED = 100
|
const MAX_CONTRACTS_RENDERED = 100
|
||||||
|
|
||||||
|
@ -15,10 +19,12 @@ export default function ContractSearchFirestore(props: {
|
||||||
groupSlug?: string
|
groupSlug?: string
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
const contracts = useContracts()
|
|
||||||
const { additionalFilter } = props
|
const { additionalFilter } = props
|
||||||
const [query, setQuery] = useQuery('', { useUrl: true })
|
const contracts = useContracts()
|
||||||
const [sort, setSort] = useSort('score', { useUrl: true })
|
const router = useRouter()
|
||||||
|
const store = urlParamStore(router)
|
||||||
|
const [query, setQuery] = usePersistentState('', { key: 'q', store })
|
||||||
|
const [sort, setSort] = usePersistentState('score', { key: 'sort', store })
|
||||||
|
|
||||||
let matches = (contracts ?? []).filter((c) =>
|
let matches = (contracts ?? []).filter((c) =>
|
||||||
searchInAny(
|
searchInAny(
|
||||||
|
@ -34,8 +40,6 @@ export default function ContractSearchFirestore(props: {
|
||||||
matches.sort((a, b) => b.createdTime - a.createdTime)
|
matches.sort((a, b) => b.createdTime - a.createdTime)
|
||||||
} else if (sort === 'resolve-date') {
|
} else if (sort === 'resolve-date') {
|
||||||
matches = sortBy(matches, (contract) => -1 * (contract.resolutionTime ?? 0))
|
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') {
|
} else if (sort === 'close-date') {
|
||||||
matches = sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
|
matches = sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours)
|
||||||
matches = sortBy(matches, (contract) => contract.closeTime ?? Infinity)
|
matches = sortBy(matches, (contract) => contract.closeTime ?? Infinity)
|
||||||
|
@ -93,7 +97,7 @@ export default function ContractSearchFirestore(props: {
|
||||||
<select
|
<select
|
||||||
className="select select-bordered"
|
className="select select-bordered"
|
||||||
value={sort}
|
value={sort}
|
||||||
onChange={(e) => setSort(e.target.value as Sort)}
|
onChange={(e) => setSort(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="score">Trending</option>
|
<option value="score">Trending</option>
|
||||||
<option value="newest">Newest</option>
|
<option value="newest">Newest</option>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { track } from 'web/lib/service/analytics'
|
||||||
import { authenticateOnServer } from 'web/lib/firebase/server-auth'
|
import { authenticateOnServer } from 'web/lib/firebase/server-auth'
|
||||||
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
import { useSaveReferral } from 'web/hooks/use-save-referral'
|
||||||
import { GetServerSideProps } from 'next'
|
import { GetServerSideProps } from 'next'
|
||||||
import { Sort } from 'web/hooks/use-sort-and-query-params'
|
import { Sort } from 'web/components/contract-search'
|
||||||
import { Button } from 'web/components/button'
|
import { Button } from 'web/components/button'
|
||||||
import { Spacer } from 'web/components/layout/spacer'
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
import { useMemberGroups } from 'web/hooks/use-group'
|
import { useMemberGroups } from 'web/hooks/use-group'
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { PencilAltIcon } from '@heroicons/react/solid'
|
import { PencilAltIcon } from '@heroicons/react/solid'
|
||||||
|
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { ContractSearch } from 'web/components/contract-search'
|
import { ContractSearch } from 'web/components/contract-search'
|
||||||
import { Contract } from 'common/contract'
|
|
||||||
import { User } from 'common/user'
|
import { User } from 'common/user'
|
||||||
import { ContractPageContent } from './[username]/[contractSlug]'
|
|
||||||
import { getContractFromSlug } from 'web/lib/firebase/contracts'
|
|
||||||
import { getUserAndPrivateUser } from 'web/lib/firebase/users'
|
import { getUserAndPrivateUser } from 'web/lib/firebase/users'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { track } from 'web/lib/service/analytics'
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
@ -25,8 +21,6 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
|
|
||||||
const Home = (props: { auth: { user: User } | null }) => {
|
const Home = (props: { auth: { user: User } | null }) => {
|
||||||
const user = props.auth ? props.auth.user : null
|
const user = props.auth ? props.auth.user : null
|
||||||
const [contract, setContract] = useContractPage()
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
useTracking('view home')
|
useTracking('view home')
|
||||||
|
|
||||||
|
@ -35,19 +29,12 @@ const Home = (props: { auth: { user: User } | null }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Page className={contract ? 'sr-only' : ''}>
|
<Page>
|
||||||
<Col className="mx-auto w-full p-2">
|
<Col className="mx-auto w-full p-2">
|
||||||
<ContractSearch
|
<ContractSearch
|
||||||
user={user}
|
user={user}
|
||||||
useQuerySortLocalStorage={true}
|
persistPrefix="home-search"
|
||||||
useQuerySortUrlParams={true}
|
useQueryUrlParam={true}
|
||||||
onContractClick={(c) => {
|
|
||||||
// Show contract without navigating to contract page.
|
|
||||||
setContract(c)
|
|
||||||
// Update the url without switching pages in Nextjs.
|
|
||||||
history.pushState(null, '', `/${c.creatorUsername}/${c.slug}`)
|
|
||||||
}}
|
|
||||||
isWholePage
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<button
|
<button
|
||||||
|
@ -61,81 +48,8 @@ const Home = (props: { auth: { user: User } | null }) => {
|
||||||
<PencilAltIcon className="h-7 w-7" aria-hidden="true" />
|
<PencilAltIcon className="h-7 w-7" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
{contract && (
|
|
||||||
<ContractPageContent
|
|
||||||
contract={contract}
|
|
||||||
user={user}
|
|
||||||
username={contract.creatorUsername}
|
|
||||||
slug={contract.slug}
|
|
||||||
bets={[]}
|
|
||||||
comments={[]}
|
|
||||||
backToHome={() => {
|
|
||||||
history.back()
|
|
||||||
}}
|
|
||||||
recommendedContracts={[]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const useContractPage = () => {
|
|
||||||
const [contract, setContract] = useState<Contract | undefined>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const updateContract = () => {
|
|
||||||
const path = location.pathname.split('/').slice(1)
|
|
||||||
if (path[0] === 'home') setContract(undefined)
|
|
||||||
else {
|
|
||||||
const [username, contractSlug] = path
|
|
||||||
if (!username || !contractSlug) setContract(undefined)
|
|
||||||
else {
|
|
||||||
// Show contract if route is to a contract: '/[username]/[contractSlug]'.
|
|
||||||
getContractFromSlug(contractSlug).then((contract) => {
|
|
||||||
const path = location.pathname.split('/').slice(1)
|
|
||||||
const [_username, contractSlug] = path
|
|
||||||
// Make sure we're still on the same contract.
|
|
||||||
if (contract?.slug === contractSlug) setContract(contract)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addEventListener('popstate', updateContract)
|
|
||||||
|
|
||||||
const { pushState, replaceState } = window.history
|
|
||||||
|
|
||||||
window.history.pushState = function () {
|
|
||||||
// eslint-disable-next-line prefer-rest-params
|
|
||||||
const args = [...(arguments as any)] as any
|
|
||||||
// Discard NextJS router state.
|
|
||||||
args[0] = null
|
|
||||||
pushState.apply(history, args)
|
|
||||||
updateContract()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.history.replaceState = function () {
|
|
||||||
// eslint-disable-next-line prefer-rest-params
|
|
||||||
const args = [...(arguments as any)] as any
|
|
||||||
// Discard NextJS router state.
|
|
||||||
args[0] = null
|
|
||||||
replaceState.apply(history, args)
|
|
||||||
updateContract()
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
removeEventListener('popstate', updateContract)
|
|
||||||
window.history.pushState = pushState
|
|
||||||
window.history.replaceState = replaceState
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (contract) window.scrollTo(0, 0)
|
|
||||||
}, [contract])
|
|
||||||
|
|
||||||
return [contract, setContract] as const
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Home
|
export default Home
|
||||||
|
|
Loading…
Reference in New Issue
Block a user