From b8e391f0f0bef7a0d175c3dfa6fbfd6a1d3d7151 Mon Sep 17 00:00:00 2001 From: Austin Chen Date: Fri, 18 Mar 2022 21:00:33 -0700 Subject: [PATCH] Show a top5 leaderboard on resolved markets --- web/components/leaderboard.tsx | 1 + web/lib/firebase/users.ts | 10 +++++ web/pages/[username]/[contractSlug].tsx | 49 ++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/web/components/leaderboard.tsx b/web/components/leaderboard.tsx index 30f4d596..5ae3ddd3 100644 --- a/web/components/leaderboard.tsx +++ b/web/components/leaderboard.tsx @@ -14,6 +14,7 @@ export function Leaderboard(props: { }[] className?: string }) { + // TODO: Ideally, highlight your own entry on the leaderboard const { title, users, columns, className } = props return (
diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index 599e711a..43253f45 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -126,6 +126,16 @@ export async function uploadData( return await getDownloadURL(uploadRef) } +export async function listUsers(userIds: string[]) { + if (userIds.length > 10) { + throw new Error('Too many users requested at once; Firestore limits to 10') + } + const userCollection = collection(db, 'users') + const q = query(userCollection, where('id', 'in', userIds)) + const docs = await getDocs(q) + return docs.docs.map((doc) => doc.data() as User) +} + export async function listAllUsers() { const userCollection = collection(db, 'users') const q = query(userCollection) diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 90929ec9..399d7587 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { useContractWithPreload } from '../../hooks/use-contract' import { ContractOverview } from '../../components/contract-overview' @@ -10,7 +10,7 @@ import { ContractBetsTable, MyBetsSummary } from '../../components/bets-list' import { useBets } from '../../hooks/use-bets' import { Title } from '../../components/title' import { Spacer } from '../../components/layout/spacer' -import { User } from '../../lib/firebase/users' +import { listUsers, User } from '../../lib/firebase/users' import { Contract, getContractFromSlug, @@ -30,6 +30,10 @@ import { listAllAnswers } from '../../lib/firebase/answers' import { Answer } from '../../../common/answer' import { AnswersPanel } from '../../components/answers/answers-panel' import { fromPropz, usePropz } from '../../hooks/use-propz' +import { Leaderboard } from '../../components/leaderboard' +import _ from 'lodash' +import { calculatePayout, resolvedPayout } from '../../../common/calculate' +import { formatMoney } from '../../../common/util/format' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { @@ -147,6 +151,9 @@ export default function ContractPage(props: { )} + {contract.isResolved && ( + + )}
@@ -195,6 +202,44 @@ function BetsSection(props: { ) } +function ContractLeaderboard(props: { contract: Contract; bets: Bet[] }) { + const { contract, bets } = props + const [users, setUsers] = useState() + + // Create a map of userIds to total profits + // TODO: Are we supposed to include sales...? + const betsWithoutSales = bets.filter((bet) => !(bet.isSold || bet.sale)) + const betsByUser = _.groupBy(betsWithoutSales, 'userId') + const userProfits = _.mapValues(betsByUser, (bets) => + _.sumBy(bets, (bet) => resolvedPayout(contract, bet) - bet.amount) + ) + + // Find the 5 users with the most profit + const topUsers = _.entries(userProfits).sort(([i1, p1], [i2, p2]) => p2 - p1) + const top5Ids = topUsers.slice(0, 5).map(([userId]) => userId) + + useEffect(() => { + listUsers(top5Ids).then((users) => { + const sortedUsers = _.sortBy(users, (user) => -userProfits[user.id]) + setUsers(sortedUsers) + }) + }, []) + + return ( + formatMoney(userProfits[user.id] || 0), + }, + ]} + className="mt-12 max-w-sm" + /> + ) +} + const getOpenGraphProps = (contract: Contract) => { const { resolution, question, creatorName, creatorUsername, outcomeType } = contract