🔍 Algolia search (#136)

* Add algolia and instantsearch packages

* Switch to hooks-web package

* Implement algolia search!

* Fix types

* Fix tags page

* Closed sort option

* Implement select for filtering on open, closed, resolved, all.

* Support search in dev environment

* Fix runtime error in landing page
This commit is contained in:
James Grugett 2022-05-09 12:38:33 -05:00 committed by GitHub
parent 3fc159f10b
commit e8ab863557
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 579 additions and 420 deletions

View File

@ -2,7 +2,7 @@ import { DEV_CONFIG } from './dev'
import { EnvConfig, PROD_CONFIG } from './prod'
import { THEOREMONE_CONFIG } from './theoremone'
const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
export const ENV = process.env.NEXT_PUBLIC_FIREBASE_ENV ?? 'PROD'
const CONFIGS = {
PROD: PROD_CONFIG,

View File

@ -9,9 +9,7 @@ const formatter = new Intl.NumberFormat('en-US', {
export function formatMoney(amount: number) {
const newAmount = Math.round(amount) === 0 ? 0 : Math.floor(amount) // handle -0 case
return (
ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '')
)
return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '')
}
export function formatWithCommas(amount: number) {

View File

@ -0,0 +1,221 @@
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'
import { ENV } from 'common/envs/constants'
const searchClient = algoliasearch(
'GJQPAYENIF',
'75c28fc084a80e1129d427d470cf41a3'
)
const indexPrefix = ENV === 'DEV' ? 'dev-' : ''
console.log('env', ENV, indexPrefix)
const sortIndexes = [
{ label: 'Newest', value: indexPrefix + 'contracts-newest' },
{ label: 'Oldest', value: indexPrefix + 'contracts-oldest' },
{ label: 'Most traded', value: indexPrefix + 'contracts-most-traded' },
{ label: '24h volume', value: indexPrefix + 'contracts-24-hour-vol' },
{ label: 'Close date', value: indexPrefix + 'contracts-close-date' },
{ label: 'Resolve date', value: indexPrefix + 'contracts-resolve-date' },
]
type filter = 'open' | 'closed' | 'resolved' | 'all'
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(`${indexPrefix}contracts-${initialSort ?? ''}`)
? initialSort
: querySortOptions?.defaultSort
const [filter, setFilter] = useState<filter>('open')
if (!sort) return <></>
return (
<InstantSearch
searchClient={searchClient}
indexName={`${indexPrefix}contracts-${sort}`}
>
<Row className="flex-wrap gap-2">
<SearchBox
className="flex-1"
classNames={{
form: 'before:top-6',
input: '!pl-10 !input !input-bordered shadow-none',
resetIcon: 'mt-2',
}}
placeholder="Search markets"
/>
<Row className="mt-2 gap-2 sm:mt-0">
<select
className="!select !select-bordered"
value={filter}
onChange={(e) => setFilter(e.target.value as filter)}
>
<option value="open">Open</option>
<option value="closed">Closed</option>
<option value="resolved">Resolved</option>
<option value="all">All</option>
</select>
<SortBy
items={sortIndexes}
classNames={{
select: '!select !select-bordered',
}}
/>
</Row>
</Row>
<ContractSearchInner
querySortOptions={querySortOptions}
filter={filter}
/>
</InstantSearch>
)
}
export function ContractSearchInner(props: {
querySortOptions?: {
defaultSort: Sort
filter?: {
creatorId?: string
tag?: string
}
shouldLoadFromStorage?: boolean
}
filter: filter
}) {
const { querySortOptions, filter } = 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)
useFilterClosed(
filter === 'closed'
? true
: filter === 'all' || filter === 'resolved'
? undefined
: false
)
useFilterResolved(
filter === 'resolved' ? true : filter === 'all' ? undefined : false
)
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())
}, [tag, refine])
}
const useFilterClosed = (value: boolean | undefined) => {
const [now] = useState(Date.now())
useRange({
attribute: 'closeTime',
min: value === false ? now : undefined,
max: value ? now : undefined,
})
}
const useFilterResolved = (value: boolean | undefined) => {
// Note (James): I don't know why this works.
const { refine: refineResolved } = useToggleRefinement({
attribute: value === undefined ? 'non-existant-field' : 'isResolved',
on: true,
off: value === undefined ? undefined : false,
})
useEffect(() => {
refineResolved({ isRefined: !value })
}, [value])
}

View File

@ -1,41 +1,19 @@
import _ from 'lodash'
import Link from 'next/link'
import clsx from 'clsx'
import { useEffect, useState } from 'react'
import {
contractMetrics,
Contract,
listContracts,
getBinaryProb,
} from 'web/lib/firebase/contracts'
import { User } from 'web/lib/firebase/users'
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 'web/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 [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, showCloseTime, hasMore, loadMore } = props
if (contracts.length === 0) {
return (
@ -49,306 +27,38 @@ export function ContractsGrid(props: {
}
return (
<>
<Col className="gap-8">
<ul className="grid w-full grid-cols-1 gap-6 md:grid-cols-2">
{contracts.map((contract) => (
<ContractCard
contract={contract}
key={contract.id}
// showHotVolume={showHotVolume}
showCloseTime={showCloseTime}
/>
))}
</ul>
{/* Show a link that increases the page num when clicked */}
{showMore && (
{hasMore && (
<button
className="btn btn-link float-right normal-case"
onClick={() => setPage(page + 1)}
className="btn btn-primary self-center normal-case"
onClick={loadMore}
>
Show more...
Show more
</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>
)
}
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)
export function CreatorContractsList(props: { creator: User }) {
const { creator } = props
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: { contracts: Contract[] }) {
const { contracts } = props
return (
<SearchableGrid
contracts={contracts}
byOneCreator
<ContractSearch
querySortOptions={{
defaultSort: 'all',
filter: {
creatorId: creator.id,
},
defaultSort: 'newest',
shouldLoadFromStorage: false,
}}
/>

View File

@ -1,3 +1,4 @@
import _ from 'lodash'
import { SparklesIcon, XIcon } from '@heroicons/react/solid'
import { Avatar } from './avatar'
import { useEffect, useRef, useState } from 'react'
@ -9,8 +10,7 @@ import { Contract, MAX_QUESTION_LENGTH } from 'common/contract'
import { Col } from './layout/col'
import clsx from 'clsx'
import { Row } from './layout/row'
import { ENV_CONFIG } from 'common/envs/constants'
import _ from 'lodash'
import { ENV_CONFIG } from '../../common/envs/constants'
import { SiteLink } from './site-link'
export function FeedPromo(props: { hotContracts: Contract[] }) {
@ -50,7 +50,8 @@ export function FeedPromo(props: { hotContracts: Contract[] }) {
</Row>
<ContractsGrid
contracts={hotContracts?.slice(0, 10) || []}
showHotVolume
loadMore={() => {}}
hasMore={false}
/>
</>
)

View File

@ -201,7 +201,7 @@ export function UserPage(props: {
tabs={[
{
title: 'Markets',
content: <CreatorContractsList contracts={usersContracts} />,
content: <CreatorContractsList creator={user} />,
tabIcon: (
<div className="px-0.5 font-bold">
{usersContracts.length}

View File

@ -1,22 +1,20 @@
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'
export type Sort =
| 'creator'
| 'tag'
| 'newest'
| '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 +24,58 @@ export function useQueryAndSortParams(options?: {
})
const router = useRouter()
const [initialSort, setInitialSort] = useState<Sort | undefined>(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) => {
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 +92,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,
}
}

View File

@ -21,6 +21,7 @@
"@heroicons/react": "1.0.5",
"@nivo/core": "0.74.0",
"@nivo/line": "0.74.0",
"algoliasearch": "4.13.0",
"clsx": "1.1.1",
"cors": "^2.8.5",
"daisyui": "1.16.4",
@ -31,9 +32,10 @@
"lodash": "4.17.21",
"next": "12.1.2",
"react": "17.0.2",
"react-confetti": "^6.0.1",
"react-confetti": "6.0.1",
"react-dom": "17.0.2",
"react-expanding-textarea": "2.3.5"
"react-expanding-textarea": "2.3.5",
"react-instantsearch-hooks-web": "6.24.1"
},
"devDependencies": {
"@tailwindcss/forms": "0.4.0",

View File

@ -18,6 +18,13 @@ export default function Document() {
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
async
src="https://www.googletagmanager.com/gtag/js?id=G-SSFK1Q138D"

View File

@ -10,17 +10,15 @@ import {
foldPath,
getFoldBySlug,
getFoldContracts,
} from 'web/lib/firebase/folds'
import { ActivityFeed } from 'web/components/feed/activity-feed'
import { TagsList } from 'web/components/tags-list'
import { Row } from 'web/components/layout/row'
import { UserLink } from 'web/components/user-page'
import { getUser, User } from 'web/lib/firebase/users'
import { Spacer } from 'web/components/layout/spacer'
import { Col } from 'web/components/layout/col'
import { useUser } from 'web/hooks/use-user'
import { useFold } from 'web/hooks/use-fold'
import { SearchableGrid } from 'web/components/contract/contracts-list'
} from '../../../lib/firebase/folds'
import { TagsList } from '../../../components/tags-list'
import { Row } from '../../../components/layout/row'
import { UserLink } from '../../../components/user-page'
import { getUser, User } from '../../../lib/firebase/users'
import { Spacer } from '../../../components/layout/spacer'
import { Col } from '../../../components/layout/col'
import { useUser } from '../../../hooks/use-user'
import { useFold } from '../../../hooks/use-fold'
import { useRouter } from 'next/router'
import { scoreCreators, scoreTraders } from 'common/scoring'
import { Leaderboard } from 'web/components/leaderboard'
@ -209,7 +207,7 @@ export default function FoldPage(props: {
tabs={[
{
title: 'Markets',
content: <SearchableGrid contracts={contracts} />,
content: <div>This view is deprecated.</div>,
href: foldPath(fold, 'markets'),
},
{

View File

@ -20,7 +20,7 @@ export default function LandingPage(props: { hotContracts: Contract[] }) {
<div>
<Hero />
<FeaturesSection />
<ExploreMarketsSection hotContracts={hotContracts} />
{/* <ExploreMarketsSection hotContracts={hotContracts} /> */}
</div>
)
}
@ -158,7 +158,11 @@ function ExploreMarketsSection(props: { hotContracts: Contract[] }) {
Today's top markets
</p>
<ContractsGrid contracts={hotContracts} />
<ContractsGrid
contracts={hotContracts}
loadMore={() => {}}
hasMore={false}
/>
</div>
)
}

View File

@ -1,17 +1,9 @@
import {
ContractsGrid,
SearchableGrid,
} from 'web/components/contract/contracts-list'
import { Page } from 'web/components/page'
import { SEO } from 'web/components/SEO'
import { Title } from 'web/components/title'
import { useContracts } from 'web/hooks/use-contracts'
import { Contract } from 'web/lib/firebase/contracts'
import { ContractSearch } from '../components/contract-search'
import { Page } from '../components/page'
import { SEO } from '../components/SEO'
// TODO: Rename endpoint to "Explore"
export default function Markets() {
const contracts = useContracts()
return (
<Page>
<SEO
@ -19,31 +11,7 @@ export default function Markets() {
description="Discover what's new, trending, or soon-to-close. Or search among our hundreds of markets."
url="/markets"
/>
<SearchableGrid contracts={contracts} />
<ContractSearch />
</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>
)
}

View File

@ -1,30 +1,23 @@
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { SearchableGrid } from 'web/components/contract/contracts-list'
import { Page } from 'web/components/page'
import { Title } from 'web/components/title'
import {
Contract,
listTaggedContractsCaseInsensitive,
} from 'web/lib/firebase/contracts'
import { ContractSearch } from '../../components/contract-search'
import { Page } from '../../components/page'
import { Title } from '../../components/title'
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<Contract[] | undefined>()
useEffect(() => {
if (tag != null) {
listTaggedContractsCaseInsensitive(tag).then(setContracts)
}
}, [tag])
// TODO: Fix error: The provided `href` (/tag/[tag]?s=newest) value is missing query values (tag)
return (
<Page>
<Title text={`#${tag}`} />
<SearchableGrid contracts={contracts} />
<ContractSearch
querySortOptions={{
filter: { tag },
defaultSort: 'newest',
shouldLoadFromStorage: false,
}}
/>
</Page>
)
}

242
yarn.lock
View File

@ -2,6 +2,115 @@
# yarn lockfile v1
"@algolia/cache-browser-local-storage@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.13.0.tgz#f8aa4fe31104b19d616ea392f9ed5c2ea847d964"
integrity sha512-nj1vHRZauTqP/bluwkRIgEADEimqojJgoTRCel5f6q8WCa9Y8QeI4bpDQP28FoeKnDRYa3J5CauDlN466jqRhg==
dependencies:
"@algolia/cache-common" "4.13.0"
"@algolia/cache-common@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.13.0.tgz#27b83fd3939d08d72261b36a07eeafc4cb4d2113"
integrity sha512-f9mdZjskCui/dA/fA/5a+6hZ7xnHaaZI5tM/Rw9X8rRB39SUlF/+o3P47onZ33n/AwkpSbi5QOyhs16wHd55kA==
"@algolia/cache-in-memory@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.13.0.tgz#10801a74550cbabb64b59ff08c56bce9c278ff2d"
integrity sha512-hHdc+ahPiMM92CQMljmObE75laYzNFYLrNOu0Q3/eyvubZZRtY2SUsEEgyUEyzXruNdzrkcDxFYa7YpWBJYHAg==
dependencies:
"@algolia/cache-common" "4.13.0"
"@algolia/client-account@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.13.0.tgz#f8646dd40d1e9e3353e10abbd5d6c293ea92a8e2"
integrity sha512-FzFqFt9b0g/LKszBDoEsW+dVBuUe1K3scp2Yf7q6pgHWM1WqyqUlARwVpLxqyc+LoyJkTxQftOKjyFUqddnPKA==
dependencies:
"@algolia/client-common" "4.13.0"
"@algolia/client-search" "4.13.0"
"@algolia/transporter" "4.13.0"
"@algolia/client-analytics@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.13.0.tgz#a00bd02df45d71becb9dd4c5c993d805f2e1786d"
integrity sha512-klmnoq2FIiiMHImkzOm+cGxqRLLu9CMHqFhbgSy9wtXZrqb8BBUIUE2VyBe7azzv1wKcxZV2RUyNOMpFqmnRZA==
dependencies:
"@algolia/client-common" "4.13.0"
"@algolia/client-search" "4.13.0"
"@algolia/requester-common" "4.13.0"
"@algolia/transporter" "4.13.0"
"@algolia/client-common@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.13.0.tgz#8bc373d164dbdcce38b4586912bbe162492bcb86"
integrity sha512-GoXfTp0kVcbgfSXOjfrxx+slSipMqGO9WnNWgeMmru5Ra09MDjrcdunsiiuzF0wua6INbIpBQFTC2Mi5lUNqGA==
dependencies:
"@algolia/requester-common" "4.13.0"
"@algolia/transporter" "4.13.0"
"@algolia/client-personalization@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.13.0.tgz#10fb7af356422551f11a67222b39c52306f1512c"
integrity sha512-KneLz2WaehJmNfdr5yt2HQETpLaCYagRdWwIwkTqRVFCv4DxRQ2ChPVW9jeTj4YfAAhfzE6F8hn7wkQ/Jfj6ZA==
dependencies:
"@algolia/client-common" "4.13.0"
"@algolia/requester-common" "4.13.0"
"@algolia/transporter" "4.13.0"
"@algolia/client-search@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.13.0.tgz#2d8ff8e755c4a37ec89968f3f9b358eed005c7f0"
integrity sha512-blgCKYbZh1NgJWzeGf+caKE32mo3j54NprOf0LZVCubQb3Kx37tk1Hc8SDs9bCAE8hUvf3cazMPIg7wscSxspA==
dependencies:
"@algolia/client-common" "4.13.0"
"@algolia/requester-common" "4.13.0"
"@algolia/transporter" "4.13.0"
"@algolia/events@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950"
integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==
"@algolia/logger-common@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.13.0.tgz#be2606e71aae618a1ff1ea9a1b5f5a74284b35a8"
integrity sha512-8yqXk7rMtmQJ9wZiHOt/6d4/JDEg5VCk83gJ39I+X/pwUPzIsbKy9QiK4uJ3aJELKyoIiDT1hpYVt+5ia+94IA==
"@algolia/logger-console@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.13.0.tgz#f28028a760e3d9191e28a10b12925e48f6c9afde"
integrity sha512-YepRg7w2/87L0vSXRfMND6VJ5d6699sFJBRWzZPOlek2p5fLxxK7O0VncYuc/IbVHEgeApvgXx0WgCEa38GVuQ==
dependencies:
"@algolia/logger-common" "4.13.0"
"@algolia/requester-browser-xhr@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.13.0.tgz#e2483f4e8d7f09e27cd0daf6c77711d15c5a919f"
integrity sha512-Dj+bnoWR5MotrnjblzGKZ2kCdQi2cK/VzPURPnE616NU/il7Ypy6U6DLGZ/ZYz+tnwPa0yypNf21uqt84fOgrg==
dependencies:
"@algolia/requester-common" "4.13.0"
"@algolia/requester-common@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.13.0.tgz#47fb3464cfb26b55ba43676d13f295d812830596"
integrity sha512-BRTDj53ecK+gn7ugukDWOOcBRul59C4NblCHqj4Zm5msd5UnHFjd/sGX+RLOEoFMhetILAnmg6wMrRrQVac9vw==
"@algolia/requester-node-http@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.13.0.tgz#7d981bbd31492f51dd11820a665f9d8906793c37"
integrity sha512-9b+3O4QFU4azLhGMrZAr/uZPydvzOR4aEZfSL8ZrpLZ7fbbqTO0S/5EVko+QIgglRAtVwxvf8UJ1wzTD2jvKxQ==
dependencies:
"@algolia/requester-common" "4.13.0"
"@algolia/transporter@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.13.0.tgz#f6379e5329efa2127da68c914d1141f5f21dbd07"
integrity sha512-8tSQYE+ykQENAdeZdofvtkOr5uJ9VcQSWgRhQ9h01AehtBIPAczk/b2CLrMsw5yQZziLs5cZ3pJ3478yI+urhA==
dependencies:
"@algolia/cache-common" "4.13.0"
"@algolia/logger-common" "4.13.0"
"@algolia/requester-common" "4.13.0"
"@babel/code-frame@7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
@ -38,6 +147,13 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.1.2":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.16.3":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa"
@ -988,6 +1104,16 @@
"@types/express-serve-static-core" "*"
"@types/serve-static" "*"
"@types/google.maps@^3.45.3":
version "3.48.7"
resolved "https://registry.yarnpkg.com/@types/google.maps/-/google.maps-3.48.7.tgz#2291e7fd41af797d4827c0f19e5609b1c07bf42e"
integrity sha512-mssJhNv6jwk+zyh0MpYWWTsL99l84nEz7m+l0p4MY2f2iqdkOYl2kDFdB7g7ME0ih41V0kaZXsjntkq5ul/UyQ==
"@types/hogan.js@^3.0.0":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/hogan.js/-/hogan.js-3.0.1.tgz#64c54407b30da359763e14877f5702b8ae85d61c"
integrity sha512-D03i/2OY7kGyMq9wdQ7oD8roE49z/ZCZThe/nbahtvuqCNZY9T2MfedOWyeBdbEpY2W8Gnh/dyJLdFtUCOkYbg==
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
@ -1041,7 +1167,7 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
"@types/qs@*":
"@types/qs@*", "@types/qs@^6.5.3":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
@ -1117,6 +1243,11 @@
"@typescript-eslint/types" "4.33.0"
eslint-visitor-keys "^2.0.0"
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@ -1205,6 +1336,33 @@ ajv@^8.0.1:
require-from-string "^2.0.2"
uri-js "^4.2.2"
algoliasearch-helper@^3.7.4, algoliasearch-helper@^3.8.2:
version "3.8.2"
resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.8.2.tgz#35726dc6d211f49dbab0bf6d37b4658165539523"
integrity sha512-AXxiF0zT9oYwl8ZBgU/eRXvfYhz7cBA5YrLPlw9inZHdaYF0QEya/f1Zp1mPYMXc1v6VkHwBq4pk6/vayBLICg==
dependencies:
"@algolia/events" "^4.0.1"
algoliasearch@4.13.0:
version "4.13.0"
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.13.0.tgz#e36611fda82b1fc548c156ae7929a7f486e4b663"
integrity sha512-oHv4faI1Vl2s+YC0YquwkK/TsaJs79g2JFg5FDm2rKN12VItPTAeQ7hyJMHarOPPYuCnNC5kixbtcqvb21wchw==
dependencies:
"@algolia/cache-browser-local-storage" "4.13.0"
"@algolia/cache-common" "4.13.0"
"@algolia/cache-in-memory" "4.13.0"
"@algolia/client-account" "4.13.0"
"@algolia/client-analytics" "4.13.0"
"@algolia/client-common" "4.13.0"
"@algolia/client-personalization" "4.13.0"
"@algolia/client-search" "4.13.0"
"@algolia/logger-common" "4.13.0"
"@algolia/logger-console" "4.13.0"
"@algolia/requester-browser-xhr" "4.13.0"
"@algolia/requester-common" "4.13.0"
"@algolia/requester-node-http" "4.13.0"
"@algolia/transporter" "4.13.0"
ansi-colors@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
@ -1533,6 +1691,11 @@ chokidar@^3.5.2:
optionalDependencies:
fsevents "~2.3.2"
classnames@^2.2.5:
version "2.3.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
clean-stack@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
@ -1962,6 +2125,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
dequal@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
@ -2985,6 +3153,14 @@ hash-stream-validation@^0.2.2:
resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512"
integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==
hogan.js@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/hogan.js/-/hogan.js-3.0.2.tgz#4cd9e1abd4294146e7679e41d7898732b02c7bfd"
integrity sha1-TNnhq9QpQUbnZ55B14mHMrAse/0=
dependencies:
mkdirp "0.3.0"
nopt "1.0.10"
http-errors@1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.8.1.tgz#7c3f28577cbc8a207388455dbd62295ed07bd68c"
@ -3117,6 +3293,22 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
instantsearch.js@^4.40.1:
version "4.40.5"
resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.40.5.tgz#17b3c2ed493bee80101b3eb9a48187b487d275c3"
integrity sha512-6YL4WGJ4mGz+b1mbAd/k+g3foz0W0HhZA7wSz0CeuNoJHnCcCcJAoAFdwLoWGRBKjxV7FoXFou0NRkLWXEmBrg==
dependencies:
"@algolia/events" "^4.0.1"
"@types/google.maps" "^3.45.3"
"@types/hogan.js" "^3.0.0"
"@types/qs" "^6.5.3"
algoliasearch-helper "^3.8.2"
classnames "^2.2.5"
hogan.js "^3.0.2"
preact "^10.6.0"
qs "^6.5.1 < 6.10"
search-insights "^2.1.0"
internal-slot@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
@ -3752,6 +3944,11 @@ minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=
mri@^1.1.5:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
@ -3869,6 +4066,13 @@ node-releases@^2.0.1:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5"
integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==
nopt@1.0.10:
version "1.0.10"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=
dependencies:
abbrev "1"
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
@ -4249,6 +4453,11 @@ preact@^10.5.12:
resolved "https://registry.yarnpkg.com/preact/-/preact-10.6.5.tgz#726d8bd12903a0d51cdd17e2e1b90cc539403e0c"
integrity sha512-i+LXM6JiVjQXSt2jG2vZZFapGpCuk1fl8o6ii3G84MA3xgj686FKjs4JFDkmUVhtxyq21+4ay74zqPykz9hU6w==
preact@^10.6.0:
version "10.7.1"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.7.1.tgz#bdd2b2dce91a5842c3b9b34dfe050e5401068c9e"
integrity sha512-MufnRFz39aIhs9AMFisonjzTud1PK1bY+jcJLo6m2T9Uh8AqjD77w11eAAawmjUogoGOnipECq7e/1RClIKsxg==
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@ -4407,6 +4616,11 @@ qs@6.9.6:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee"
integrity sha512-TIRk4aqYLNoJUbd+g2lEdz5kLWIuTMRagAXxl78Q0RiVjAOugHmeKNGdd3cwo/ktpf9aL9epCfFqWDEKysUlLQ==
"qs@^6.5.1 < 6.10":
version "6.9.7"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe"
integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==
qs@^6.6.0:
version "6.10.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
@ -4446,7 +4660,7 @@ raw-body@2.4.2, raw-body@^2.2.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
react-confetti@^6.0.1:
react-confetti@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.0.1.tgz#d4f57b5a021dd908a6243b8f63b6009b00818d10"
integrity sha512-ZpOTBrqSNhWE4rRXCZ6E6U+wGd7iYHF5MGrqwikoiBpgBq9Akdu0DcLW+FdFnLjyZYC+VfAiV2KeFgYRMyMrkA==
@ -4471,6 +4685,25 @@ react-expanding-textarea@2.3.5:
react-with-forwarded-ref "^0.3.3"
tslib "^2.0.3"
react-instantsearch-hooks-web@6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/react-instantsearch-hooks-web/-/react-instantsearch-hooks-web-6.24.1.tgz#392be70c584583f3cd9fe22eda5a59f7449e5ac9"
integrity sha512-UX7zaxDE834LxxUNHB5lBg8ujO0KFyOjg8iRZs+WVEBsYdQskGEAHiLlyaSjcfDZS0tPBsZA/ybYjwpGmOMqKg==
dependencies:
"@babel/runtime" "^7.1.2"
instantsearch.js "^4.40.1"
react-instantsearch-hooks "6.24.1"
react-instantsearch-hooks@6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/react-instantsearch-hooks/-/react-instantsearch-hooks-6.24.1.tgz#59869c4492a5dffdb9ad979e141ec754d87fa668"
integrity sha512-X7Wg4thRsybYNYpKFKr2YpZeN8eDH/sIhxxUz4uHhO2U0DD7tnZKa7fJVhCMHZf8ci3lTx9Lz6MBbCK1mr+pvQ==
dependencies:
"@babel/runtime" "^7.1.2"
algoliasearch-helper "^3.7.4"
dequal "^2.0.0"
instantsearch.js "^4.40.1"
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -4681,6 +4914,11 @@ scheduler@^0.20.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
search-insights@^2.1.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.2.1.tgz#9c93344fbae5fbf2f88c1a81b46b4b5d888c11f7"
integrity sha512-JDfVGZbKqTtiKVZjAVbkNw9C9f0ib80yx6Ea17M3z4RvPmuD0GYWXuFwA9++dpbreBEMH4TC3lQ29Zq7O4b5oA==
selenium-webdriver@4.0.0-rc-1:
version "4.0.0-rc-1"
resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-rc-1.tgz#b1e7e5821298c8a071e988518dd6b759f0c41281"