Add trending groups section
This commit is contained in:
parent
a7e3414cc9
commit
7744ac84e1
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 ?? '')
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user