Add trending groups section

This commit is contained in:
James Grugett 2022-09-14 00:40:22 -05:00
parent a7e3414cc9
commit 7744ac84e1
6 changed files with 93 additions and 47 deletions

View File

@ -46,3 +46,10 @@ export const shuffle = (array: unknown[], rand: () => number) => {
;[array[i], array[swapIndex]] = [array[swapIndex], array[i]] ;[array[i], array[swapIndex]] = [array[swapIndex], array[i]]
} }
} }
export function chooseRandomSubset<T>(items: T[], count: number) {
const fiveMinutes = 5 * 60 * 1000
const seed = Math.round(Date.now() / fiveMinutes).toString()
shuffle(items, createRNG(seed))
return items.slice(0, count)
}

View File

@ -108,9 +108,9 @@ const SectionItem = (props: {
export const getHomeItems = (groups: Group[], sections: string[]) => { export const getHomeItems = (groups: Group[], sections: string[]) => {
const items = [ const items = [
{ label: 'Daily movers', id: 'daily-movers' },
{ label: 'Trending', id: 'score' }, { label: 'Trending', id: 'score' },
{ label: 'New for you', id: 'newest' }, { label: 'New for you', id: 'newest' },
{ label: 'Daily movers', id: 'daily-movers' },
...groups.map((g) => ({ ...groups.map((g) => ({
label: g.name, label: g.name,
id: g.id, id: g.id,

View File

@ -16,11 +16,12 @@ import {
import { getUser } from 'web/lib/firebase/users' import { getUser } from 'web/lib/firebase/users'
import { filterDefined } from 'common/util/array' import { filterDefined } from 'common/util/array'
import { Contract } from 'common/contract' import { Contract } from 'common/contract'
import { uniq } from 'lodash' import { keyBy, uniq, uniqBy } from 'lodash'
import { listenForValues } from 'web/lib/firebase/utils' import { listenForValues } from 'web/lib/firebase/utils'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { useFirestoreQueryData } from '@react-query-firebase/firestore' import { useFirestoreQueryData } from '@react-query-firebase/firestore'
import { limit, query } from 'firebase/firestore' import { limit, query } from 'firebase/firestore'
import { useTrendingContracts } from './use-contracts'
export const useGroup = (groupId: string | undefined) => { export const useGroup = (groupId: string | undefined) => {
const [group, setGroup] = useState<Group | null | undefined>() const [group, setGroup] = useState<Group | null | undefined>()
@ -60,6 +61,22 @@ export const useTopFollowedGroups = (count: number) => {
return result.data return result.data
} }
export const useTrendingGroups = () => {
const topGroups = useTopFollowedGroups(200)
const groupsById = keyBy(topGroups, 'id')
const trendingContracts = useTrendingContracts(200)
const groupLinks = uniqBy(
(trendingContracts ?? []).map((c) => c.groupLinks ?? []).flat(),
(link) => link.groupId
)
return filterDefined(
groupLinks.map((link) => groupsById[link.groupId])
).filter((group) => group.totalMembers >= 3)
}
export const useMemberGroups = (userId: string | null | undefined) => { export const useMemberGroups = (userId: string | null | undefined) => {
const result = useQuery(['member-groups', userId ?? ''], () => const result = useQuery(['member-groups', userId ?? ''], () =>
getMemberGroups(userId ?? '') getMemberGroups(userId ?? '')

View File

@ -17,7 +17,7 @@ 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, CPMMContract } from 'common/contract'
import { createRNG, shuffle } 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'
import { Bet } from 'common/bet' import { Bet } from 'common/bet'
@ -238,13 +238,6 @@ export async function unFollowContract(contractId: string, userId: string) {
await deleteDoc(followDoc) await deleteDoc(followDoc)
} }
function chooseRandomSubset(contracts: Contract[], count: number) {
const fiveMinutes = 5 * 60 * 1000
const seed = Math.round(Date.now() / fiveMinutes).toString()
shuffle(contracts, createRNG(seed))
return contracts.slice(0, count)
}
const hotContractsQuery = query( const hotContractsQuery = query(
contracts, contracts,
where('isResolved', '==', false), where('isResolved', '==', false),

View File

@ -1,51 +1,37 @@
import Masonry from 'react-masonry-css' import Masonry from 'react-masonry-css'
import { filterDefined } from 'common/util/array'
import { keyBy, uniqBy } from 'lodash'
import { Col } from 'web/components/layout/col' import { Col } from 'web/components/layout/col'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
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 { useTrendingContracts } from 'web/hooks/use-contracts' import { useMemberGroupIds, useTrendingGroups } from 'web/hooks/use-group'
import { useTopFollowedGroups } from 'web/hooks/use-group'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { GroupCard } from '../groups' import { GroupCard } from '../groups'
export default function Explore() { export default function Explore() {
const user = useUser() const user = useUser()
const groups = useTrendingGroups()
const topGroups = useTopFollowedGroups(200) const memberGroupIds = useMemberGroupIds(user) || []
const groupsById = keyBy(topGroups, 'id')
const trendingContracts = useTrendingContracts(200)
const groupLinks = uniqBy(
(trendingContracts ?? []).map((c) => c.groupLinks ?? []).flat(),
(link) => link.groupId
)
const groups = filterDefined(
groupLinks.map((link) => groupsById[link.groupId])
).filter((group) => group.totalMembers >= 3)
return ( return (
<Page> <Page>
<Col className="pm:mx-10 gap-4 px-4 pb-12 xl:w-[115%]"> <Col className="pm:mx-10 gap-4 px-4 pb-12 xl:w-[115%]">
<Row className={'w-full items-center justify-between'}> <Row className={'w-full items-center justify-between'}>
<Title className="!mb-0" text="Explore" /> <Title className="!mb-0" text="Trending groups" />
</Row> </Row>
<Masonry <Masonry
// Show only 1 column on tailwind's md breakpoint (768px)
breakpointCols={{ default: 3, 1200: 2, 570: 1 }} breakpointCols={{ default: 3, 1200: 2, 570: 1 }}
className="-ml-4 flex w-auto self-center" className="-ml-4 flex w-auto self-center"
columnClassName="pl-4 bg-clip-padding" columnClassName="pl-4 bg-clip-padding"
> >
{groups.map((g) => ( {groups.map((g) => (
<GroupCard <GroupCard
key={g.id}
className="mb-4 !min-w-[250px]" className="mb-4 !min-w-[250px]"
group={g} group={g}
creator={null} creator={null}
user={user} user={user}
isMember={false} isMember={memberGroupIds.includes(g.id)}
/> />
))} ))}
</Masonry> </Masonry>

View File

@ -7,6 +7,7 @@ import {
SearchIcon, SearchIcon,
} from '@heroicons/react/solid' } from '@heroicons/react/solid'
import clsx from 'clsx' import clsx from 'clsx'
import Masonry from 'react-masonry-css'
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'
@ -19,7 +20,11 @@ import { Sort } from 'web/components/contract-search'
import { Group } from 'common/group' import { Group } from 'common/group'
import { SiteLink } from 'web/components/site-link' import { SiteLink } from 'web/components/site-link'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import { useMemberGroups } from 'web/hooks/use-group' import {
useMemberGroupIds,
useMemberGroups,
useTrendingGroups,
} from 'web/hooks/use-group'
import { Button } from 'web/components/button' import { Button } from 'web/components/button'
import { getHomeItems } from '../../../components/arrange-home' import { getHomeItems } from '../../../components/arrange-home'
import { Title } from 'web/components/title' import { Title } from 'web/components/title'
@ -31,6 +36,8 @@ import { formatMoney } from 'common/util/format'
import { useProbChanges } 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 { GroupCard } from 'web/pages/groups'
import { chooseRandomSubset } from 'common/util/random'
export default function Home() { export default function Home() {
const user = useUser() const user = useUser()
@ -78,6 +85,7 @@ export default function Home() {
return null return null
})} })}
<TrendingGroupsSection user={user} />
</Col> </Col>
<button <button
type="button" type="button"
@ -93,6 +101,22 @@ export default function Home() {
) )
} }
function SectionHeader(props: { label: string; href: string }) {
const { label, href } = props
return (
<Row className="mb-3 items-center justify-between">
<SiteLink className="text-xl" href={href}>
{label}{' '}
<ArrowSmRightIcon
className="mb-0.5 inline h-6 w-6 text-gray-500"
aria-hidden="true"
/>
</SiteLink>
</Row>
)
}
function SearchSection(props: { function SearchSection(props: {
label: string label: string
user: User | null | undefined | undefined user: User | null | undefined | undefined
@ -152,22 +176,6 @@ function DailyMoversSection(props: { userId: string | null | undefined }) {
) )
} }
function SectionHeader(props: { label: string; href: string }) {
const { label, href } = props
return (
<Row className="mb-3 items-center justify-between">
<SiteLink className="text-xl" href={href}>
{label}{' '}
<ArrowSmRightIcon
className="mb-0.5 inline h-6 w-6 text-gray-500"
aria-hidden="true"
/>
</SiteLink>
</Row>
)
}
function EditButton(props: { className?: string }) { function EditButton(props: { className?: string }) {
const { className } = props const { className } = props
@ -229,3 +237,38 @@ function DailyProfitAndBalance(props: {
</Row> </Row>
) )
} }
function TrendingGroupsSection(props: { user: User | null | undefined }) {
const { user } = props
const memberGroupIds = useMemberGroupIds(user) || []
const groups = useTrendingGroups().filter(
(g) => !memberGroupIds.includes(g.id)
)
const chosenGroups = chooseRandomSubset(groups.slice(0, 25), 9)
return (
<Col>
<SectionHeader
label="Trending groups"
href="/experimental/explore-groups"
/>
<Masonry
breakpointCols={{ default: 3, 768: 2, 480: 1 }}
className="-ml-4 flex w-auto self-center"
columnClassName="pl-4 bg-clip-padding"
>
{chosenGroups.map((g) => (
<GroupCard
key={g.id}
className="mb-4 !min-w-[250px]"
group={g}
creator={null}
user={user}
isMember={memberGroupIds.includes(g.id)}
/>
))}
</Masonry>
</Col>
)
}