From 5608fe91e02a2365f8580d97f2a1d26e20eb1dc3 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Thu, 12 Oct 2023 12:18:37 -0400 Subject: [PATCH 01/14] chore: copy leaderboard pagination feature from old branch --- .../desktop/Points/PointsLeaderBoard.tsx | 186 ++++++++++-------- apps/marginfi-v2-ui/src/pages/points.tsx | 89 ++++++++- .../marginfi-v2-ui-state/src/lib/points.ts | 54 +++-- 3 files changed, 212 insertions(+), 117 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index 248b446672..49b267a2e2 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -1,11 +1,11 @@ import React, { FC } from "react"; -import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material"; +import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Skeleton } from "@mui/material"; import { groupedNumberFormatterDyn } from "@mrgnlabs/mrgn-common"; import { LeaderboardRow } from "@mrgnlabs/marginfi-v2-ui-state"; interface PointsLeaderBoardProps { - leaderboardData: LeaderboardRow[]; + leaderboardData: LeaderboardRow[] | {}[]; currentUserId?: string; } @@ -63,90 +63,106 @@ export const PointsLeaderBoard: FC = ({ leaderboardData, - {leaderboardData.map((row: LeaderboardRow, index: number) => ( - - - {index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : index + 1} - - - { + if (!row.hasOwnProperty("id")) { + return ( + + {[...new Array(7)].map((_, index) => ( + + + + ))} + + ); + } + + const data = row as LeaderboardRow; + + return ( + + + {index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : index + 1} + + + + {`${data.id.slice(0, 5)}...${data.id.slice(-5)}`} + + + + + {groupedNumberFormatterDyn.format(Math.round(data.total_activity_deposit_points))} + + + {groupedNumberFormatterDyn.format(Math.round(data.total_activity_borrow_points))} + + + {groupedNumberFormatterDyn.format( + Math.round(data.total_referral_deposit_points + data.total_referral_borrow_points) + )} + + + {groupedNumberFormatterDyn.format(Math.round(data.socialPoints ? data.socialPoints : 0))} + + - {`${row.id.slice(0, 5)}...${row.id.slice(-5)}`} - - - - - {groupedNumberFormatterDyn.format(Math.round(row.total_activity_deposit_points))} - - - {groupedNumberFormatterDyn.format(Math.round(row.total_activity_borrow_points))} - - - {groupedNumberFormatterDyn.format( - Math.round(row.total_referral_deposit_points + row.total_referral_borrow_points) - )} - - - {groupedNumberFormatterDyn.format(Math.round(row.socialPoints ? row.socialPoints : 0))} - - - {groupedNumberFormatterDyn.format( - Math.round( - row.total_deposit_points + row.total_borrow_points + (row.socialPoints ? row.socialPoints : 0) - ) - )} - - - ))} + {groupedNumberFormatterDyn.format( + Math.round( + data.total_deposit_points + data.total_borrow_points + (data.socialPoints ? data.socialPoints : 0) + ) + )} + + + ); + })} diff --git a/apps/marginfi-v2-ui/src/pages/points.tsx b/apps/marginfi-v2-ui/src/pages/points.tsx index 3f336076f8..2448961ec2 100644 --- a/apps/marginfi-v2-ui/src/pages/points.tsx +++ b/apps/marginfi-v2-ui/src/pages/points.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, FC, useEffect, useState, useCallback } from "react"; +import React, { useMemo, FC, useEffect, useState, useCallback, useRef } from "react"; import Link from "next/link"; import { Button } from "@mui/material"; import FileCopyIcon from "@mui/icons-material/FileCopy"; @@ -31,13 +31,55 @@ const Points: FC = () => { state.userPointsData, ]); - const [leaderboardData, setLeaderboardData] = useState([]); + const leaderboardPerPage = 50; + const [leaderboardData, setLeaderboardData] = useState([ + ...new Array(leaderboardPerPage).fill({}), + ]); + const [leaderboardPage, setLeaderboardPage] = useState(0); + const [isFetchingLeaderboardPage, setIsFetchingLeaderboardPage] = useState(false); + const leaderboardSentinelRef = useRef(null); const [domain, setDomain] = useState(); const currentUserId = useMemo(() => domain ?? currentFirebaseUser?.uid, [currentFirebaseUser, domain]); const referralCode = useMemo(() => routerQuery.referralCode as string | undefined, [routerQuery.referralCode]); const [isReferralCopied, setIsReferralCopied] = useState(false); + // fetch next page of leaderboard results + const fetchLeaderboardPage = useCallback(async () => { + // grab last row of current leaderboard data for cursor + const lastRow = [...leaderboardData].filter((row) => row.hasOwnProperty("id"))[ + leaderboardPage * leaderboardPerPage - 2 + ] as LeaderboardRow; + if (!lastRow || !lastRow.hasOwnProperty("id")) return; + setIsFetchingLeaderboardPage(true); + + // fetch new page of data with cursor + const queryCursor = leaderboardData.length > 0 ? lastRow.doc : undefined; + setLeaderboardData((current) => [...current, ...new Array(50).fill({})]); + fetchLeaderboardData({ + connection, + queryCursor, + }).then((data) => { + // filter out skeleton rows + const filtered = [...leaderboardData].filter((row) => row.hasOwnProperty("id")); + + // additional check for duplicate values + const uniqueData = data.reduce((acc, curr) => { + const isDuplicate = acc.some((item) => { + const data = item as LeaderboardRow; + data.id === curr.id; + }); + if (!isDuplicate) { + acc.push(curr); + } + return acc; + }, filtered); + + setLeaderboardData(uniqueData); + setIsFetchingLeaderboardPage(false); + }); + }, [connection, leaderboardData, setLeaderboardData, setIsFetchingLeaderboardPage, leaderboardPage]); + const resolveDomain = async (connection: Connection, user: PublicKey) => { try { const { reverse } = await getFavoriteDomain(connection, user); @@ -53,9 +95,47 @@ const Points: FC = () => { } }, [connection, walletAddress]); + // fetch new page when page counter changed + useEffect(() => { + fetchLeaderboardPage(); + }, [leaderboardPage]); + useEffect(() => { - fetchLeaderboardData(connection).then(setLeaderboardData); // TODO: cache leaderboard and avoid call - }, [connection, connected, walletAddress]); // Dependency array to re-fetch when these variables change + // fetch initial page and overwrite skeleton rows + if (leaderboardPage === 0) { + fetchLeaderboardData({ + connection, + }).then((data) => { + setLeaderboardData([...data]); + }); + } + + // intersection observer to fetch new page of data + // when sentinel element is scrolled into view + const observer = new IntersectionObserver( + (entries) => { + console.log("here", entries); + if (entries[0].isIntersecting && !isFetchingLeaderboardPage) { + setLeaderboardPage((current) => current + 1); + } + }, + { + root: null, + rootMargin: "0px", + threshold: 0, + } + ); + + if (leaderboardSentinelRef.current) { + observer.observe(leaderboardSentinelRef.current); + } + + return () => { + if (leaderboardSentinelRef.current) { + observer.unobserve(leaderboardSentinelRef.current); + } + }; + }, [connection, fetchLeaderboardPage]); return ( <> @@ -130,6 +210,7 @@ const Points: FC = () => { +
); diff --git a/packages/marginfi-v2-ui-state/src/lib/points.ts b/packages/marginfi-v2-ui-state/src/lib/points.ts index 2898bd2551..e7c8e7a200 100644 --- a/packages/marginfi-v2-ui-state/src/lib/points.ts +++ b/packages/marginfi-v2-ui-state/src/lib/points.ts @@ -11,6 +11,7 @@ import { getDoc, getCountFromServer, where, + QueryDocumentSnapshot, } from "firebase/firestore"; import { FavouriteDomain, NAME_OFFERS_ID, reverseLookupBatch } from "@bonfida/spl-name-service"; import { Connection, PublicKey } from "@solana/web3.js"; @@ -18,6 +19,7 @@ import { firebaseApi } from "."; type LeaderboardRow = { id: string; + doc: QueryDocumentSnapshot; total_activity_deposit_points: number; total_activity_borrow_points: number; total_referral_deposit_points: number; @@ -27,38 +29,33 @@ type LeaderboardRow = { socialPoints: number; }; -async function fetchLeaderboardData(connection?: Connection, rowCap = 100, pageSize = 50): Promise { +async function fetchLeaderboardData({ + connection, + queryCursor, + pageSize = 50, +}: { + connection?: Connection; + queryCursor?: QueryDocumentSnapshot; + pageSize?: number; +}): Promise { const pointsCollection = collection(firebaseApi.db, "points"); - const leaderboardMap = new Map(); - let initialQueryCursor = null; - do { - let pointsQuery: Query; - if (initialQueryCursor) { - pointsQuery = query( - pointsCollection, - orderBy("total_points", "desc"), - startAfter(initialQueryCursor), - limit(pageSize) - ); - } else { - pointsQuery = query(pointsCollection, orderBy("total_points", "desc"), limit(pageSize)); - } - - const querySnapshot = await getDocs(pointsQuery); - const leaderboardSlice = querySnapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })); - const leaderboardSliceFiltered = leaderboardSlice.filter( - (item) => item.id !== null && item.id !== undefined && item.id != "None" - ); - - for (const row of leaderboardSliceFiltered) { - leaderboardMap.set(row.id, row); - } + const pointsQuery: Query = query( + pointsCollection, + orderBy("total_points", "desc"), + ...(queryCursor ? [startAfter(queryCursor)] : []), + limit(pageSize) + ); - initialQueryCursor = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null; - } while (initialQueryCursor !== null && leaderboardMap.size < rowCap); + const querySnapshot = await getDocs(pointsQuery); + const leaderboardSlice = querySnapshot.docs + .filter((item) => item.id !== null && item.id !== undefined && item.id != "None") + .map((doc) => { + const data = { id: doc.id, doc, ...doc.data() } as LeaderboardRow; + return data; + }); - const leaderboardFinalSlice = [...leaderboardMap.values()].slice(0, 100); + const leaderboardFinalSlice: LeaderboardRow[] = [...leaderboardSlice]; if (connection) { const publicKeys = leaderboardFinalSlice.map((value) => { @@ -72,6 +69,7 @@ async function fetchLeaderboardData(connection?: Connection, rowCap = 100, pageS leaderboardFinalSlice.map((value, idx) => (value.id = reverseLookup[idx] ? `${reverseLookup[idx]}.sol` : value.id)); } + return leaderboardFinalSlice; } From ace09d7e7a38c95727b65175c233a6cca6b4e224 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Thu, 12 Oct 2023 13:56:52 -0400 Subject: [PATCH 02/14] chore: install clsx --- apps/marginfi-v2-ui/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/marginfi-v2-ui/package.json b/apps/marginfi-v2-ui/package.json index 799873397b..0ec8d73ab7 100644 --- a/apps/marginfi-v2-ui/package.json +++ b/apps/marginfi-v2-ui/package.json @@ -41,6 +41,7 @@ "bignumber.js": "^9.1.1", "bn.js": "^5.2.1", "bs58": "^5.0.0", + "clsx": "^2.0.0", "firebase": "^9.22.1", "firebase-admin": "^11.9.0", "jsbi": "^4.3.0", From 18c4962a9e678fcfc6de18a08d961f523e0c934a Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Thu, 12 Oct 2023 13:57:04 -0400 Subject: [PATCH 03/14] feat: alternating leaderboard table row colors with mono font for numbers --- .../desktop/Points/PointsLeaderBoard.tsx | 36 +++++++++++++------ apps/marginfi-v2-ui/src/pages/points.tsx | 1 - apps/marginfi-v2-ui/tailwind.config.js | 10 ++++++ 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index 49b267a2e2..a3a6259009 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -1,6 +1,16 @@ import React, { FC } from "react"; -import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Skeleton } from "@mui/material"; - +import clsx from "clsx"; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Skeleton, + Typography, +} from "@mui/material"; import { groupedNumberFormatterDyn } from "@mrgnlabs/mrgn-common"; import { LeaderboardRow } from "@mrgnlabs/marginfi-v2-ui-state"; @@ -14,10 +24,10 @@ export const PointsLeaderBoard: FC = ({ leaderboardData, - + Rank @@ -79,7 +89,13 @@ export const PointsLeaderBoard: FC = ({ leaderboardData, const data = row as LeaderboardRow; return ( - + 0 ? "bg-zinc-800/50" : "bg-none", + `${data.id === currentUserId ? "glow" : ""}` + )} + > = ({ leaderboardData, = ({ leaderboardData, = ({ leaderboardData, = ({ leaderboardData, = ({ leaderboardData, { // when sentinel element is scrolled into view const observer = new IntersectionObserver( (entries) => { - console.log("here", entries); if (entries[0].isIntersecting && !isFetchingLeaderboardPage) { setLeaderboardPage((current) => current + 1); } diff --git a/apps/marginfi-v2-ui/tailwind.config.js b/apps/marginfi-v2-ui/tailwind.config.js index 3f0c40a196..18f254e0e0 100644 --- a/apps/marginfi-v2-ui/tailwind.config.js +++ b/apps/marginfi-v2-ui/tailwind.config.js @@ -35,6 +35,16 @@ module.exports = { }, fontFamily: { aeonik: ['"Aeonik Pro"'], + mono: [ + "ui-monospace", + "SFMono-Regular", + "Menlo", + "Monaco", + "Consolas", + "Liberation Mono", + "Courier New", + "monospace", + ], }, screens: { sm: "640px", From c37e314dd433098ac41b3ae92ce26cff152de0c8 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Thu, 12 Oct 2023 14:04:43 -0400 Subject: [PATCH 04/14] fix: move address truncation to fetchLeaderboardData method to avoid truncating .sol domains / fix chartreuse hover color --- .../components/desktop/Points/PointsLeaderBoard.tsx | 11 +++-------- packages/marginfi-v2-ui-state/src/lib/points.ts | 7 ++++++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index a3a6259009..925e588b7b 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -21,7 +21,7 @@ interface PointsLeaderBoardProps { export const PointsLeaderBoard: FC = ({ leaderboardData, currentUserId }) => { return ( - +
@@ -115,14 +115,9 @@ export const PointsLeaderBoard: FC = ({ leaderboardData, target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "inherit" }} - className="hover:text-[#7fff00]" + className="hover:text-[#DCE85D]" > - {`${data.id.slice(0, 5)}...${data.id.slice(-5)}`} - + {data.id} (value.id = reverseLookup[idx] ? `${reverseLookup[idx]}.sol` : value.id)); + leaderboardFinalSlice.map( + (value, idx) => + (value.id = reverseLookup[idx] + ? `${reverseLookup[idx]}.sol` + : `${value.id.slice(0, 5)}...${value.id.slice(-5)}`) + ); } return leaderboardFinalSlice; From c9a82a901192538858ca0904cf6fb7a63c29a654 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Thu, 12 Oct 2023 15:14:17 -0400 Subject: [PATCH 05/14] chore: move leaderboard data fetching inside leaderboard component, add total user count fetch --- .../desktop/Points/PointsLeaderBoard.tsx | 419 +++++++++++------- apps/marginfi-v2-ui/src/pages/points.tsx | 90 +--- .../marginfi-v2-ui-state/src/lib/points.ts | 20 +- 3 files changed, 284 insertions(+), 245 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index 925e588b7b..0a0c456e89 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -1,4 +1,4 @@ -import React, { FC } from "react"; +import React, { FC, useEffect, useState, useCallback, useRef, useMemo } from "react"; import clsx from "clsx"; import { Paper, @@ -12,170 +12,277 @@ import { Typography, } from "@mui/material"; import { groupedNumberFormatterDyn } from "@mrgnlabs/mrgn-common"; -import { LeaderboardRow } from "@mrgnlabs/marginfi-v2-ui-state"; +import { LeaderboardRow, fetchLeaderboardData, fetchTotalUserCount } from "@mrgnlabs/marginfi-v2-ui-state"; +import { useConnection } from "@solana/wallet-adapter-react"; interface PointsLeaderBoardProps { - leaderboardData: LeaderboardRow[] | {}[]; currentUserId?: string; } -export const PointsLeaderBoard: FC = ({ leaderboardData, currentUserId }) => { - return ( - -
- - - - Rank - - - User - - - Lending Points - - - Borrowing Points - - - Referral Points - - - Social Points - - - Total Points - - - - - {leaderboardData.map((row: LeaderboardRow | {}, index: number) => { - if (!row.hasOwnProperty("id")) { - return ( - - {[...new Array(7)].map((_, index) => ( - - - - ))} - - ); - } +export const PointsLeaderBoard: FC = ({ currentUserId }) => { + const { connection } = useConnection(); + const leaderboardPerPage = 50; + const [leaderboardData, setLeaderboardData] = useState([ + ...new Array(leaderboardPerPage).fill({}), + ]); + const [leaderboardPage, setLeaderboardPage] = useState(0); + const [totalUserCount, setTotalUserCount] = useState(0); + const [isFetchingLeaderboardPage, setIsFetchingLeaderboardPage] = useState(false); + const leaderboardSentinelRef = useRef(null); + + const getTotalUserCount = useCallback(async () => { + const totalUserCount = await fetchTotalUserCount(); + setTotalUserCount(totalUserCount); + }, [setTotalUserCount, fetchTotalUserCount]); + + // fetch next page of leaderboard results + const fetchLeaderboardPage = useCallback(async () => { + // grab last row of current leaderboard data for cursor + const lastRow = [...leaderboardData].filter((row) => row.hasOwnProperty("id"))[ + leaderboardPage * leaderboardPerPage - 2 + ] as LeaderboardRow; + if (!lastRow || !lastRow.hasOwnProperty("id")) return; + setIsFetchingLeaderboardPage(true); + + // fetch new page of data with cursor + const queryCursor = leaderboardData.length > 0 ? lastRow.doc : undefined; + setLeaderboardData((current) => [...current, ...new Array(50).fill({})]); + fetchLeaderboardData({ + connection, + queryCursor, + }).then((data) => { + // filter out skeleton rows + const filtered = [...leaderboardData].filter((row) => row.hasOwnProperty("id")); + + // additional check for duplicate values + const uniqueData = data.reduce((acc, curr) => { + const isDuplicate = acc.some((item) => { + const data = item as LeaderboardRow; + data.id === curr.id; + }); + if (!isDuplicate) { + acc.push(curr); + } + return acc; + }, filtered); + + setLeaderboardData(uniqueData); + setIsFetchingLeaderboardPage(false); + }); + }, [connection, leaderboardData, setLeaderboardData, setIsFetchingLeaderboardPage, leaderboardPage]); + + // fetch new page when page counter changed + useEffect(() => { + fetchLeaderboardPage(); + }, [leaderboardPage]); - const data = row as LeaderboardRow; + useEffect(() => { + // fetch initial page and overwrite skeleton rows + if (leaderboardPage === 0) { + fetchLeaderboardData({ + connection, + }).then((data) => { + setLeaderboardData([...data]); + }); + } - return ( - 0 ? "bg-zinc-800/50" : "bg-none", - `${data.id === currentUserId ? "glow" : ""}` - )} + // intersection observer to fetch new page of data + // when sentinel element is scrolled into view + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && !isFetchingLeaderboardPage) { + setLeaderboardPage((current) => current + 1); + } + }, + { + root: null, + rootMargin: "0px", + threshold: 0, + } + ); + + if (leaderboardSentinelRef.current) { + observer.observe(leaderboardSentinelRef.current); + } + + return () => { + if (leaderboardSentinelRef.current) { + observer.unobserve(leaderboardSentinelRef.current); + } + }; + }, [connection, fetchLeaderboardPage]); + + useEffect(() => { + getTotalUserCount(); + }, []); + + return ( + <> +

Total users: {totalUserCount}

+ +
+ + + - - {index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : index + 1} - - - - {data.id} - - - - {groupedNumberFormatterDyn.format(Math.round(data.total_activity_deposit_points))} - - - {groupedNumberFormatterDyn.format(Math.round(data.total_activity_borrow_points))} - - - {groupedNumberFormatterDyn.format( - Math.round(data.total_referral_deposit_points + data.total_referral_borrow_points) + Rank + + + User + + + Lending Points + + + Borrowing Points + + + Referral Points + + + Social Points + + + Total Points + + + + + {leaderboardData.map((row: LeaderboardRow | {}, index: number) => { + if (!row.hasOwnProperty("id")) { + return ( + + {[...new Array(7)].map((_, index) => ( + + + + ))} + + ); + } + + const data = row as LeaderboardRow; + + return ( + 0 ? "bg-zinc-800/50" : "bg-none", + `${data.id === currentUserId ? "glow" : ""}` )} - - - {groupedNumberFormatterDyn.format(Math.round(data.socialPoints ? data.socialPoints : 0))} - - - {groupedNumberFormatterDyn.format( - Math.round( - data.total_deposit_points + data.total_borrow_points + (data.socialPoints ? data.socialPoints : 0) - ) - )} - - - ); - })} - -
-
+ + {index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : index + 1} + {/* {userPointsData.totalUserCount - index} */} + + + + {data.id} + + + + {groupedNumberFormatterDyn.format(Math.round(data.total_activity_deposit_points))} + + + {groupedNumberFormatterDyn.format(Math.round(data.total_activity_borrow_points))} + + + {groupedNumberFormatterDyn.format( + Math.round(data.total_referral_deposit_points + data.total_referral_borrow_points) + )} + + + {groupedNumberFormatterDyn.format(Math.round(data.socialPoints ? data.socialPoints : 0))} + + + {groupedNumberFormatterDyn.format( + Math.round( + data.total_deposit_points + + data.total_borrow_points + + (data.socialPoints ? data.socialPoints : 0) + ) + )} + + + ); + })} + + + +
+ ); }; diff --git a/apps/marginfi-v2-ui/src/pages/points.tsx b/apps/marginfi-v2-ui/src/pages/points.tsx index 07f3945f1f..c7e2eed5d5 100644 --- a/apps/marginfi-v2-ui/src/pages/points.tsx +++ b/apps/marginfi-v2-ui/src/pages/points.tsx @@ -6,7 +6,7 @@ import CheckIcon from "@mui/icons-material/Check"; import { useRouter } from "next/router"; import { getFavoriteDomain } from "@bonfida/spl-name-service"; import { Connection, PublicKey } from "@solana/web3.js"; -import { LeaderboardRow, fetchLeaderboardData } from "@mrgnlabs/marginfi-v2-ui-state"; +import { useConnection } from "@solana/wallet-adapter-react"; import { CopyToClipboard } from "react-copy-to-clipboard"; import { useUserProfileStore } from "~/store"; import { useWalletContext } from "~/hooks/useWalletContext"; @@ -19,7 +19,6 @@ import { PointsCheckingUser, PointsConnectWallet, } from "~/components/desktop/Points"; -import { useConnection } from "@solana/wallet-adapter-react"; const Points: FC = () => { const { connected, walletAddress } = useWalletContext(); @@ -31,55 +30,12 @@ const Points: FC = () => { state.userPointsData, ]); - const leaderboardPerPage = 50; - const [leaderboardData, setLeaderboardData] = useState([ - ...new Array(leaderboardPerPage).fill({}), - ]); - const [leaderboardPage, setLeaderboardPage] = useState(0); - const [isFetchingLeaderboardPage, setIsFetchingLeaderboardPage] = useState(false); - const leaderboardSentinelRef = useRef(null); const [domain, setDomain] = useState(); const currentUserId = useMemo(() => domain ?? currentFirebaseUser?.uid, [currentFirebaseUser, domain]); const referralCode = useMemo(() => routerQuery.referralCode as string | undefined, [routerQuery.referralCode]); const [isReferralCopied, setIsReferralCopied] = useState(false); - // fetch next page of leaderboard results - const fetchLeaderboardPage = useCallback(async () => { - // grab last row of current leaderboard data for cursor - const lastRow = [...leaderboardData].filter((row) => row.hasOwnProperty("id"))[ - leaderboardPage * leaderboardPerPage - 2 - ] as LeaderboardRow; - if (!lastRow || !lastRow.hasOwnProperty("id")) return; - setIsFetchingLeaderboardPage(true); - - // fetch new page of data with cursor - const queryCursor = leaderboardData.length > 0 ? lastRow.doc : undefined; - setLeaderboardData((current) => [...current, ...new Array(50).fill({})]); - fetchLeaderboardData({ - connection, - queryCursor, - }).then((data) => { - // filter out skeleton rows - const filtered = [...leaderboardData].filter((row) => row.hasOwnProperty("id")); - - // additional check for duplicate values - const uniqueData = data.reduce((acc, curr) => { - const isDuplicate = acc.some((item) => { - const data = item as LeaderboardRow; - data.id === curr.id; - }); - if (!isDuplicate) { - acc.push(curr); - } - return acc; - }, filtered); - - setLeaderboardData(uniqueData); - setIsFetchingLeaderboardPage(false); - }); - }, [connection, leaderboardData, setLeaderboardData, setIsFetchingLeaderboardPage, leaderboardPage]); - const resolveDomain = async (connection: Connection, user: PublicKey) => { try { const { reverse } = await getFavoriteDomain(connection, user); @@ -95,47 +51,6 @@ const Points: FC = () => { } }, [connection, walletAddress]); - // fetch new page when page counter changed - useEffect(() => { - fetchLeaderboardPage(); - }, [leaderboardPage]); - - useEffect(() => { - // fetch initial page and overwrite skeleton rows - if (leaderboardPage === 0) { - fetchLeaderboardData({ - connection, - }).then((data) => { - setLeaderboardData([...data]); - }); - } - - // intersection observer to fetch new page of data - // when sentinel element is scrolled into view - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && !isFetchingLeaderboardPage) { - setLeaderboardPage((current) => current + 1); - } - }, - { - root: null, - rootMargin: "0px", - threshold: 0, - } - ); - - if (leaderboardSentinelRef.current) { - observer.observe(leaderboardSentinelRef.current); - } - - return () => { - if (leaderboardSentinelRef.current) { - observer.unobserve(leaderboardSentinelRef.current); - } - }; - }, [connection, fetchLeaderboardPage]); - return ( <> points @@ -208,8 +123,7 @@ const Points: FC = () => {
- -
+
); diff --git a/packages/marginfi-v2-ui-state/src/lib/points.ts b/packages/marginfi-v2-ui-state/src/lib/points.ts index 4fb25572b2..7b097a0213 100644 --- a/packages/marginfi-v2-ui-state/src/lib/points.ts +++ b/packages/marginfi-v2-ui-state/src/lib/points.ts @@ -44,6 +44,7 @@ async function fetchLeaderboardData({ pointsCollection, orderBy("total_points", "desc"), ...(queryCursor ? [startAfter(queryCursor)] : []), + where("total_points", ">=", 1), limit(pageSize) ); @@ -101,6 +102,16 @@ async function fetchUserRank(userPoints: number): Promise { return allGreaterDocsCount - nullGreaterDocsCount; } +async function fetchTotalUserCount() { + const q1 = query(collection(firebaseApi.db, "points")); + const q2 = query(collection(firebaseApi.db, "points"), where("owner", "==", null)); + const q3 = query(collection(firebaseApi.db, "points"), where("total_points", "<", 1)); + const q1Count = await getCountFromServer(q1); + const q2Count = await getCountFromServer(q2); + const q3Count = await getCountFromServer(q3); + return q1Count.data().count - q2Count.data().count - q3Count.data().count; +} + interface UserPointsData { owner: string; depositPoints: number; @@ -186,6 +197,13 @@ async function getPointsSummary() { return pointSummary; } -export { fetchLeaderboardData, fetchUserRank, getPointsSummary, getPointsDataForUser, DEFAULT_USER_POINTS_DATA }; +export { + fetchLeaderboardData, + fetchUserRank, + fetchTotalUserCount, + getPointsSummary, + getPointsDataForUser, + DEFAULT_USER_POINTS_DATA, +}; export type { LeaderboardRow, UserPointsData }; From 55f22a337067f96a704789fe0128b7ccf04bdc6a Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Thu, 12 Oct 2023 18:46:41 -0400 Subject: [PATCH 06/14] chore: refactor leaderboard to use single state obj --- .../desktop/Points/PointsLeaderBoard.tsx | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index 0a0c456e89..6e1a5c5bf6 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -21,28 +21,38 @@ interface PointsLeaderBoardProps { export const PointsLeaderBoard: FC = ({ currentUserId }) => { const { connection } = useConnection(); - const leaderboardPerPage = 50; + const [leaderboardSettings, setLeaderboardSettings] = useState({ + orderCol: "total_points", + orderDir: "desc", + totalUserCount: 0, + perPage: 50, + currentPage: 0, + isFetchingLeaderboardPage: false, + }); const [leaderboardData, setLeaderboardData] = useState([ - ...new Array(leaderboardPerPage).fill({}), + ...new Array(leaderboardSettings.perPage).fill({}), ]); - const [leaderboardPage, setLeaderboardPage] = useState(0); - const [totalUserCount, setTotalUserCount] = useState(0); - const [isFetchingLeaderboardPage, setIsFetchingLeaderboardPage] = useState(false); const leaderboardSentinelRef = useRef(null); const getTotalUserCount = useCallback(async () => { const totalUserCount = await fetchTotalUserCount(); - setTotalUserCount(totalUserCount); - }, [setTotalUserCount, fetchTotalUserCount]); + setLeaderboardSettings({ + ...leaderboardSettings, + totalUserCount, + }); + }, [setLeaderboardSettings, fetchTotalUserCount]); // fetch next page of leaderboard results const fetchLeaderboardPage = useCallback(async () => { // grab last row of current leaderboard data for cursor const lastRow = [...leaderboardData].filter((row) => row.hasOwnProperty("id"))[ - leaderboardPage * leaderboardPerPage - 2 + leaderboardSettings.currentPage * leaderboardSettings.perPage - 2 ] as LeaderboardRow; if (!lastRow || !lastRow.hasOwnProperty("id")) return; - setIsFetchingLeaderboardPage(true); + setLeaderboardSettings({ + ...leaderboardSettings, + isFetchingLeaderboardPage: true, + }); // fetch new page of data with cursor const queryCursor = leaderboardData.length > 0 ? lastRow.doc : undefined; @@ -67,18 +77,21 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) }, filtered); setLeaderboardData(uniqueData); - setIsFetchingLeaderboardPage(false); + setLeaderboardSettings({ + ...leaderboardSettings, + isFetchingLeaderboardPage: false, + }); }); - }, [connection, leaderboardData, setLeaderboardData, setIsFetchingLeaderboardPage, leaderboardPage]); + }, [connection, leaderboardData, setLeaderboardData, leaderboardSettings, setLeaderboardSettings]); // fetch new page when page counter changed useEffect(() => { fetchLeaderboardPage(); - }, [leaderboardPage]); + }, [leaderboardSettings.currentPage]); useEffect(() => { // fetch initial page and overwrite skeleton rows - if (leaderboardPage === 0) { + if (leaderboardSettings.currentPage === 0) { fetchLeaderboardData({ connection, }).then((data) => { @@ -90,8 +103,11 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) // when sentinel element is scrolled into view const observer = new IntersectionObserver( (entries) => { - if (entries[0].isIntersecting && !isFetchingLeaderboardPage) { - setLeaderboardPage((current) => current + 1); + if (entries[0].isIntersecting && !leaderboardSettings.isFetchingLeaderboardPage) { + setLeaderboardSettings({ + ...leaderboardSettings, + currentPage: leaderboardSettings.currentPage + 1, + }); } }, { @@ -118,7 +134,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) return ( <> -

Total users: {totalUserCount}

+

Total users: {leaderboardSettings.totalUserCount}

= ({ currentUserId }) { + console.log("Sort by rank"); + }} > Rank From 809f3b2d6c770c6a80b65df4ca98342544c1cc78 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Thu, 12 Oct 2023 19:58:40 -0400 Subject: [PATCH 07/14] feat: leaderboard column sorting WIP --- .../desktop/Points/PointsLeaderBoard.tsx | 70 +++++++++++++++---- .../marginfi-v2-ui-state/src/lib/points.ts | 6 +- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index 6e1a5c5bf6..4ae39188b5 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -21,13 +21,22 @@ interface PointsLeaderBoardProps { export const PointsLeaderBoard: FC = ({ currentUserId }) => { const { connection } = useConnection(); - const [leaderboardSettings, setLeaderboardSettings] = useState({ + const [leaderboardSettings, setLeaderboardSettings] = useState<{ + orderCol: string; + orderDir: "asc" | "desc"; + totalUserCount: number; + perPage: number; + currentPage: number; + isFetchingLeaderboardPage: boolean; + initialLoad: boolean; + }>({ orderCol: "total_points", orderDir: "desc", totalUserCount: 0, perPage: 50, currentPage: 0, isFetchingLeaderboardPage: false, + initialLoad: true, }); const [leaderboardData, setLeaderboardData] = useState([ ...new Array(leaderboardSettings.perPage).fill({}), @@ -46,7 +55,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) const fetchLeaderboardPage = useCallback(async () => { // grab last row of current leaderboard data for cursor const lastRow = [...leaderboardData].filter((row) => row.hasOwnProperty("id"))[ - leaderboardSettings.currentPage * leaderboardSettings.perPage - 2 + leaderboardSettings.currentPage * leaderboardSettings.perPage - (leaderboardSettings.orderDir === "asc" ? 1 : 2) ] as LeaderboardRow; if (!lastRow || !lastRow.hasOwnProperty("id")) return; setLeaderboardSettings({ @@ -60,6 +69,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) fetchLeaderboardData({ connection, queryCursor, + orderDir: leaderboardSettings.orderDir, }).then((data) => { // filter out skeleton rows const filtered = [...leaderboardData].filter((row) => row.hasOwnProperty("id")); @@ -82,7 +92,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) isFetchingLeaderboardPage: false, }); }); - }, [connection, leaderboardData, setLeaderboardData, leaderboardSettings, setLeaderboardSettings]); + }, [setLeaderboardData, leaderboardSettings, setLeaderboardSettings]); // fetch new page when page counter changed useEffect(() => { @@ -91,9 +101,15 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) useEffect(() => { // fetch initial page and overwrite skeleton rows - if (leaderboardSettings.currentPage === 0) { + if (leaderboardSettings.currentPage === 0 && leaderboardSettings.initialLoad) { + setLeaderboardSettings({ + ...leaderboardSettings, + initialLoad: false, + }); fetchLeaderboardData({ connection, + orderDir: leaderboardSettings.orderDir, + orderCol: leaderboardSettings.orderCol, }).then((data) => { setLeaderboardData([...data]); }); @@ -106,6 +122,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) if (entries[0].isIntersecting && !leaderboardSettings.isFetchingLeaderboardPage) { setLeaderboardSettings({ ...leaderboardSettings, + isFetchingLeaderboardPage: true, currentPage: leaderboardSettings.currentPage + 1, }); } @@ -126,7 +143,15 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) observer.unobserve(leaderboardSentinelRef.current); } }; - }, [connection, fetchLeaderboardPage]); + }, [ + connection, + fetchLeaderboardPage, + setLeaderboardSettings, + leaderboardSettings.isFetchingLeaderboardPage, + leaderboardSettings.currentPage, + leaderboardSettings.initialLoad, + setLeaderboardSettings, + ]); useEffect(() => { getTotalUserCount(); @@ -147,7 +172,14 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) className="text-white text-base font-aeonik font-bold border-none text-center cursor-pointer" style={{ fontWeight: 500 }} onClick={() => { - console.log("Sort by rank"); + setLeaderboardData([...new Array(leaderboardSettings.perPage).fill({})]); + setLeaderboardSettings({ + ...leaderboardSettings, + orderCol: "total_points", + orderDir: leaderboardSettings.orderDir === "asc" ? "desc" : "asc", + currentPage: 0, + initialLoad: true, + }); }} > Rank @@ -218,13 +250,27 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) > 2 && + "text-base", + leaderboardSettings.orderCol !== "total_points" && "text-base", data.id === currentUserId ? "text-[#DCE85D]" : "text-white" - }`} - // className={`border-none font-aeonik ${data.id === currentUserId ? "text-[#DCE85D]" : "text-white"}`} + )} > - {index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : index + 1} - {/* {userPointsData.totalUserCount - index} */} + {leaderboardSettings.orderCol === "total_points" && leaderboardSettings.orderDir === "desc" && ( + <>{index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : index + 1} + )} + {leaderboardSettings.orderCol === "total_points" && leaderboardSettings.orderDir === "asc" && ( + <>{leaderboardSettings.totalUserCount - index} + )} + {leaderboardSettings.orderCol !== "total_points" && <>{index}} = ({ currentUserId }) -
+
); }; diff --git a/packages/marginfi-v2-ui-state/src/lib/points.ts b/packages/marginfi-v2-ui-state/src/lib/points.ts index 7b097a0213..671dfe1076 100644 --- a/packages/marginfi-v2-ui-state/src/lib/points.ts +++ b/packages/marginfi-v2-ui-state/src/lib/points.ts @@ -33,16 +33,20 @@ async function fetchLeaderboardData({ connection, queryCursor, pageSize = 50, + orderCol = "total_points", + orderDir = "desc", }: { connection?: Connection; queryCursor?: QueryDocumentSnapshot; pageSize?: number; + orderCol?: string; + orderDir?: "desc" | "asc"; }): Promise { const pointsCollection = collection(firebaseApi.db, "points"); const pointsQuery: Query = query( pointsCollection, - orderBy("total_points", "desc"), + orderBy(orderCol, orderDir), ...(queryCursor ? [startAfter(queryCursor)] : []), where("total_points", ">=", 1), limit(pageSize) From 21e90a1d65e60fce019b61900f9af8220d408314 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Thu, 12 Oct 2023 20:06:01 -0400 Subject: [PATCH 08/14] chore: fix linting error --- .../src/components/desktop/Points/PointsLeaderBoard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index 4ae39188b5..2b3765ca2b 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -230,7 +230,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) return ( {[...new Array(7)].map((_, index) => ( - + ))} From 2fbb827e23b4f628984e95d2da3ae0d8ee938ee8 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 13 Oct 2023 11:28:33 -0400 Subject: [PATCH 09/14] feat: sorting across all applicable leaderbaord table columns --- .../desktop/Points/PointsLeaderBoard.tsx | 165 +++++++++++------- .../marginfi-v2-ui-state/src/lib/points.ts | 5 +- 2 files changed, 104 insertions(+), 66 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index 2b3765ca2b..ed199b8b62 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -1,20 +1,26 @@ import React, { FC, useEffect, useState, useCallback, useRef, useMemo } from "react"; import clsx from "clsx"; -import { - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Skeleton, - Typography, -} from "@mui/material"; +import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Skeleton } from "@mui/material"; +import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp"; +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import { groupedNumberFormatterDyn } from "@mrgnlabs/mrgn-common"; import { LeaderboardRow, fetchLeaderboardData, fetchTotalUserCount } from "@mrgnlabs/marginfi-v2-ui-state"; import { useConnection } from "@solana/wallet-adapter-react"; +const SortIcon = ({ orderDir }: { orderDir: "asc" | "desc" }) => { + return ( +
+ + + + + + + +
+ ); +}; + interface PointsLeaderBoardProps { currentUserId?: string; } @@ -43,6 +49,22 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) ]); const leaderboardSentinelRef = useRef(null); + const setOrderCol = useCallback( + (col: string) => { + setLeaderboardData([...new Array(leaderboardSettings.perPage).fill({})]); + const direction = + col === leaderboardSettings.orderCol ? (leaderboardSettings.orderDir === "asc" ? "desc" : "asc") : "desc"; + setLeaderboardSettings({ + ...leaderboardSettings, + orderCol: col, + orderDir: direction, + currentPage: 0, + initialLoad: true, + }); + }, + [setLeaderboardSettings, leaderboardSettings] + ); + const getTotalUserCount = useCallback(async () => { const totalUserCount = await fetchTotalUserCount(); setLeaderboardSettings({ @@ -69,6 +91,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) fetchLeaderboardData({ connection, queryCursor, + orderCol: leaderboardSettings.orderCol, orderDir: leaderboardSettings.orderDir, }).then((data) => { // filter out skeleton rows @@ -169,56 +192,66 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) { - setLeaderboardData([...new Array(leaderboardSettings.perPage).fill({})]); - setLeaderboardSettings({ - ...leaderboardSettings, - orderCol: "total_points", - orderDir: leaderboardSettings.orderDir === "asc" ? "desc" : "asc", - currentPage: 0, - initialLoad: true, - }); - }} + className={clsx( + "text-white text-base font-aeonik border-none cursor-pointer", + leaderboardSettings.orderCol === "total_points" && "flex items-center justify-center gap-3" + )} + onClick={() => setOrderCol("total_points")} > Rank + {leaderboardSettings.orderCol === "total_points" && ( + + )} - - User - + User setOrderCol("total_activity_deposit_points")} > Lending Points + {leaderboardSettings.orderCol === "total_activity_deposit_points" && ( + + )} setOrderCol("total_activity_borrow_points")} > Borrowing Points + {leaderboardSettings.orderCol === "total_activity_borrow_points" && ( + + )} - + Referral Points setOrderCol("socialPoints")} > Social Points + {leaderboardSettings.orderCol === "socialPoints" && ( + + )} setOrderCol("total_points")} > Total Points @@ -230,7 +263,15 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) return ( {[...new Array(7)].map((_, index) => ( - + 1 && "w-[15%]" + )} + key={index} + > ))} @@ -251,7 +292,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) = ({ currentUserId }) {leaderboardSettings.orderCol === "total_points" && leaderboardSettings.orderDir === "desc" && ( <>{index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : index + 1} )} - {leaderboardSettings.orderCol === "total_points" && leaderboardSettings.orderDir === "asc" && ( - <>{leaderboardSettings.totalUserCount - index} + {leaderboardSettings.orderCol !== "total_points" && leaderboardSettings.orderDir === "desc" && ( + <>{index + 1} )} - {leaderboardSettings.orderCol !== "total_points" && <>{index}} + {leaderboardSettings.orderDir === "asc" && <>{leaderboardSettings.totalUserCount - (index + 1)}} = ({ currentUserId }) {groupedNumberFormatterDyn.format(Math.round(data.total_activity_deposit_points))} {groupedNumberFormatterDyn.format(Math.round(data.total_activity_borrow_points))} {groupedNumberFormatterDyn.format( Math.round(data.total_referral_deposit_points + data.total_referral_borrow_points) @@ -319,19 +360,19 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) {groupedNumberFormatterDyn.format(Math.round(data.socialPoints ? data.socialPoints : 0))} {groupedNumberFormatterDyn.format( Math.round( diff --git a/packages/marginfi-v2-ui-state/src/lib/points.ts b/packages/marginfi-v2-ui-state/src/lib/points.ts index 671dfe1076..d18d66b2fd 100644 --- a/packages/marginfi-v2-ui-state/src/lib/points.ts +++ b/packages/marginfi-v2-ui-state/src/lib/points.ts @@ -48,7 +48,6 @@ async function fetchLeaderboardData({ pointsCollection, orderBy(orderCol, orderDir), ...(queryCursor ? [startAfter(queryCursor)] : []), - where("total_points", ">=", 1), limit(pageSize) ); @@ -109,11 +108,9 @@ async function fetchUserRank(userPoints: number): Promise { async function fetchTotalUserCount() { const q1 = query(collection(firebaseApi.db, "points")); const q2 = query(collection(firebaseApi.db, "points"), where("owner", "==", null)); - const q3 = query(collection(firebaseApi.db, "points"), where("total_points", "<", 1)); const q1Count = await getCountFromServer(q1); const q2Count = await getCountFromServer(q2); - const q3Count = await getCountFromServer(q3); - return q1Count.data().count - q2Count.data().count - q3Count.data().count; + return q1Count.data().count - q2Count.data().count; } interface UserPointsData { From 3e1c3552350e729c518feab940beac8b1501bac3 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 13 Oct 2023 11:34:38 -0400 Subject: [PATCH 10/14] fix: logic to fetch last row for firebase query cursor --- .../src/components/desktop/Points/PointsLeaderBoard.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index ed199b8b62..d5e736eafa 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -76,9 +76,8 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) // fetch next page of leaderboard results const fetchLeaderboardPage = useCallback(async () => { // grab last row of current leaderboard data for cursor - const lastRow = [...leaderboardData].filter((row) => row.hasOwnProperty("id"))[ - leaderboardSettings.currentPage * leaderboardSettings.perPage - (leaderboardSettings.orderDir === "asc" ? 1 : 2) - ] as LeaderboardRow; + const filtered = [...leaderboardData].filter((row) => row.hasOwnProperty("id")); + const lastRow = filtered[filtered.length - 1] as LeaderboardRow; if (!lastRow || !lastRow.hasOwnProperty("id")) return; setLeaderboardSettings({ ...leaderboardSettings, From c651a8a6fac1df18e29ade5ecb5039759b026772 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 13 Oct 2023 14:47:08 -0400 Subject: [PATCH 11/14] feat: fetch .sol domains (favorite > first > pub key) --- .../marginfi-v2-ui-state/src/lib/points.ts | 70 ++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/packages/marginfi-v2-ui-state/src/lib/points.ts b/packages/marginfi-v2-ui-state/src/lib/points.ts index d18d66b2fd..fa9764aa58 100644 --- a/packages/marginfi-v2-ui-state/src/lib/points.ts +++ b/packages/marginfi-v2-ui-state/src/lib/points.ts @@ -13,7 +13,14 @@ import { where, QueryDocumentSnapshot, } from "firebase/firestore"; -import { FavouriteDomain, NAME_OFFERS_ID, reverseLookupBatch } from "@bonfida/spl-name-service"; +import { + FavouriteDomain, + NAME_OFFERS_ID, + reverseLookupBatch, + reverseLookup, + getAllDomains, + getFavoriteDomain, +} from "@bonfida/spl-name-service"; import { Connection, PublicKey } from "@solana/web3.js"; import { firebaseApi } from "."; @@ -29,6 +36,8 @@ type LeaderboardRow = { socialPoints: number; }; +const shortAddress = (address: string) => `${address.slice(0, 4)}...${address.slice(-4)}`; + async function fetchLeaderboardData({ connection, queryCursor, @@ -61,25 +70,50 @@ async function fetchLeaderboardData({ const leaderboardFinalSlice: LeaderboardRow[] = [...leaderboardSlice]; - if (connection) { - const publicKeys = leaderboardFinalSlice.map((value) => { - const [favoriteDomains] = FavouriteDomain.getKeySync(NAME_OFFERS_ID, new PublicKey(value.id)); - return favoriteDomains; - }); - const favoriteDomainsInfo = (await connection.getMultipleAccountsInfo(publicKeys)).map((accountInfo, idx) => - accountInfo ? FavouriteDomain.deserialize(accountInfo.data).nameAccount : publicKeys[idx] - ); - const reverseLookup = await reverseLookupBatch(connection, favoriteDomainsInfo); - - leaderboardFinalSlice.map( - (value, idx) => - (value.id = reverseLookup[idx] - ? `${reverseLookup[idx]}.sol` - : `${value.id.slice(0, 5)}...${value.id.slice(-5)}`) - ); + if (!connection) { + return leaderboardFinalSlice; } - return leaderboardFinalSlice; + const leaderboardFinalSliceWithDomains: LeaderboardRow[] = await Promise.all( + leaderboardFinalSlice.map(async (value) => { + // attempt to get favorite domain + try { + const { reverse } = await getFavoriteDomain(connection, new PublicKey(value.id)); + if (reverse) { + return { + ...value, + id: `${reverse}.sol`, + }; + } + } catch (e) { + // attempt to get all domains + try { + const domains = await getAllDomains(connection, new PublicKey(value.id)); + if (domains.length > 0) { + const reverse = await reverseLookup(connection, domains[0]); + if (reverse) { + return { + ...value, + id: `${reverse}.sol`, + }; + } + } + } catch (e) { + return { + ...value, + id: shortAddress(value.id), + }; + } + } + + return { + ...value, + id: shortAddress(value.id), + }; + }) + ); + + return leaderboardFinalSliceWithDomains; } // Firebase query is very constrained, so we calculate the number of users with more points From d0e9bee5c8b0009a1c148f42e70be6990acbcf75 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 13 Oct 2023 16:27:23 -0400 Subject: [PATCH 12/14] fix: increment user rank by 1 to account for zero based index --- packages/marginfi-v2-ui-state/src/lib/points.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/marginfi-v2-ui-state/src/lib/points.ts b/packages/marginfi-v2-ui-state/src/lib/points.ts index fa9764aa58..4158dd81f8 100644 --- a/packages/marginfi-v2-ui-state/src/lib/points.ts +++ b/packages/marginfi-v2-ui-state/src/lib/points.ts @@ -136,7 +136,7 @@ async function fetchUserRank(userPoints: number): Promise { const nullGreaterDocsCount = querySnapshot1.data().count; const allGreaterDocsCount = querySnapshot2.data().count; - return allGreaterDocsCount - nullGreaterDocsCount; + return allGreaterDocsCount - nullGreaterDocsCount + 1; } async function fetchTotalUserCount() { From 875adfd8b72158a5d85d294f6e6301ad2ea5b1e8 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 13 Oct 2023 23:02:03 -0400 Subject: [PATCH 13/14] feat: leaderboard responsive improvements --- .../desktop/Points/PointsLeaderBoard.tsx | 37 +++++++++---------- .../desktop/Points/PointsOverview.tsx | 2 +- apps/marginfi-v2-ui/src/pages/points.tsx | 2 +- apps/marginfi-v2-ui/tailwind.config.js | 3 ++ 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index d5e736eafa..c4b829ed2d 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -181,26 +181,19 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) return ( <> -

Total users: {leaderboardSettings.totalUserCount}

setOrderCol("total_points")} > Rank - {leaderboardSettings.orderCol === "total_points" && ( - - )} User = ({ currentUserId }) setOrderCol("total_points")} > Total Points + {leaderboardSettings.orderCol === "total_points" && ( + + )} @@ -266,8 +265,8 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) className={clsx( "border-none", index === 0 && "w-[10%]", - index === 1 && "w-[15%]", - index > 1 && "w-[15%]" + index === 1 && "w-[15%] min-w-[190px]", + index > 1 && "w-[15%] min-w-[190px]" )} key={index} > @@ -314,7 +313,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) @@ -323,7 +322,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "inherit" }} - className="hover:text-[#DCE85D]" + className={clsx("hover:text-[#DCE85D]")} > {data.id} @@ -331,7 +330,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) @@ -340,7 +339,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) @@ -349,7 +348,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) @@ -360,7 +359,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) @@ -369,7 +368,7 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsOverview.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsOverview.tsx index 8abf225ab6..498001be2d 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsOverview.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsOverview.tsx @@ -12,7 +12,7 @@ interface PointsOverviewProps { export const PointsOverview: FC = ({ userPointsData }) => { return ( <> -
+
diff --git a/apps/marginfi-v2-ui/src/pages/points.tsx b/apps/marginfi-v2-ui/src/pages/points.tsx index c7e2eed5d5..cb65235602 100644 --- a/apps/marginfi-v2-ui/src/pages/points.tsx +++ b/apps/marginfi-v2-ui/src/pages/points.tsx @@ -54,7 +54,7 @@ const Points: FC = () => { return ( <> points -
+
{!connected ? ( ) : currentFirebaseUser ? ( diff --git a/apps/marginfi-v2-ui/tailwind.config.js b/apps/marginfi-v2-ui/tailwind.config.js index 18f254e0e0..bb3775d25d 100644 --- a/apps/marginfi-v2-ui/tailwind.config.js +++ b/apps/marginfi-v2-ui/tailwind.config.js @@ -32,6 +32,9 @@ module.exports = { warning: "#daa204", error: "#e07d6f", }, + maxWidth: { + "8xl": "90rem", + }, }, fontFamily: { aeonik: ['"Aeonik Pro"'], From 50633c1809731fd03eab3e6e3197ab21bc611017 Mon Sep 17 00:00:00 2001 From: Adam Chambers Date: Fri, 13 Oct 2023 23:12:13 -0400 Subject: [PATCH 14/14] feat: highlight sol domains, fix solscan links --- .../desktop/Points/PointsLeaderBoard.tsx | 8 +++-- .../marginfi-v2-ui-state/src/lib/points.ts | 33 ++++++++----------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx index c4b829ed2d..2a2725bc77 100644 --- a/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx +++ b/apps/marginfi-v2-ui/src/components/desktop/Points/PointsLeaderBoard.tsx @@ -322,9 +322,13 @@ export const PointsLeaderBoard: FC = ({ currentUserId }) target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "inherit" }} - className={clsx("hover:text-[#DCE85D]")} + className={clsx( + "hover:text-[#DCE85D] hover:opacity-100 transition", + data.domain && "font-bold", + !data.domain && "opacity-80" + )} > - {data.id} + {data.domain || data.shortAddress || data.id} ; total_activity_deposit_points: number; total_activity_borrow_points: number; @@ -76,40 +78,31 @@ async function fetchLeaderboardData({ const leaderboardFinalSliceWithDomains: LeaderboardRow[] = await Promise.all( leaderboardFinalSlice.map(async (value) => { + const newValue = { ...value, shortAddress: shortAddress(value.id) }; // attempt to get favorite domain try { const { reverse } = await getFavoriteDomain(connection, new PublicKey(value.id)); - if (reverse) { - return { - ...value, - id: `${reverse}.sol`, - }; - } + return { + ...newValue, + domain: `${reverse}.sol`, + }; } catch (e) { // attempt to get all domains try { const domains = await getAllDomains(connection, new PublicKey(value.id)); if (domains.length > 0) { const reverse = await reverseLookup(connection, domains[0]); - if (reverse) { - return { - ...value, - id: `${reverse}.sol`, - }; - } + return { + ...newValue, + domain: `${reverse}.sol`, + }; } } catch (e) { - return { - ...value, - id: shortAddress(value.id), - }; + return newValue; } } - return { - ...value, - id: shortAddress(value.id), - }; + return newValue; }) );