Home: Prob change cards. Sort by daily score. (#925)
* Add dailyScore: product of unique bettors (3 days) and probChanges.day * Increase memory and duration of scoreContracts * Home: Smaller prob change card for groups. Use dailyScore for sort order (algolia) * Add back hover
This commit is contained in:
parent
eaaa46294a
commit
c6d034545a
|
@ -57,6 +57,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
|
||||||
uniqueBettorIds?: string[]
|
uniqueBettorIds?: string[]
|
||||||
uniqueBettorCount?: number
|
uniqueBettorCount?: number
|
||||||
popularityScore?: number
|
popularityScore?: number
|
||||||
|
dailyScore?: number
|
||||||
followerCount?: number
|
followerCount?: number
|
||||||
featuredOnHomeRank?: number
|
featuredOnHomeRank?: number
|
||||||
likedByUserIds?: string[]
|
likedByUserIds?: string[]
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import * as functions from 'firebase-functions'
|
import * as functions from 'firebase-functions'
|
||||||
import * as admin from 'firebase-admin'
|
import * as admin from 'firebase-admin'
|
||||||
import { Bet } from 'common/bet'
|
|
||||||
import { uniq } from 'lodash'
|
import { uniq } from 'lodash'
|
||||||
import { Contract } from 'common/contract'
|
import { Bet } from '../../common/bet'
|
||||||
|
import { Contract } from '../../common/contract'
|
||||||
import { log } from './utils'
|
import { log } from './utils'
|
||||||
|
import { removeUndefinedProps } from '../../common/util/object'
|
||||||
|
|
||||||
export const scoreContracts = functions.pubsub
|
export const scoreContracts = functions
|
||||||
.schedule('every 1 hours')
|
.runWith({ memory: '4GB', timeoutSeconds: 540 })
|
||||||
|
.pubsub.schedule('every 1 hours')
|
||||||
.onRun(async () => {
|
.onRun(async () => {
|
||||||
await scoreContractsInternal()
|
await scoreContractsInternal()
|
||||||
})
|
})
|
||||||
|
@ -44,11 +46,22 @@ async function scoreContractsInternal() {
|
||||||
const bettors = bets.docs
|
const bettors = bets.docs
|
||||||
.map((doc) => doc.data() as Bet)
|
.map((doc) => doc.data() as Bet)
|
||||||
.map((bet) => bet.userId)
|
.map((bet) => bet.userId)
|
||||||
const score = uniq(bettors).length
|
const popularityScore = uniq(bettors).length
|
||||||
if (contract.popularityScore !== score)
|
|
||||||
|
let dailyScore: number | undefined
|
||||||
|
if (contract.outcomeType === 'BINARY' && contract.mechanism === 'cpmm-1') {
|
||||||
|
const percentChange = Math.abs(contract.probChanges.day)
|
||||||
|
dailyScore = popularityScore * percentChange
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
contract.popularityScore !== popularityScore ||
|
||||||
|
contract.dailyScore !== dailyScore
|
||||||
|
) {
|
||||||
await firestore
|
await firestore
|
||||||
.collection('contracts')
|
.collection('contracts')
|
||||||
.doc(contract.id)
|
.doc(contract.id)
|
||||||
.update({ popularityScore: score })
|
.update(removeUndefinedProps({ popularityScore, dailyScore }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { Col } from '../layout/col'
|
||||||
import {
|
import {
|
||||||
BinaryContract,
|
BinaryContract,
|
||||||
Contract,
|
Contract,
|
||||||
|
CPMMBinaryContract,
|
||||||
FreeResponseContract,
|
FreeResponseContract,
|
||||||
MultipleChoiceContract,
|
MultipleChoiceContract,
|
||||||
NumericContract,
|
NumericContract,
|
||||||
|
@ -32,6 +33,8 @@ import { track } from '@amplitude/analytics-browser'
|
||||||
import { trackCallback } from 'web/lib/service/analytics'
|
import { trackCallback } from 'web/lib/service/analytics'
|
||||||
import { getMappedValue } from 'common/pseudo-numeric'
|
import { getMappedValue } from 'common/pseudo-numeric'
|
||||||
import { Tooltip } from '../tooltip'
|
import { Tooltip } from '../tooltip'
|
||||||
|
import { SiteLink } from '../site-link'
|
||||||
|
import { ProbChange } from './prob-change-table'
|
||||||
|
|
||||||
export function ContractCard(props: {
|
export function ContractCard(props: {
|
||||||
contract: Contract
|
contract: Contract
|
||||||
|
@ -379,3 +382,34 @@ export function PseudoNumericResolutionOrExpectation(props: {
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ContractCardProbChange(props: {
|
||||||
|
contract: CPMMBinaryContract
|
||||||
|
noLinkAvatar?: boolean
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, noLinkAvatar, className } = props
|
||||||
|
return (
|
||||||
|
<Col
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'mb-4 rounded-lg bg-white shadow hover:bg-gray-100 hover:shadow-lg'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<AvatarDetails
|
||||||
|
contract={contract}
|
||||||
|
className={'px-6 pt-4'}
|
||||||
|
noLink={noLinkAvatar}
|
||||||
|
/>
|
||||||
|
<Row className={clsx('items-start justify-between gap-4 ', className)}>
|
||||||
|
<SiteLink
|
||||||
|
className="pl-6 pr-0 pt-2 pb-4 font-semibold text-indigo-700"
|
||||||
|
href={contractPath(contract)}
|
||||||
|
>
|
||||||
|
<span className="line-clamp-3">{contract.question}</span>
|
||||||
|
</SiteLink>
|
||||||
|
<ProbChange className="py-2 pr-4" contract={contract} />
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Contract } from 'web/lib/firebase/contracts'
|
||||||
import { User } from 'web/lib/firebase/users'
|
import { User } from 'web/lib/firebase/users'
|
||||||
import { Col } from '../layout/col'
|
import { Col } from '../layout/col'
|
||||||
import { SiteLink } from '../site-link'
|
import { SiteLink } from '../site-link'
|
||||||
import { ContractCard } from './contract-card'
|
import { ContractCard, ContractCardProbChange } from './contract-card'
|
||||||
import { ShowTime } from './contract-details'
|
import { ShowTime } from './contract-details'
|
||||||
import { ContractSearch } from '../contract-search'
|
import { ContractSearch } from '../contract-search'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
@ -10,6 +10,7 @@ import clsx from 'clsx'
|
||||||
import { LoadingIndicator } from '../loading-indicator'
|
import { LoadingIndicator } from '../loading-indicator'
|
||||||
import { VisibilityObserver } from '../visibility-observer'
|
import { VisibilityObserver } from '../visibility-observer'
|
||||||
import Masonry from 'react-masonry-css'
|
import Masonry from 'react-masonry-css'
|
||||||
|
import { CPMMBinaryContract} from 'common/contract'
|
||||||
|
|
||||||
export type ContractHighlightOptions = {
|
export type ContractHighlightOptions = {
|
||||||
contractIds?: string[]
|
contractIds?: string[]
|
||||||
|
@ -25,6 +26,7 @@ export function ContractsGrid(props: {
|
||||||
hideQuickBet?: boolean
|
hideQuickBet?: boolean
|
||||||
hideGroupLink?: boolean
|
hideGroupLink?: boolean
|
||||||
noLinkAvatar?: boolean
|
noLinkAvatar?: boolean
|
||||||
|
showProbChange?: boolean
|
||||||
}
|
}
|
||||||
highlightOptions?: ContractHighlightOptions
|
highlightOptions?: ContractHighlightOptions
|
||||||
trackingPostfix?: string
|
trackingPostfix?: string
|
||||||
|
@ -39,7 +41,8 @@ export function ContractsGrid(props: {
|
||||||
highlightOptions,
|
highlightOptions,
|
||||||
trackingPostfix,
|
trackingPostfix,
|
||||||
} = props
|
} = props
|
||||||
const { hideQuickBet, hideGroupLink, noLinkAvatar } = cardUIOptions || {}
|
const { hideQuickBet, hideGroupLink, noLinkAvatar, showProbChange } =
|
||||||
|
cardUIOptions || {}
|
||||||
const { contractIds, highlightClassName } = highlightOptions || {}
|
const { contractIds, highlightClassName } = highlightOptions || {}
|
||||||
const onVisibilityUpdated = useCallback(
|
const onVisibilityUpdated = useCallback(
|
||||||
(visible) => {
|
(visible) => {
|
||||||
|
@ -73,24 +76,31 @@ export function ContractsGrid(props: {
|
||||||
className="-ml-4 flex w-auto"
|
className="-ml-4 flex w-auto"
|
||||||
columnClassName="pl-4 bg-clip-padding"
|
columnClassName="pl-4 bg-clip-padding"
|
||||||
>
|
>
|
||||||
{contracts.map((contract) => (
|
{contracts.map((contract) =>
|
||||||
<ContractCard
|
showProbChange && contract.mechanism === 'cpmm-1' ? (
|
||||||
contract={contract}
|
<ContractCardProbChange
|
||||||
key={contract.id}
|
key={contract.id}
|
||||||
showTime={showTime}
|
contract={contract as CPMMBinaryContract}
|
||||||
onClick={
|
/>
|
||||||
onContractClick ? () => onContractClick(contract) : undefined
|
) : (
|
||||||
}
|
<ContractCard
|
||||||
noLinkAvatar={noLinkAvatar}
|
contract={contract}
|
||||||
hideQuickBet={hideQuickBet}
|
key={contract.id}
|
||||||
hideGroupLink={hideGroupLink}
|
showTime={showTime}
|
||||||
trackingPostfix={trackingPostfix}
|
onClick={
|
||||||
className={clsx(
|
onContractClick ? () => onContractClick(contract) : undefined
|
||||||
'mb-4 break-inside-avoid-column overflow-hidden', // prevent content from wrapping (needs overflow on firefox)
|
}
|
||||||
contractIds?.includes(contract.id) && highlightClassName
|
noLinkAvatar={noLinkAvatar}
|
||||||
)}
|
hideQuickBet={hideQuickBet}
|
||||||
/>
|
hideGroupLink={hideGroupLink}
|
||||||
))}
|
trackingPostfix={trackingPostfix}
|
||||||
|
className={clsx(
|
||||||
|
'mb-4 break-inside-avoid-column overflow-hidden', // prevent content from wrapping (needs overflow on firefox)
|
||||||
|
contractIds?.includes(contract.id) && highlightClassName
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</Masonry>
|
</Masonry>
|
||||||
{loadMore && (
|
{loadMore && (
|
||||||
<VisibilityObserver
|
<VisibilityObserver
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
|
import { partition } from 'lodash'
|
||||||
import { contractPath } from 'web/lib/firebase/contracts'
|
import { contractPath } from 'web/lib/firebase/contracts'
|
||||||
import { CPMMContract } from 'common/contract'
|
import { CPMMContract } from 'common/contract'
|
||||||
import { formatPercent } from 'common/util/format'
|
import { formatPercent } from 'common/util/format'
|
||||||
|
@ -8,16 +9,17 @@ import { Row } from '../layout/row'
|
||||||
import { LoadingIndicator } from '../loading-indicator'
|
import { LoadingIndicator } from '../loading-indicator'
|
||||||
|
|
||||||
export function ProbChangeTable(props: {
|
export function ProbChangeTable(props: {
|
||||||
changes:
|
changes: CPMMContract[] | undefined
|
||||||
| { positiveChanges: CPMMContract[]; negativeChanges: CPMMContract[] }
|
|
||||||
| undefined
|
|
||||||
full?: boolean
|
full?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { changes, full } = props
|
const { changes, full } = props
|
||||||
|
|
||||||
if (!changes) return <LoadingIndicator />
|
if (!changes) return <LoadingIndicator />
|
||||||
|
|
||||||
const { positiveChanges, negativeChanges } = changes
|
const [positiveChanges, negativeChanges] = partition(
|
||||||
|
changes,
|
||||||
|
(c) => c.probChanges.day > 0
|
||||||
|
)
|
||||||
|
|
||||||
const threshold = 0.01
|
const threshold = 0.01
|
||||||
const positiveAboveThreshold = positiveChanges.filter(
|
const positiveAboveThreshold = positiveChanges.filter(
|
||||||
|
@ -53,10 +55,18 @@ export function ProbChangeTable(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProbChangeRow(props: { contract: CPMMContract }) {
|
export function ProbChangeRow(props: {
|
||||||
const { contract } = props
|
contract: CPMMContract
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { contract, className } = props
|
||||||
return (
|
return (
|
||||||
<Row className="items-center justify-between gap-4 hover:bg-gray-100">
|
<Row
|
||||||
|
className={clsx(
|
||||||
|
'items-center justify-between gap-4 hover:bg-gray-100',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
<SiteLink
|
<SiteLink
|
||||||
className="p-4 pr-0 font-semibold text-indigo-700"
|
className="p-4 pr-0 font-semibold text-indigo-700"
|
||||||
href={contractPath(contract)}
|
href={contractPath(contract)}
|
||||||
|
|
|
@ -11,10 +11,13 @@ import {
|
||||||
trendingContractsQuery,
|
trendingContractsQuery,
|
||||||
getContractsQuery,
|
getContractsQuery,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/firebase/contracts'
|
||||||
import { QueryClient, useQueryClient } from 'react-query'
|
import { QueryClient, useQuery, useQueryClient } from 'react-query'
|
||||||
import { MINUTE_MS } from 'common/util/time'
|
import { MINUTE_MS } from 'common/util/time'
|
||||||
import { query, limit } from 'firebase/firestore'
|
import { query, limit } from 'firebase/firestore'
|
||||||
import { Sort } from 'web/components/contract-search'
|
import { Sort } from 'web/components/contract-search'
|
||||||
|
import { dailyScoreIndex } from 'web/lib/service/algolia'
|
||||||
|
import { CPMMBinaryContract } from 'common/contract'
|
||||||
|
import { zipObject } from 'lodash'
|
||||||
|
|
||||||
export const useContracts = () => {
|
export const useContracts = () => {
|
||||||
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
const [contracts, setContracts] = useState<Contract[] | undefined>()
|
||||||
|
@ -26,6 +29,29 @@ export const useContracts = () => {
|
||||||
return contracts
|
return contracts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useContractsByDailyScoreGroups = (
|
||||||
|
groupSlugs: string[] | undefined
|
||||||
|
) => {
|
||||||
|
const facetFilters = ['isResolved:false']
|
||||||
|
|
||||||
|
const { data } = useQuery(['daily-score', groupSlugs], () =>
|
||||||
|
Promise.all(
|
||||||
|
(groupSlugs ?? []).map((slug) =>
|
||||||
|
dailyScoreIndex.search<CPMMBinaryContract>('', {
|
||||||
|
facetFilters: [...facetFilters, `groupLinks.slug:${slug}`],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (!groupSlugs || !data || data.length !== groupSlugs.length)
|
||||||
|
return undefined
|
||||||
|
|
||||||
|
return zipObject(
|
||||||
|
groupSlugs,
|
||||||
|
data.map((d) => d.hits.filter((c) => c.dailyScore))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const q = new QueryClient()
|
const q = new QueryClient()
|
||||||
export const getCachedContracts = async () =>
|
export const getCachedContracts = async () =>
|
||||||
q.fetchQuery(['contracts'], () => listAllContracts(1000), {
|
q.fetchQuery(['contracts'], () => listAllContracts(1000), {
|
||||||
|
|
|
@ -104,7 +104,7 @@ export const useMemberGroupIds = (user: User | null | undefined) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMemberGroupsSubscription(user: User | null | undefined) {
|
export function useMemberGroupsSubscription(user: User | null | undefined) {
|
||||||
const cachedGroups = useMemberGroups(user?.id) ?? []
|
const cachedGroups = useMemberGroups(user?.id)
|
||||||
const [groups, setGroups] = useState(cachedGroups)
|
const [groups, setGroups] = useState(cachedGroups)
|
||||||
|
|
||||||
const userId = user?.id
|
const userId = user?.id
|
||||||
|
|
|
@ -1,75 +1,47 @@
|
||||||
import { useFirestoreQueryData } from '@react-query-firebase/firestore'
|
import { CPMMBinaryContract } from 'common/contract'
|
||||||
import { CPMMContract } from 'common/contract'
|
import { sortBy, uniqBy } from 'lodash'
|
||||||
import { MINUTE_MS } from 'common/util/time'
|
import { useQuery } from 'react-query'
|
||||||
import { useQuery, useQueryClient } from 'react-query'
|
|
||||||
import {
|
import {
|
||||||
getProbChangesNegative,
|
probChangeAscendingIndex,
|
||||||
getProbChangesPositive,
|
probChangeDescendingIndex,
|
||||||
} from 'web/lib/firebase/contracts'
|
} from 'web/lib/service/algolia'
|
||||||
import { getValues } from 'web/lib/firebase/utils'
|
|
||||||
import { getIndexName, searchClient } from 'web/lib/service/algolia'
|
|
||||||
|
|
||||||
export const useProbChangesAlgolia = (userId: string) => {
|
export const useProbChanges = (
|
||||||
const { data: positiveData } = useQuery(['prob-change-day', userId], () =>
|
filters: { bettorId?: string; groupSlugs?: string[] } = {}
|
||||||
searchClient
|
) => {
|
||||||
.initIndex(getIndexName('prob-change-day'))
|
const { bettorId, groupSlugs } = filters
|
||||||
.search<CPMMContract>('', {
|
|
||||||
facetFilters: ['uniqueBettorIds:' + userId, 'isResolved:false'],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
const { data: negativeData } = useQuery(
|
|
||||||
['prob-change-day-ascending', userId],
|
|
||||||
() =>
|
|
||||||
searchClient
|
|
||||||
.initIndex(getIndexName('prob-change-day-ascending'))
|
|
||||||
.search<CPMMContract>('', {
|
|
||||||
facetFilters: ['uniqueBettorIds:' + userId, 'isResolved:false'],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!positiveData || !negativeData) {
|
const bettorFilter = bettorId ? `uniqueBettorIds:${bettorId}` : ''
|
||||||
return undefined
|
const groupFilters = groupSlugs
|
||||||
|
? groupSlugs.map((slug) => `groupLinks.slug:${slug}`)
|
||||||
|
: []
|
||||||
|
|
||||||
|
const facetFilters = [
|
||||||
|
'isResolved:false',
|
||||||
|
'outcomeType:BINARY',
|
||||||
|
bettorFilter,
|
||||||
|
groupFilters,
|
||||||
|
]
|
||||||
|
const searchParams = {
|
||||||
|
facetFilters,
|
||||||
|
hitsPerPage: 50,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const { data: positiveChanges } = useQuery(
|
||||||
positiveChanges: positiveData.hits
|
['prob-change-day', groupSlugs],
|
||||||
.filter((c) => c.probChanges && c.probChanges.day > 0)
|
() => probChangeDescendingIndex.search<CPMMBinaryContract>('', searchParams)
|
||||||
.filter((c) => c.outcomeType === 'BINARY'),
|
)
|
||||||
negativeChanges: negativeData.hits
|
const { data: negativeChanges } = useQuery(
|
||||||
.filter((c) => c.probChanges && c.probChanges.day < 0)
|
['prob-change-day-ascending', groupSlugs],
|
||||||
.filter((c) => c.outcomeType === 'BINARY'),
|
() => probChangeAscendingIndex.search<CPMMBinaryContract>('', searchParams)
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
if (!positiveChanges || !negativeChanges) return undefined
|
||||||
export const useProbChanges = (userId: string) => {
|
|
||||||
const { data: positiveChanges } = useFirestoreQueryData(
|
const hits = uniqBy(
|
||||||
['prob-changes-day-positive', userId],
|
[...positiveChanges.hits, ...negativeChanges.hits],
|
||||||
getProbChangesPositive(userId)
|
(c) => c.id
|
||||||
)
|
)
|
||||||
const { data: negativeChanges } = useFirestoreQueryData(
|
|
||||||
['prob-changes-day-negative', userId],
|
return sortBy(hits, (c) => Math.abs(c.probChanges.day)).reverse()
|
||||||
getProbChangesNegative(userId)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!positiveChanges || !negativeChanges) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return { positiveChanges, negativeChanges }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const usePrefetchProbChanges = (userId: string | undefined) => {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
if (userId) {
|
|
||||||
queryClient.prefetchQuery(
|
|
||||||
['prob-changes-day-positive', userId],
|
|
||||||
() => getValues(getProbChangesPositive(userId)),
|
|
||||||
{ staleTime: MINUTE_MS }
|
|
||||||
)
|
|
||||||
queryClient.prefetchQuery(
|
|
||||||
['prob-changes-day-negative', userId],
|
|
||||||
() => getValues(getProbChangesNegative(userId)),
|
|
||||||
{ staleTime: MINUTE_MS }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
import { partition, sortBy, sum, uniqBy } from 'lodash'
|
import { partition, sortBy, sum, uniqBy } from 'lodash'
|
||||||
|
|
||||||
import { coll, getValues, listenForValue, listenForValues } from './utils'
|
import { coll, getValues, listenForValue, listenForValues } from './utils'
|
||||||
import { BinaryContract, Contract, CPMMContract } from 'common/contract'
|
import { BinaryContract, Contract } from 'common/contract'
|
||||||
import { chooseRandomSubset } from 'common/util/random'
|
import { chooseRandomSubset } from 'common/util/random'
|
||||||
import { formatMoney, formatPercent } from 'common/util/format'
|
import { formatMoney, formatPercent } from 'common/util/format'
|
||||||
import { DAY_MS } from 'common/util/time'
|
import { DAY_MS } from 'common/util/time'
|
||||||
|
@ -426,21 +426,3 @@ export async function getRecentBetsAndComments(contract: Contract) {
|
||||||
recentComments,
|
recentComments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getProbChangesPositive = (userId: string) =>
|
|
||||||
query(
|
|
||||||
contracts,
|
|
||||||
where('uniqueBettorIds', 'array-contains', userId),
|
|
||||||
where('probChanges.day', '>', 0),
|
|
||||||
orderBy('probChanges.day', 'desc'),
|
|
||||||
limit(10)
|
|
||||||
) as Query<CPMMContract>
|
|
||||||
|
|
||||||
export const getProbChangesNegative = (userId: string) =>
|
|
||||||
query(
|
|
||||||
contracts,
|
|
||||||
where('uniqueBettorIds', 'array-contains', userId),
|
|
||||||
where('probChanges.day', '<', 0),
|
|
||||||
orderBy('probChanges.day', 'asc'),
|
|
||||||
limit(10)
|
|
||||||
) as Query<CPMMContract>
|
|
||||||
|
|
|
@ -13,3 +13,13 @@ export const searchIndexName =
|
||||||
export const getIndexName = (sort: string) => {
|
export const getIndexName = (sort: string) => {
|
||||||
return `${indexPrefix}contracts-${sort}`
|
return `${indexPrefix}contracts-${sort}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const probChangeDescendingIndex = searchClient.initIndex(
|
||||||
|
getIndexName('prob-change-day')
|
||||||
|
)
|
||||||
|
export const probChangeAscendingIndex = searchClient.initIndex(
|
||||||
|
getIndexName('prob-change-day-ascending')
|
||||||
|
)
|
||||||
|
export const dailyScoreIndex = searchClient.initIndex(
|
||||||
|
getIndexName('daily-score')
|
||||||
|
)
|
||||||
|
|
|
@ -2,14 +2,17 @@ import { ProbChangeTable } from 'web/components/contract/prob-change-table'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
import { useProbChangesAlgolia } from 'web/hooks/use-prob-changes'
|
import { useProbChanges } from 'web/hooks/use-prob-changes'
|
||||||
import { useTracking } from 'web/hooks/use-tracking'
|
import { useTracking } from 'web/hooks/use-tracking'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
|
||||||
export default function DailyMovers() {
|
export default function DailyMovers() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
const bettorId = user?.id ?? undefined
|
||||||
|
|
||||||
const changes = useProbChangesAlgolia(user?.id ?? '')
|
const changes = useProbChanges({ bettorId })?.filter(
|
||||||
|
(c) => Math.abs(c.probChanges.day) >= 0.01
|
||||||
|
)
|
||||||
|
|
||||||
useTracking('view daily movers')
|
useTracking('view daily movers')
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default function Home() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const groups = useMemberGroupsSubscription(user)
|
const groups = useMemberGroupsSubscription(user)
|
||||||
const { sections } = getHomeItems(groups, homeSections)
|
const { sections } = getHomeItems(groups ?? [], homeSections)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
import { PlusCircleIcon, XCircleIcon } from '@heroicons/react/outline'
|
import { PlusCircleIcon, XCircleIcon } from '@heroicons/react/outline'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { toast, Toaster } from 'react-hot-toast'
|
import { toast, Toaster } from 'react-hot-toast'
|
||||||
|
import { Dictionary } from 'lodash'
|
||||||
|
|
||||||
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'
|
||||||
|
@ -31,11 +32,10 @@ import { ProbChangeTable } from 'web/components/contract/prob-change-table'
|
||||||
import { groupPath, joinGroup, leaveGroup } from 'web/lib/firebase/groups'
|
import { groupPath, joinGroup, leaveGroup } from 'web/lib/firebase/groups'
|
||||||
import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
|
import { usePortfolioHistory } from 'web/hooks/use-portfolio-history'
|
||||||
import { formatMoney } from 'common/util/format'
|
import { formatMoney } from 'common/util/format'
|
||||||
import { useProbChangesAlgolia } from 'web/hooks/use-prob-changes'
|
import { useProbChanges } from 'web/hooks/use-prob-changes'
|
||||||
import { ProfitBadge } from 'web/components/bets-list'
|
import { ProfitBadge } from 'web/components/bets-list'
|
||||||
import { calculatePortfolioProfit } from 'common/calculate-metrics'
|
import { calculatePortfolioProfit } from 'common/calculate-metrics'
|
||||||
import { hasCompletedStreakToday } from 'web/components/profile/betting-streak-modal'
|
import { hasCompletedStreakToday } from 'web/components/profile/betting-streak-modal'
|
||||||
import { useContractsQuery } from 'web/hooks/use-contracts'
|
|
||||||
import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
import { ContractsGrid } from 'web/components/contract/contracts-grid'
|
||||||
import { PillButton } from 'web/components/buttons/pill-button'
|
import { PillButton } from 'web/components/buttons/pill-button'
|
||||||
import { filterDefined } from 'common/util/array'
|
import { filterDefined } from 'common/util/array'
|
||||||
|
@ -43,6 +43,8 @@ import { updateUser } from 'web/lib/firebase/users'
|
||||||
import { isArray, keyBy } from 'lodash'
|
import { isArray, keyBy } from 'lodash'
|
||||||
import { usePrefetch } from 'web/hooks/use-prefetch'
|
import { usePrefetch } from 'web/hooks/use-prefetch'
|
||||||
import { Title } from 'web/components/title'
|
import { Title } from 'web/components/title'
|
||||||
|
import { CPMMBinaryContract } from 'common/contract'
|
||||||
|
import { useContractsByDailyScoreGroups } from 'web/hooks/use-contracts'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -54,20 +56,19 @@ export default function Home() {
|
||||||
|
|
||||||
const groups = useMemberGroupsSubscription(user)
|
const groups = useMemberGroupsSubscription(user)
|
||||||
|
|
||||||
const { sections } = getHomeItems(groups, user?.homeSections ?? [])
|
const { sections } = getHomeItems(groups ?? [], user?.homeSections ?? [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (user && !user.homeSections && sections.length > 0 && groups) {
|
||||||
user &&
|
|
||||||
!user.homeSections &&
|
|
||||||
sections.length > 0 &&
|
|
||||||
groups.length > 0
|
|
||||||
) {
|
|
||||||
// Save initial home sections.
|
// Save initial home sections.
|
||||||
updateUser(user.id, { homeSections: sections.map((s) => s.id) })
|
updateUser(user.id, { homeSections: sections.map((s) => s.id) })
|
||||||
}
|
}
|
||||||
}, [user, sections, groups])
|
}, [user, sections, groups])
|
||||||
|
|
||||||
|
const groupContracts = useContractsByDailyScoreGroups(
|
||||||
|
groups?.map((g) => g.slug)
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
@ -81,9 +82,13 @@ export default function Home() {
|
||||||
<DailyStats user={user} />
|
<DailyStats user={user} />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
{sections.map((section) => renderSection(section, user, groups))}
|
<>
|
||||||
|
{sections.map((section) =>
|
||||||
|
renderSection(section, user, groups, groupContracts)
|
||||||
|
)}
|
||||||
|
|
||||||
<TrendingGroupsSection user={user} />
|
<TrendingGroupsSection user={user} />
|
||||||
|
</>
|
||||||
</Col>
|
</Col>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -134,7 +139,8 @@ export const getHomeItems = (groups: Group[], sections: string[]) => {
|
||||||
function renderSection(
|
function renderSection(
|
||||||
section: { id: string; label: string },
|
section: { id: string; label: string },
|
||||||
user: User | null | undefined,
|
user: User | null | undefined,
|
||||||
groups: Group[]
|
groups: Group[] | undefined,
|
||||||
|
groupContracts: Dictionary<CPMMBinaryContract[]> | undefined
|
||||||
) {
|
) {
|
||||||
const { id, label } = section
|
const { id, label } = section
|
||||||
if (id === 'daily-movers') {
|
if (id === 'daily-movers') {
|
||||||
|
@ -156,8 +162,23 @@ function renderSection(
|
||||||
<SearchSection key={id} label={label} sort={sort.value} user={user} />
|
<SearchSection key={id} label={label} sort={sort.value} user={user} />
|
||||||
)
|
)
|
||||||
|
|
||||||
const group = groups.find((g) => g.id === id)
|
if (groups && groupContracts) {
|
||||||
if (group) return <GroupSection key={id} group={group} user={user} />
|
const group = groups.find((g) => g.id === id)
|
||||||
|
if (group) {
|
||||||
|
const contracts = groupContracts[group.slug].filter(
|
||||||
|
(c) => Math.abs(c.probChanges.day) >= 0.01
|
||||||
|
)
|
||||||
|
if (contracts.length === 0) return null
|
||||||
|
return (
|
||||||
|
<GroupSection
|
||||||
|
key={id}
|
||||||
|
group={group}
|
||||||
|
user={user}
|
||||||
|
contracts={contracts}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -207,7 +228,6 @@ function SearchSection(props: {
|
||||||
defaultPill={pill}
|
defaultPill={pill}
|
||||||
noControls
|
noControls
|
||||||
maxResults={6}
|
maxResults={6}
|
||||||
headerClassName="sticky"
|
|
||||||
persistPrefix={`home-${sort}`}
|
persistPrefix={`home-${sort}`}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -217,10 +237,9 @@ function SearchSection(props: {
|
||||||
function GroupSection(props: {
|
function GroupSection(props: {
|
||||||
group: Group
|
group: Group
|
||||||
user: User | null | undefined | undefined
|
user: User | null | undefined | undefined
|
||||||
|
contracts: CPMMBinaryContract[]
|
||||||
}) {
|
}) {
|
||||||
const { group, user } = props
|
const { group, user, contracts } = props
|
||||||
|
|
||||||
const contracts = useContractsQuery('score', 4, { groupSlug: group.slug })
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
|
@ -245,22 +264,22 @@ function GroupSection(props: {
|
||||||
<XCircleIcon className={'h-5 w-5 flex-shrink-0'} aria-hidden="true" />
|
<XCircleIcon className={'h-5 w-5 flex-shrink-0'} aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
</SectionHeader>
|
</SectionHeader>
|
||||||
<ContractsGrid contracts={contracts} />
|
<ContractsGrid
|
||||||
|
contracts={contracts.slice(0, 4)}
|
||||||
|
cardUIOptions={{ showProbChange: true }}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DailyMoversSection(props: { userId: string | null | undefined }) {
|
function DailyMoversSection(props: { userId: string | null | undefined }) {
|
||||||
const { userId } = props
|
const { userId } = props
|
||||||
const changes = useProbChangesAlgolia(userId ?? '')
|
const changes = useProbChanges({ bettorId: userId ?? undefined })?.filter(
|
||||||
|
(c) => Math.abs(c.probChanges.day) >= 0.01
|
||||||
|
)
|
||||||
|
|
||||||
if (changes) {
|
if (changes && changes.length === 0) {
|
||||||
const { positiveChanges, negativeChanges } = changes
|
return null
|
||||||
if (
|
|
||||||
!positiveChanges.find((c) => c.probChanges.day >= 0.01) ||
|
|
||||||
!negativeChanges.find((c) => c.probChanges.day <= -0.01)
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -332,6 +351,10 @@ export function TrendingGroupsSection(props: {
|
||||||
const count = full ? 100 : 25
|
const count = full ? 100 : 25
|
||||||
const chosenGroups = groups.slice(0, count)
|
const chosenGroups = groups.slice(0, count)
|
||||||
|
|
||||||
|
if (chosenGroups.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col className={className}>
|
<Col className={className}>
|
||||||
<SectionHeader label="Trending groups" href="/explore-groups">
|
<SectionHeader label="Trending groups" href="/explore-groups">
|
||||||
|
|
Loading…
Reference in New Issue
Block a user