From 7744ac84e112c113f9a3e8d1d11186615ebd2bad Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 14 Sep 2022 00:40:22 -0500 Subject: [PATCH] Add trending groups section --- common/util/random.ts | 7 +++ web/components/arrange-home.tsx | 2 +- web/hooks/use-group.ts | 19 +++++- web/lib/firebase/contracts.ts | 9 +-- web/pages/experimental/explore-groups.tsx | 26 ++------ web/pages/experimental/home/index.tsx | 77 ++++++++++++++++++----- 6 files changed, 93 insertions(+), 47 deletions(-) diff --git a/common/util/random.ts b/common/util/random.ts index c26b361b..93a574ab 100644 --- a/common/util/random.ts +++ b/common/util/random.ts @@ -46,3 +46,10 @@ export const shuffle = (array: unknown[], rand: () => number) => { ;[array[i], array[swapIndex]] = [array[swapIndex], array[i]] } } + +export function chooseRandomSubset(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) +} diff --git a/web/components/arrange-home.tsx b/web/components/arrange-home.tsx index 212b358e..25e814b8 100644 --- a/web/components/arrange-home.tsx +++ b/web/components/arrange-home.tsx @@ -108,9 +108,9 @@ const SectionItem = (props: { export const getHomeItems = (groups: Group[], sections: string[]) => { const items = [ + { label: 'Daily movers', id: 'daily-movers' }, { label: 'Trending', id: 'score' }, { label: 'New for you', id: 'newest' }, - { label: 'Daily movers', id: 'daily-movers' }, ...groups.map((g) => ({ label: g.name, id: g.id, diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 3948bd4a..dac97d8b 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -16,11 +16,12 @@ import { import { getUser } from 'web/lib/firebase/users' import { filterDefined } from 'common/util/array' import { Contract } from 'common/contract' -import { uniq } from 'lodash' +import { keyBy, uniq, uniqBy } from 'lodash' import { listenForValues } from 'web/lib/firebase/utils' import { useQuery } from 'react-query' import { useFirestoreQueryData } from '@react-query-firebase/firestore' import { limit, query } from 'firebase/firestore' +import { useTrendingContracts } from './use-contracts' export const useGroup = (groupId: string | undefined) => { const [group, setGroup] = useState() @@ -60,6 +61,22 @@ export const useTopFollowedGroups = (count: number) => { 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) => { const result = useQuery(['member-groups', userId ?? ''], () => getMemberGroups(userId ?? '') diff --git a/web/lib/firebase/contracts.ts b/web/lib/firebase/contracts.ts index bdb25684..89af8235 100644 --- a/web/lib/firebase/contracts.ts +++ b/web/lib/firebase/contracts.ts @@ -17,7 +17,7 @@ import { partition, sortBy, sum, uniqBy } from 'lodash' import { coll, getValues, listenForValue, listenForValues } from './utils' 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 { DAY_MS } from 'common/util/time' import { Bet } from 'common/bet' @@ -238,13 +238,6 @@ export async function unFollowContract(contractId: string, userId: string) { 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( contracts, where('isResolved', '==', false), diff --git a/web/pages/experimental/explore-groups.tsx b/web/pages/experimental/explore-groups.tsx index 26a848b3..dd42f4b2 100644 --- a/web/pages/experimental/explore-groups.tsx +++ b/web/pages/experimental/explore-groups.tsx @@ -1,51 +1,37 @@ 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 { Row } from 'web/components/layout/row' import { Page } from 'web/components/page' import { Title } from 'web/components/title' -import { useTrendingContracts } from 'web/hooks/use-contracts' -import { useTopFollowedGroups } from 'web/hooks/use-group' +import { useMemberGroupIds, useTrendingGroups } from 'web/hooks/use-group' import { useUser } from 'web/hooks/use-user' import { GroupCard } from '../groups' export default function Explore() { const user = useUser() - - 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 - ) - const groups = filterDefined( - groupLinks.map((link) => groupsById[link.groupId]) - ).filter((group) => group.totalMembers >= 3) + const groups = useTrendingGroups() + const memberGroupIds = useMemberGroupIds(user) || [] return ( - + <Title className="!mb-0" text="Trending groups" /> </Row> <Masonry - // Show only 1 column on tailwind's md breakpoint (768px) breakpointCols={{ default: 3, 1200: 2, 570: 1 }} className="-ml-4 flex w-auto self-center" columnClassName="pl-4 bg-clip-padding" > {groups.map((g) => ( <GroupCard + key={g.id} className="mb-4 !min-w-[250px]" group={g} creator={null} user={user} - isMember={false} + isMember={memberGroupIds.includes(g.id)} /> ))} </Masonry> diff --git a/web/pages/experimental/home/index.tsx b/web/pages/experimental/home/index.tsx index f00e071f..5217e0cc 100644 --- a/web/pages/experimental/home/index.tsx +++ b/web/pages/experimental/home/index.tsx @@ -7,6 +7,7 @@ import { SearchIcon, } from '@heroicons/react/solid' import clsx from 'clsx' +import Masonry from 'react-masonry-css' import { Page } from 'web/components/page' import { Col } from 'web/components/layout/col' @@ -19,7 +20,11 @@ import { Sort } from 'web/components/contract-search' import { Group } from 'common/group' import { SiteLink } from 'web/components/site-link' 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 { getHomeItems } from '../../../components/arrange-home' 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 { ProfitBadge } from 'web/components/bets-list' import { calculatePortfolioProfit } from 'common/calculate-metrics' +import { GroupCard } from 'web/pages/groups' +import { chooseRandomSubset } from 'common/util/random' export default function Home() { const user = useUser() @@ -78,6 +85,7 @@ export default function Home() { return null })} + <TrendingGroupsSection user={user} /> </Col> <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: { label: string 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 }) { const { className } = props @@ -229,3 +237,38 @@ function DailyProfitAndBalance(props: { </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> + ) +}