diff --git a/web/components/profile-menu.tsx b/web/components/profile-menu.tsx index 4bbe48c4..b732d059 100644 --- a/web/components/profile-menu.tsx +++ b/web/components/profile-menu.tsx @@ -51,6 +51,10 @@ function getNavigationOptions(user: User, options: { mobile: boolean }) { name: 'Your markets', href: `/${user.username}`, }, + { + name: 'Leaderboards', + href: '/leaderboards', + }, { name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh', diff --git a/web/lib/firebase/users.ts b/web/lib/firebase/users.ts index e4a07406..ad276155 100644 --- a/web/lib/firebase/users.ts +++ b/web/lib/firebase/users.ts @@ -10,6 +10,7 @@ import { where, limit, getDocs, + orderBy, } from 'firebase/firestore' import { getAuth } from 'firebase/auth' import { ref, getStorage, uploadBytes, getDownloadURL } from 'firebase/storage' @@ -20,7 +21,7 @@ import { } from 'firebase/auth' import { User } from '../../../common/user' -import { listenForValues } from './utils' +import { getValues, listenForValues } from './utils' export type { User } export const STARTING_BALANCE = 1000 @@ -127,3 +128,24 @@ export function listenForAllUsers(setUsers: (users: User[]) => void) { const q = query(userCollection) listenForValues(q, setUsers) } + +const topTradersQuery = query( + collection(db, 'users'), + orderBy('totalPnLCached', 'desc'), + limit(21) +) + +export async function getTopTraders() { + const users = await getValues(topTradersQuery) + return users.filter((user) => user.username !== 'SG').slice(0, 20) +} + +const topCreatorsQuery = query( + collection(db, 'users'), + orderBy('creatorVolumeCached', 'desc'), + limit(20) +) + +export function getTopCreators() { + return getValues(topCreatorsQuery) +} diff --git a/web/pages/leaderboards.tsx b/web/pages/leaderboards.tsx new file mode 100644 index 00000000..30a22185 --- /dev/null +++ b/web/pages/leaderboards.tsx @@ -0,0 +1,112 @@ +import _ from 'lodash' +import Image from 'next/image' +import { Col } from '../components/layout/col' +import { Row } from '../components/layout/row' +import { Page } from '../components/page' +import { SiteLink } from '../components/site-link' +import { Title } from '../components/title' +import { getTopCreators, getTopTraders, User } from '../lib/firebase/users' +import { formatMoney } from '../lib/util/format' + +export async function getStaticProps() { + const [topTraders, topCreators] = await Promise.all([ + getTopTraders().catch((_) => {}), + getTopCreators().catch((_) => {}), + ]) + + return { + props: { + topTraders, + topCreators, + }, + + revalidate: 60, // regenerate after a minute + } +} + +export default function Leaderboards(props: { + topTraders: User[] + topCreators: User[] +}) { + const { topTraders, topCreators } = props + + return ( + + + formatMoney(user.totalPnLCached), + }, + ]} + /> + formatMoney(user.creatorVolumeCached), + }, + ]} + /> + + + ) +} + +function Leaderboard(props: { + title: string + users: User[] + columns: { + header: string + renderCell: (user: User) => any + }[] +}) { + const { title, users, columns } = props + return ( +
+ + <div className="overflow-x-auto"> + <table className="table table-zebra table-compact text-gray-500 w-full"> + <thead> + <tr className="p-2"> + <th>#</th> + <th>Name</th> + {columns.map((column) => ( + <th key={column.header}>{column.header}</th> + ))} + </tr> + </thead> + <tbody> + {users.map((user, index) => ( + <tr key={user.id}> + <td>{index + 1}</td> + <td> + <SiteLink className="relative" href={`/${user.username}`}> + <Row className="items-center gap-4"> + <Image + className="rounded-full bg-gray-400 flex-shrink-0 ring-8 ring-gray-50" + src={user.avatarUrl} + alt="" + width={32} + height={32} + /> + <div>{user.name}</div> + </Row> + </SiteLink> + </td> + {columns.map((column) => ( + <td key={column.header}>{column.renderCell(user)}</td> + ))} + </tr> + ))} + </tbody> + </table> + </div> + </div> + ) +}