Group leaderboards show members only by default
This commit is contained in:
parent
8357361038
commit
da81035e58
38
web/components/widgets/short-toggle.tsx
Normal file
38
web/components/widgets/short-toggle.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/* This example requires Tailwind CSS v2.0+ */
|
||||||
|
import { Switch } from '@headlessui/react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export default function ShortToggle(props: {
|
||||||
|
enabled: boolean
|
||||||
|
setEnabled: (enabled: boolean) => void
|
||||||
|
}) {
|
||||||
|
const { enabled, setEnabled } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
checked={enabled}
|
||||||
|
onChange={setEnabled}
|
||||||
|
className="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Use setting</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="pointer-events-none absolute h-full w-full rounded-md bg-white"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={clsx(
|
||||||
|
enabled ? 'bg-indigo-600' : 'bg-gray-200',
|
||||||
|
'pointer-events-none absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={clsx(
|
||||||
|
enabled ? 'translate-x-5' : 'translate-x-0',
|
||||||
|
'pointer-events-none absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow ring-0 transition-transform duration-200 ease-in-out'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
)
|
||||||
|
}
|
|
@ -65,19 +65,18 @@ export const useMemberGroupIds = (user: User | null | undefined) => {
|
||||||
export function useMembers(group: Group) {
|
export function useMembers(group: Group) {
|
||||||
const [members, setMembers] = useState<User[]>([])
|
const [members, setMembers] = useState<User[]>([])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { memberIds, creatorId } = group
|
const { memberIds } = group
|
||||||
if (memberIds.length > 1)
|
if (memberIds.length > 0) {
|
||||||
// get users via their user ids:
|
listMembers(group).then((members) => setMembers(members))
|
||||||
Promise.all(
|
}
|
||||||
memberIds.filter((mId) => mId !== creatorId).map(getUser)
|
|
||||||
).then((users) => {
|
|
||||||
const members = users.filter((user) => user)
|
|
||||||
setMembers(members)
|
|
||||||
})
|
|
||||||
}, [group])
|
}, [group])
|
||||||
return members
|
return members
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listMembers(group: Group) {
|
||||||
|
return await Promise.all(group.memberIds.map(getUser))
|
||||||
|
}
|
||||||
|
|
||||||
export const useGroupsWithContract = (contractId: string | undefined) => {
|
export const useGroupsWithContract = (contractId: string | undefined) => {
|
||||||
const [groups, setGroups] = useState<Group[] | null | undefined>()
|
const [groups, setGroups] = useState<Group[] | null | undefined>()
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { firebaseLogin, getUser, User } from 'web/lib/firebase/users'
|
||||||
import { Spacer } from 'web/components/layout/spacer'
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { useGroup, useMembers } from 'web/hooks/use-group'
|
import { listMembers, useGroup, useMembers } from 'web/hooks/use-group'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { scoreCreators, scoreTraders } from 'common/scoring'
|
import { scoreCreators, scoreTraders } from 'common/scoring'
|
||||||
import { Leaderboard } from 'web/components/leaderboard'
|
import { Leaderboard } from 'web/components/leaderboard'
|
||||||
|
@ -39,12 +39,14 @@ import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params'
|
||||||
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
import { ChoicesToggleGroup } from 'web/components/choices-toggle-group'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { useCommentsOnGroup } from 'web/hooks/use-comments'
|
import { useCommentsOnGroup } from 'web/hooks/use-comments'
|
||||||
|
import ShortToggle from 'web/components/widgets/short-toggle'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
const { slugs } = props.params
|
const { slugs } = props.params
|
||||||
|
|
||||||
const group = await getGroupBySlug(slugs[0])
|
const group = await getGroupBySlug(slugs[0])
|
||||||
|
const members = group ? await listMembers(group) : []
|
||||||
const creatorPromise = group ? getUser(group.creatorId) : null
|
const creatorPromise = group ? getUser(group.creatorId) : null
|
||||||
|
|
||||||
const contracts = group ? await getGroupContracts(group).catch((_) => []) : []
|
const contracts = group ? await getGroupContracts(group).catch((_) => []) : []
|
||||||
|
@ -65,6 +67,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
group,
|
group,
|
||||||
|
members,
|
||||||
creator,
|
creator,
|
||||||
traderScores,
|
traderScores,
|
||||||
topTraders,
|
topTraders,
|
||||||
|
@ -95,6 +98,7 @@ const groupSubpages = [undefined, 'chat', 'questions', 'details'] as const
|
||||||
|
|
||||||
export default function GroupPage(props: {
|
export default function GroupPage(props: {
|
||||||
group: Group | null
|
group: Group | null
|
||||||
|
members: User[]
|
||||||
creator: User
|
creator: User
|
||||||
traderScores: { [userId: string]: number }
|
traderScores: { [userId: string]: number }
|
||||||
topTraders: User[]
|
topTraders: User[]
|
||||||
|
@ -103,14 +107,21 @@ export default function GroupPage(props: {
|
||||||
}) {
|
}) {
|
||||||
props = usePropz(props, getStaticPropz) ?? {
|
props = usePropz(props, getStaticPropz) ?? {
|
||||||
group: null,
|
group: null,
|
||||||
|
members: [],
|
||||||
creator: null,
|
creator: null,
|
||||||
traderScores: {},
|
traderScores: {},
|
||||||
topTraders: [],
|
topTraders: [],
|
||||||
creatorScores: {},
|
creatorScores: {},
|
||||||
topCreators: [],
|
topCreators: [],
|
||||||
}
|
}
|
||||||
const { creator, traderScores, topTraders, creatorScores, topCreators } =
|
const {
|
||||||
props
|
creator,
|
||||||
|
members,
|
||||||
|
traderScores,
|
||||||
|
topTraders,
|
||||||
|
creatorScores,
|
||||||
|
topCreators,
|
||||||
|
} = props
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { slugs } = router.query as { slugs: string[] }
|
const { slugs } = router.query as { slugs: string[] }
|
||||||
|
@ -175,15 +186,15 @@ export default function GroupPage(props: {
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<Spacer h={8} />
|
<Spacer h={8} />
|
||||||
<Col className="mt-4 gap-8 px-4 md:flex-row">
|
|
||||||
<GroupLeaderboards
|
<GroupLeaderboards
|
||||||
traderScores={traderScores}
|
traderScores={traderScores}
|
||||||
creatorScores={creatorScores}
|
creatorScores={creatorScores}
|
||||||
topTraders={topTraders}
|
topTraders={topTraders}
|
||||||
topCreators={topCreators}
|
topCreators={topCreators}
|
||||||
user={user}
|
members={members}
|
||||||
/>
|
user={user}
|
||||||
</Col>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
@ -312,7 +323,7 @@ function GroupOverview(props: {
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
<Row className="items-center justify-end rounded-t bg-indigo-500 px-4 py-3 text-sm text-white">
|
<Row className="items-center justify-end rounded-t bg-indigo-500 px-4 py-3 text-sm text-white">
|
||||||
<Row className="flex-1 justify-start">About group</Row>
|
<Row className="flex-1 justify-start">About {group.name}</Row>
|
||||||
{isCreator && <EditGroupButton className={'ml-1'} group={group} />}
|
{isCreator && <EditGroupButton className={'ml-1'} group={group} />}
|
||||||
</Row>
|
</Row>
|
||||||
<Col className="gap-2 rounded-b bg-white p-4">
|
<Col className="gap-2 rounded-b bg-white p-4">
|
||||||
|
@ -324,7 +335,6 @@ function GroupOverview(props: {
|
||||||
username={creator.username}
|
username={creator.username}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<GroupMembersList group={group} />
|
|
||||||
<Row className={'items-center gap-1'}>
|
<Row className={'items-center gap-1'}>
|
||||||
<span className={'text-gray-500'}>Membership</span>
|
<span className={'text-gray-500'}>Membership</span>
|
||||||
{user && user.id === creator.id ? (
|
{user && user.id === creator.id ? (
|
||||||
|
@ -343,14 +353,6 @@ function GroupOverview(props: {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
{about && (
|
|
||||||
<>
|
|
||||||
<Spacer h={2} />
|
|
||||||
<div className="text-gray-500">
|
|
||||||
<Linkify text={about} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Col>
|
</Col>
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
@ -381,46 +383,94 @@ export function GroupMembersList(props: { group: Group }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SortedLeaderboard(props: {
|
||||||
|
users: User[]
|
||||||
|
scoreFunction: (user: User) => number
|
||||||
|
title: string
|
||||||
|
header: string
|
||||||
|
}) {
|
||||||
|
const { users, scoreFunction, title, header } = props
|
||||||
|
const sortedUsers = users.sort((a, b) => scoreFunction(b) - scoreFunction(a))
|
||||||
|
return (
|
||||||
|
<Leaderboard
|
||||||
|
className="max-w-xl"
|
||||||
|
users={sortedUsers}
|
||||||
|
title={title}
|
||||||
|
columns={[
|
||||||
|
{ header, renderCell: (user) => formatMoney(scoreFunction(user)) },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function GroupLeaderboards(props: {
|
function GroupLeaderboards(props: {
|
||||||
traderScores: { [userId: string]: number }
|
traderScores: { [userId: string]: number }
|
||||||
creatorScores: { [userId: string]: number }
|
creatorScores: { [userId: string]: number }
|
||||||
topTraders: User[]
|
topTraders: User[]
|
||||||
topCreators: User[]
|
topCreators: User[]
|
||||||
|
members: User[]
|
||||||
user: User | null | undefined
|
user: User | null | undefined
|
||||||
}) {
|
}) {
|
||||||
const { traderScores, creatorScores, topTraders, topCreators } = props
|
const { traderScores, creatorScores, members, topTraders, topCreators } =
|
||||||
|
props
|
||||||
const topTraderScores = topTraders.map((user) => traderScores[user.id])
|
const [includeOutsiders, setIncludeOutsiders] = useState(false)
|
||||||
const topCreatorScores = topCreators.map((user) => creatorScores[user.id])
|
|
||||||
|
|
||||||
|
// Consider hiding M$0
|
||||||
return (
|
return (
|
||||||
<>
|
<Col>
|
||||||
<Leaderboard
|
<Row className="items-center justify-end gap-4 text-gray-500">
|
||||||
className="max-w-xl"
|
Include all users
|
||||||
title="🏅 Top bettors"
|
<ShortToggle
|
||||||
users={topTraders}
|
enabled={includeOutsiders}
|
||||||
columns={[
|
setEnabled={setIncludeOutsiders}
|
||||||
{
|
/>
|
||||||
header: 'Profit',
|
</Row>
|
||||||
renderCell: (user) =>
|
|
||||||
formatMoney(topTraderScores[topTraders.indexOf(user)]),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Leaderboard
|
<div className="mt-4 flex flex-col gap-8 px-4 md:flex-row">
|
||||||
className="max-w-xl"
|
{!includeOutsiders ? (
|
||||||
title="🏅 Top creators"
|
<>
|
||||||
users={topCreators}
|
<SortedLeaderboard
|
||||||
columns={[
|
users={members}
|
||||||
{
|
scoreFunction={(user) => traderScores[user.id] ?? 0}
|
||||||
header: 'Market volume',
|
title="🏅 Top bettors"
|
||||||
renderCell: (user) =>
|
header="Profit"
|
||||||
formatMoney(topCreatorScores[topCreators.indexOf(user)]),
|
/>
|
||||||
},
|
<SortedLeaderboard
|
||||||
]}
|
users={members}
|
||||||
/>
|
scoreFunction={(user) => creatorScores[user.id] ?? 0}
|
||||||
</>
|
title="🏅 Top creators"
|
||||||
|
header="Market volume"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Leaderboard
|
||||||
|
className="max-w-xl"
|
||||||
|
title="🏅 Top bettors"
|
||||||
|
users={topTraders}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'Profit',
|
||||||
|
renderCell: (user) => formatMoney(traderScores[user.id] ?? 0),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<Leaderboard
|
||||||
|
className="max-w-xl"
|
||||||
|
title="🏅 Top creators"
|
||||||
|
users={topCreators}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
header: 'Market volume',
|
||||||
|
renderCell: (user) =>
|
||||||
|
formatMoney(creatorScores[user.id] ?? 0),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user