From 1369f3b967c59ca83d806afb7f0ffe1414114092 Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Mon, 29 Aug 2022 21:56:11 -0700 Subject: [PATCH] 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 --- common/util/object.ts | 3 +- web/components/contract-search.tsx | 260 +++++++++++++----------- web/hooks/use-persistent-state.ts | 106 ++++++++++ web/hooks/use-preserve-scroll.ts | 41 ---- web/hooks/use-sort-and-query-params.tsx | 65 ------ web/lib/util/local.ts | 15 +- web/next.config.js | 1 + web/pages/_app.tsx | 3 - web/pages/contract-search-firestore.tsx | 18 +- web/pages/experimental/home.tsx | 2 +- web/pages/home.tsx | 92 +-------- 11 files changed, 280 insertions(+), 326 deletions(-) create mode 100644 web/hooks/use-persistent-state.ts delete mode 100644 web/hooks/use-preserve-scroll.ts delete mode 100644 web/hooks/use-sort-and-query-params.tsx diff --git a/common/util/object.ts b/common/util/object.ts index 5596286e..41d2cd70 100644 --- a/common/util/object.ts +++ b/common/util/object.ts @@ -1,6 +1,6 @@ import { union } from 'lodash' -export const removeUndefinedProps = (obj: T): T => { +export const removeUndefinedProps = (obj: T): T => { const newObj: any = {} for (const key of Object.keys(obj)) { @@ -37,4 +37,3 @@ export const subtractObjects = ( return newObj as T } - diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index 34e1ff0d..fa6ea204 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -1,44 +1,35 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import algoliasearch, { SearchIndex } from 'algoliasearch/lite' +import algoliasearch from 'algoliasearch/lite' import { SearchOptions } from '@algolia/client-search' - +import { useRouter } from 'next/router' import { Contract } from 'common/contract' import { User } from 'common/user' -import { Sort, useQuery, useSort } from '../hooks/use-sort-and-query-params' import { ContractHighlightOptions, ContractsGrid, } from './contract/contracts-grid' import { ShowTime } from './contract/contract-details' import { Row } from './layout/row' -import { useEffect, useRef, useMemo, useState } from 'react' -import { unstable_batchedUpdates } from 'react-dom' +import { useEffect, useLayoutEffect, useRef, useMemo } from 'react' import { ENV, IS_PRIVATE_MANIFOLD } from 'common/envs/constants' 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 ContractSearchFirestore from 'web/pages/contract-search-firestore' import { useMemberGroups } from 'web/hooks/use-group' import { NEW_USER_GROUP_SLUGS } from 'common/group' 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 { Col } from './layout/col' -import { safeLocalStorage } from 'web/lib/util/local' 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( 'GJQPAYENIF', '75c28fc084a80e1129d427d470cf41a3' @@ -47,7 +38,7 @@ const searchClient = algoliasearch( const indexPrefix = ENV === 'DEV' ? 'dev-' : '' const searchIndexName = ENV === 'DEV' ? 'dev-contracts' : 'contractsIndex' -const sortOptions = [ +const SORTS = [ { label: 'Newest', value: 'newest' }, { label: 'Trending', value: 'score' }, { label: 'Most traded', value: 'most-traded' }, @@ -56,16 +47,17 @@ const sortOptions = [ { label: 'Subsidy', value: 'liquidity' }, { label: 'Close date', value: 'close-date' }, { label: 'Resolve date', value: 'resolve-date' }, -] +] as const + +export type Sort = typeof SORTS[number]['value'] type filter = 'personal' | 'open' | 'closed' | 'resolved' | 'all' type SearchParameters = { - index: SearchIndex query: string - numericFilters: SearchOptions['numericFilters'] + sort: Sort + openClosedFilter: 'open' | 'closed' | undefined facetFilters: SearchOptions['facetFilters'] - showTime?: ShowTime } type AdditionalFilter = { @@ -88,8 +80,8 @@ export function ContractSearch(props: { hideQuickBet?: boolean } headerClassName?: string - useQuerySortLocalStorage?: boolean - useQuerySortUrlParams?: boolean + persistPrefix?: string + useQueryUrlParam?: boolean isWholePage?: boolean maxItems?: number noControls?: boolean @@ -104,66 +96,94 @@ export function ContractSearch(props: { cardHideOptions, highlightOptions, headerClassName, - useQuerySortLocalStorage, - useQuerySortUrlParams, + persistPrefix, + useQueryUrlParam, isWholePage, maxItems, noControls, } = props - const [numPages, setNumPages] = useState(1) - const [pages, setPages] = useState([]) - const [showTime, setShowTime] = useState() + const [state, setState] = usePersistentState( + { + numPages: 1, + pages: [] as Contract[][], + showTime: null as ShowTime | null, + }, + !persistPrefix + ? undefined + : { key: `${persistPrefix}-search`, store: historyStore() } + ) - const searchParameters = useRef() + const searchParams = useRef(null) + const searchParamsStore = historyStore() 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) => { - if (searchParameters.current === undefined) { + if (searchParams.current == null) { return } - const params = searchParameters.current + const { query, sort, openClosedFilter, facetFilters } = searchParams.current const id = ++requestId.current - const requestedPage = freshQuery ? 0 : pages.length - if (freshQuery || requestedPage < numPages) { - const results = await params.index.search(params.query, { - facetFilters: params.facetFilters, - numericFilters: params.numericFilters, + const requestedPage = freshQuery ? 0 : state.pages.length + if (freshQuery || requestedPage < state.numPages) { + const index = query + ? searchIndex + : 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, hitsPerPage: 20, }) // if there's a more recent request, forget about this one if (id === requestId.current) { const newPage = results.hits as any as Contract[] - // this spooky looking function is the easiest way to get react to - // batch this and not do multiple renders. we can throw it out in react 18. - // see https://github.com/reactwg/react-18/discussions/21 - unstable_batchedUpdates(() => { - setShowTime(params.showTime) - setNumPages(results.nbPages) - if (freshQuery) { - setPages([newPage]) - if (isWholePage) window.scrollTo(0, 0) - } else { - setPages((pages) => [...pages, newPage]) - } - }) + const showTime = + sort === 'close-date' || sort === 'resolve-date' ? sort : null + const pages = freshQuery ? [newPage] : [...state.pages, newPage] + setState({ numPages: results.nbPages, pages, showTime }) + if (freshQuery && isWholePage) window.scrollTo(0, 0) } } } const onSearchParametersChanged = useRef( debounce((params) => { - searchParameters.current = params - performQuery(true) + if (!isEqual(searchParams.current, params)) { + if (persistPrefix) { + searchParamsStore.set(`${persistPrefix}-params`, params) + } + searchParams.current = params + performQuery(true) + } }, 100) ).current - const contracts = pages + const contracts = state.pages .flat() .filter((c) => !additionalFilter?.excludeContractIds?.includes(c.id)) 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) { return @@ -177,8 +197,8 @@ export function ContractSearch(props: { defaultFilter={defaultFilter} additionalFilter={additionalFilter} hideOrderSelector={hideOrderSelector} - useQuerySortLocalStorage={useQuerySortLocalStorage} - useQuerySortUrlParams={useQuerySortUrlParams} + persistPrefix={persistPrefix ? `${persistPrefix}-controls` : undefined} + useQueryUrlParam={useQueryUrlParam} user={user} onSearchParametersChanged={onSearchParametersChanged} noControls={noControls} @@ -186,7 +206,7 @@ export function ContractSearch(props: { void - useQuerySortLocalStorage?: boolean - useQuerySortUrlParams?: boolean + persistPrefix?: string + useQueryUrlParam?: boolean user?: User | null noControls?: boolean }) { @@ -214,25 +234,36 @@ function ContractSearchControls(props: { additionalFilter, hideOrderSelector, onSearchParametersChanged, - useQuerySortLocalStorage, - useQuerySortUrlParams, + persistPrefix, + useQueryUrlParam, user, noControls, } = props - const savedSort = useQuerySortLocalStorage ? getSavedSort() : null - const initialSort = savedSort ?? defaultSort ?? 'score' - const querySortOpts = { useUrl: !!useQuerySortUrlParams } - const [sort, setSort] = useSort(initialSort, querySortOpts) - const [query, setQuery] = useQuery('', querySortOpts) - const [filter, setFilter] = useState(defaultFilter ?? 'open') - const [pillFilter, setPillFilter] = useState(undefined) + const router = useRouter() + const [query, setQuery] = usePersistentState( + '', + !useQueryUrlParam + ? undefined + : { + key: 'q', + store: urlParamStore(router), + } + ) - useEffect(() => { - if (useQuerySortLocalStorage) { - setSavedSort(sort) - } - }, [sort]) + const [state, setState] = usePersistentState( + { + sort: defaultSort ?? 'score', + filter: defaultFilter ?? 'open', + pillFilter: null as string | null, + }, + !persistPrefix + ? undefined + : { + key: `${persistPrefix}-params`, + store: storageStore(safeLocalStorage()), + } + ) const follows = useFollows(user?.id) const memberGroups = (useMemberGroups(user?.id) ?? []).filter( @@ -266,14 +297,16 @@ function ContractSearchControls(props: { ...additionalFilters, additionalFilter ? '' : 'visibility:public', - filter === 'open' ? 'isResolved:false' : '', - filter === 'closed' ? 'isResolved:false' : '', - filter === 'resolved' ? 'isResolved:true' : '', + state.filter === 'open' ? 'isResolved:false' : '', + state.filter === 'closed' ? 'isResolved:false' : '', + state.filter === 'resolved' ? 'isResolved:true' : '', - pillFilter && pillFilter !== 'personal' && pillFilter !== 'your-bets' - ? `groupLinks.slug:${pillFilter}` + state.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 memberGroupSlugs .map((slug) => `groupLinks.slug:${slug}`) @@ -285,22 +318,24 @@ function ContractSearchControls(props: { ) : '', // Subtract contracts you bet on from For you. - pillFilter === 'personal' && user ? `uniqueBettorIds:-${user.id}` : '', - pillFilter === 'your-bets' && user + state.pillFilter === 'personal' && user + ? `uniqueBettorIds:-${user.id}` + : '', + state.pillFilter === 'your-bets' && user ? // Show contracts bet on by the user `uniqueBettorIds:${user.id}` : '', ].filter((f) => f) - const numericFilters = query - ? [] - : [ - filter === 'open' ? `closeTime > ${Date.now()}` : '', - filter === 'closed' ? `closeTime <= ${Date.now()}` : '', - ].filter((f) => f) + const openClosedFilter = + state.filter === 'open' + ? 'open' + : state.filter === 'closed' + ? 'closed' + : undefined - const selectPill = (pill: string | undefined) => () => { - setPillFilter(pill) + const selectPill = (pill: string | null) => () => { + setState({ ...state, pillFilter: pill }) track('select search category', { category: pill ?? 'all' }) } @@ -309,34 +344,25 @@ function ContractSearchControls(props: { } const selectFilter = (newFilter: filter) => { - if (newFilter === filter) return - setFilter(newFilter) + if (newFilter === state.filter) return + setState({ ...state, filter: newFilter }) track('select search filter', { filter: newFilter }) } const selectSort = (newSort: Sort) => { - if (newSort === sort) return - setSort(newSort) + if (newSort === state.sort) return + setState({ ...state, 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(() => { onSearchParametersChanged({ - index: query ? searchIndex : index, query: query, - numericFilters: numericFilters, + sort: state.sort, + openClosedFilter: openClosedFilter, 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) { return <> @@ -351,14 +377,14 @@ function ContractSearchControls(props: { type="text" value={query} onChange={(e) => updateQuery(e.target.value)} - onBlur={trackCallback('search', { query })} + onBlur={trackCallback('search', { query: query })} placeholder={'Search'} className="input input-bordered w-full" /> {!query && ( selectSort(e.target.value as Sort)} > - {sortOptions.map((option) => ( + {SORTS.map((option) => ( @@ -386,14 +412,14 @@ function ContractSearchControls(props: { All {user ? 'For you' : 'Featured'} @@ -402,7 +428,7 @@ function ContractSearchControls(props: { {user && ( Your bets @@ -413,7 +439,7 @@ function ContractSearchControls(props: { return ( {name} diff --git a/web/hooks/use-persistent-state.ts b/web/hooks/use-persistent-state.ts new file mode 100644 index 00000000..090aa264 --- /dev/null +++ b/web/hooks/use-persistent-state.ts @@ -0,0 +1,106 @@ +import { useEffect } from 'react' +import { useStateCheckEquality } from './use-state-check-equality' +import { NextRouter } from 'next/router' + +export type PersistenceOptions = { key: string; store: PersistentStore } + +export interface PersistentStore { + 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 = (storage?: Storage): PersistentStore => ({ + 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 => ({ + 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 = (prefix = '__manifold'): PersistentStore => ({ + 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 = ( + initial: T, + persist?: PersistenceOptions +) => { + 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 +} diff --git a/web/hooks/use-preserve-scroll.ts b/web/hooks/use-preserve-scroll.ts deleted file mode 100644 index e314d11f..00000000 --- a/web/hooks/use-preserve-scroll.ts +++ /dev/null @@ -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]) -} diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx deleted file mode 100644 index 0a2834d0..00000000 --- a/web/hooks/use-sort-and-query-params.tsx +++ /dev/null @@ -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 - } -} diff --git a/web/lib/util/local.ts b/web/lib/util/local.ts index 0778c0ac..d533e345 100644 --- a/web/lib/util/local.ts +++ b/web/lib/util/local.ts @@ -1,4 +1,7 @@ -export const safeLocalStorage = () => (isLocalStorage() ? localStorage : null) +export const safeLocalStorage = () => + isLocalStorage() ? localStorage : undefined +export const safeSessionStorage = () => + isSessionStorage() ? sessionStorage : undefined const isLocalStorage = () => { try { @@ -9,3 +12,13 @@ const isLocalStorage = () => { return false } } + +const isSessionStorage = () => { + try { + sessionStorage.getItem('test') + sessionStorage.setItem('hi', 'mom') + return true + } catch (e) { + return false + } +} diff --git a/web/next.config.js b/web/next.config.js index 5a418016..6ade8674 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -8,6 +8,7 @@ module.exports = { reactStrictMode: true, optimizeFonts: false, experimental: { + scrollRestoration: true, externalDir: true, modularizeImports: { '@heroicons/react/solid/?(((\\w*)?/?)*)': { diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx index bb620950..d5a38272 100644 --- a/web/pages/_app.tsx +++ b/web/pages/_app.tsx @@ -3,7 +3,6 @@ import type { AppProps } from 'next/app' import { useEffect } from 'react' import Head from 'next/head' import Script from 'next/script' -import { usePreserveScroll } from 'web/hooks/use-preserve-scroll' import { QueryClient, QueryClientProvider } from 'react-query' import { AuthProvider } from 'web/components/auth-context' import Welcome from 'web/components/onboarding/welcome' @@ -26,8 +25,6 @@ function printBuildInfo() { } function MyApp({ Component, pageProps }: AppProps) { - usePreserveScroll() - useEffect(printBuildInfo, []) return ( diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index ec480269..4691030c 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -1,9 +1,13 @@ +import { useRouter } from 'next/router' import { Answer } from 'common/answer' import { searchInAny } from 'common/util/parse' import { sortBy } from 'lodash' import { ContractsGrid } from 'web/components/contract/contracts-grid' 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 @@ -15,10 +19,12 @@ export default function ContractSearchFirestore(props: { groupSlug?: string } }) { - const contracts = useContracts() const { additionalFilter } = props - const [query, setQuery] = useQuery('', { useUrl: true }) - const [sort, setSort] = useSort('score', { useUrl: true }) + const contracts = useContracts() + 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) => searchInAny( @@ -34,8 +40,6 @@ export default function ContractSearchFirestore(props: { matches.sort((a, b) => b.createdTime - a.createdTime) } else if (sort === 'resolve-date') { 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') { matches = sortBy(matches, ({ volume24Hours }) => -1 * volume24Hours) matches = sortBy(matches, (contract) => contract.closeTime ?? Infinity) @@ -93,7 +97,7 @@ export default function ContractSearchFirestore(props: {