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