From 3701013571d717b111617e412d8f66a6d752b9fe Mon Sep 17 00:00:00 2001 From: Maria Rykova Date: Thu, 7 Dec 2023 14:58:39 +0800 Subject: [PATCH] add new queries, update filtering, sorting, receiving ensNames --- web/leaderboard/package.json | 2 + .../src/app/components/Dropdown/index.tsx | 9 +- .../src/app/components/Table/index.tsx | 2 +- web/leaderboard/src/app/config.ts | 6 +- web/leaderboard/src/app/ensName.tsx | 25 ++ web/leaderboard/src/app/layout.tsx | 26 +- web/leaderboard/src/app/page.module.scss | 21 +- web/leaderboard/src/app/page.tsx | 241 ++++++------------ .../src/app/queries/useLeaderboardRanking.ts | 97 +++++++ web/leaderboard/src/app/types.ts | 20 +- web/leaderboard/src/app/utils/client.ts | 6 + 11 files changed, 240 insertions(+), 215 deletions(-) create mode 100644 web/leaderboard/src/app/ensName.tsx create mode 100644 web/leaderboard/src/app/queries/useLeaderboardRanking.ts create mode 100644 web/leaderboard/src/app/utils/client.ts diff --git a/web/leaderboard/package.json b/web/leaderboard/package.json index 9fff44554..964c2e73c 100644 --- a/web/leaderboard/package.json +++ b/web/leaderboard/package.json @@ -9,10 +9,12 @@ "lint": "next lint" }, "dependencies": { + "@apollo/client": "^3.8.8", "@fluidity-money/surfing": "*", "framer-motion": "^10.16.4", "graphql": "^16.8.1", "next": "14.0.1", + "next-urql": "^5.0.2", "react": "^18", "react-dom": "^18", "urql": "^4.0.6", diff --git a/web/leaderboard/src/app/components/Dropdown/index.tsx b/web/leaderboard/src/app/components/Dropdown/index.tsx index 748c8963e..6714d12fb 100644 --- a/web/leaderboard/src/app/components/Dropdown/index.tsx +++ b/web/leaderboard/src/app/components/Dropdown/index.tsx @@ -2,17 +2,19 @@ import { Text } from "@fluidity-money/surfing"; import styles from "./Dropdown.module.scss"; const sortedBy = [ - { title: "volume transacted", name: "volume" }, - { title: "rewards earned", name: "rewards" }, - { title: "number of transactions", name: "number" }, + { title: "VOLUME", name: "volume" }, + { title: "REWARDS", name: "rewards" }, + { title: "#TX", name: "number" }, ]; export const DropdownOptions = ({ setSortedByItem, setOpenDropdown, + sortData, }: { setSortedByItem: (value: string) => void; setOpenDropdown: (value: boolean) => void; + sortData: (value: string) => void; }) => { return (
@@ -24,6 +26,7 @@ export const DropdownOptions = ({ onClick={() => { setSortedByItem(option.name); setOpenDropdown(false); + sortData(option.name); }} > diff --git a/web/leaderboard/src/app/components/Table/index.tsx b/web/leaderboard/src/app/components/Table/index.tsx index 9f87fa64d..00f5c4ab8 100644 --- a/web/leaderboard/src/app/components/Table/index.tsx +++ b/web/leaderboard/src/app/components/Table/index.tsx @@ -1,7 +1,7 @@ import React, { ReactNode } from "react"; import { useSearchParams } from "next/navigation"; import { AnimatePresence, motion } from "framer-motion"; -import { GeneralButton, LoadingDots, Text } from "@fluidity-money/surfing"; +import { LoadingDots, Text } from "@fluidity-money/surfing"; import styles from "../../page.module.scss"; type Filter = { diff --git a/web/leaderboard/src/app/config.ts b/web/leaderboard/src/app/config.ts index ee6b15457..1335ceb0b 100644 --- a/web/leaderboard/src/app/config.ts +++ b/web/leaderboard/src/app/config.ts @@ -54,7 +54,7 @@ export const tableHeadings = [ ]; export enum SORTED_ITEM { - volume = "volume transacted", - rewards = "rewards earned", - number = "number of transactions", + volume = "VOLUME", + rewards = "REWARDS", + number = "#TX", } diff --git a/web/leaderboard/src/app/ensName.tsx b/web/leaderboard/src/app/ensName.tsx new file mode 100644 index 000000000..ab0ad4054 --- /dev/null +++ b/web/leaderboard/src/app/ensName.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { useEnsName } from "wagmi"; + +const trimAddress = (address: string): string => { + const leftSide = address.slice(0, 4); + + const rightSide = address.slice(-4); + + return leftSide + "..." + rightSide; +}; + +const UseEnsName = (address: any) => { + const ensName = useEnsName({ + address: address.address, + chainId: 1, + }); + + return ( +
+ {ensName.data === null ? trimAddress(address.address) : ensName.data} +
+ ); +}; + +export default UseEnsName; diff --git a/web/leaderboard/src/app/layout.tsx b/web/leaderboard/src/app/layout.tsx index 737a24025..7087fccc2 100644 --- a/web/leaderboard/src/app/layout.tsx +++ b/web/leaderboard/src/app/layout.tsx @@ -1,27 +1,31 @@ -import type { Metadata } from "next"; +"use client"; + +//import type { Metadata } from "next"; import { inter, spline, aeonik } from "./config"; import { Providers } from "./providers"; +import { ApolloProvider } from "@apollo/client"; +import { client } from "./utils/client"; import "./globals.css"; -export const metadata: Metadata = { - title: "Leaderboard", - description: "Fluidity Leaderbord page", -}; +//export const metadata: Metadata = { +// title: "Leaderboard", +// description: "Fluidity Leaderbord page", +//}; -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { +function RootLayout({ children }: { children: React.ReactNode }) { return ( - {children} + + {children} + ); } + +export default RootLayout; diff --git a/web/leaderboard/src/app/page.module.scss b/web/leaderboard/src/app/page.module.scss index a0ea05724..d3de84907 100644 --- a/web/leaderboard/src/app/page.module.scss +++ b/web/leaderboard/src/app/page.module.scss @@ -217,18 +217,17 @@ $gray: #999999; .table_content > tbody > tr.table_row.highlighted_row { background: linear-gradient( 90deg, - #f3b8d8 0%, - #b793e9 15.1%, - #9fd4f3 26.04%, - #ffd2c4 36.46%, - #fbf3f3 46.88%, - #d9abdf 57.29%, - #af9ce3 72.4%, - #aae4e1 85.42%, - #c6ead0 93.23%, - #fdb5e4 100% + rgba(243, 184, 216, 20%) 0%, + rgba(183, 147, 233, 20%) 15.1%, + rgba(159, 212, 243, 20%) 26.04%, + rgba(255, 210, 196, 20%) 36.46%, + rgba(251, 243, 243, 20%) 46.88%, + rgba(217, 171, 223, 20%) 57.29%, + rgba(175, 156, 227, 20%) 72.4%, + rgba(170, 228, 225, 20%) 85.42%, + rgba(198, 234, 208, 20%) 93.23%, + rgba(253, 181, 228, 20%) 100% ); - opacity: 20%; } .table_content > tbody > tr > td { diff --git a/web/leaderboard/src/app/page.tsx b/web/leaderboard/src/app/page.tsx index bf7081e78..88055e37e 100644 --- a/web/leaderboard/src/app/page.tsx +++ b/web/leaderboard/src/app/page.tsx @@ -1,9 +1,7 @@ "use client"; -import { useState, useEffect, useContext, useRef } from "react"; -import { AnimatePresence, motion } from "framer-motion"; -import { useEnsName, useAccount, useConnect, useDisconnect } from "wagmi"; -import { fetchEnsName } from "@wagmi/core"; +import { useState, useEffect, useContext, useRef, useCallback } from "react"; +import { useAccount, useConnect, useDisconnect, useEnsAvatar } from "wagmi"; import Image from "next/image"; import styles from "./page.module.scss"; import { @@ -12,53 +10,22 @@ import { Heading, ArrowTopRight, useClickOutside, + toSignificantDecimals, } from "@fluidity-money/surfing"; import Socials from "./components/Socials"; import Table from "./components/Table"; import Footer from "./components/Footer"; import { DropdownOptions } from "./components/Dropdown"; -import { Transfer, AggregatedData, Data, Reward } from "./types"; +import { Data } from "./types"; import { tableHeadings, SORTED_ITEM } from "./config"; -import { Client, cacheExchange, fetchExchange, gql } from "urql"; - -import { timestamp24HoursAgo } from "./utils"; - -const APIURL = - "https://gateway-arbitrum.network.thegraph.com/api/f9ffa86b5ab1229ce9b91179448a0891/subgraphs/id/CdS3475tZUcWVHsecnELJxBEGXV8nrbe5h3VmCbhe9qd"; - -const queryAllTime = gql` - query { - transfers { - value - from - blockTimestamp - } - rewards { - amount - winner - } - } -`; - -const queryLast24hours = gql` - query { - transfers(where: { blockTimestamp_gte: ${String(timestamp24HoursAgo)} }) { - value - from - } - rewards(where: { blockNumber_gte: ${String(timestamp24HoursAgo)} }) { - amount - winner - } - } -`; +import { + useLeaderboardRanking24Hours, + useLeaderboardRankingAllTime, +} from "./queries/useLeaderboardRanking"; -const client = new Client({ - url: APIURL, - exchanges: [cacheExchange, fetchExchange], -}); +import UseEnsName from "./ensName"; export type IRow = React.HTMLAttributes & { RowElement: React.FC<{ heading: string }>; @@ -68,8 +35,13 @@ export default function Home() { const [filterIndex, setFilterIndex] = useState(0); const [loaded, setLoaded] = useState(); + const data24Hours = useLeaderboardRanking24Hours(); + const dataAllTime = useLeaderboardRankingAllTime(); + + const [last24HoursTimeData, setLast24HoursTimeData] = useState( + [] + ); const [allTimeData, setAllTimeData] = useState([]); - const [last24TimeData, setLast24TimeData] = useState([]); const [userAddress, setUserAddress] = useState( "0x1cb94adfd3314d48ca8145b2c6983419257c0486" @@ -87,127 +59,52 @@ export default function Home() { const [sortedData, setSortedData] = useState([]); const [sortedByItem, setSortedByItem] = useState("number"); - async function fetch24HoursData() { - const response = await client.query(queryLast24hours, {}).toPromise(); - const data: Data[] = createNewArray( - response.data.transfers, - response.data.rewards - ); - - console.log(response); - setLast24TimeData(data); - } - - async function fetchAllTimeData() { - const response = await client.query(queryAllTime, {}).toPromise(); - console.log("response:", response); - - const data: Data[] = createNewArray( - response.data.transfers, - response.data.rewards - ); - setAllTimeData(data); - } - - function sortData(sortBy: string, data: Data[]) { - switch (sortBy) { - case "volume": - const newSortedData = data.sort( - (a, b) => - parseInt(b.volume.replace(/,/g, ""), 10) - - parseInt(a.volume.replace(/,/g, ""), 10) - ); - setSortedData(newSortedData); - break; - case "rewards": - const newSortedDataEarned = data.sort( - (a, b) => - parseInt(b.earned.replace(/,/g, ""), 10) - - parseInt(a.earned.replace(/,/g, ""), 10) - ); - setSortedData(newSortedDataEarned); - break; - default: - setSortedData(data.sort((a, b) => b.tx - a.tx)); - } - } - - useEffect(() => { - fetchAllTimeData(); - }, []); - - useEffect(() => { - sortData(sortedByItem, sortedData); - }, [sortedByItem]); - - function createNewArray(transfers: Transfer[], rewards: Reward[]) { - const newArr: AggregatedData = {}; - - transfers.forEach(({ value, from }: Transfer) => { - const numericValue = parseInt(value, 10); - if (!newArr[from]) { - newArr[from] = { - volume: numericValue, - tx: 1, - earned: 0, - user: from, - }; - } else { - newArr[from].volume += numericValue; - newArr[from].tx += 1; - } - }); - - for (let key in newArr) { - for (let i = 0; i < rewards.length; i++) { - if (newArr[key].user === rewards[i].winner) { - const numericValue = parseInt(rewards[i].amount, 10); - newArr[key].earned += numericValue; - } else { - console.log(`${rewards[i]} is not found`); - } + const sortData = useCallback( + (sortBy: string) => { + switch (sortBy) { + case "volume": + const newSortedData = sortedData.sort( + (a, b) => Number(b.volume) - Number(a.volume) + ); + setSortedData(newSortedData); + break; + case "rewards": + const newSortedDataEarned = sortedData.sort( + (a, b) => Number(b.yield_earned) - Number(a.yield_earned) + ); + setSortedData(newSortedDataEarned); + break; + default: + setSortedData( + sortedData.sort( + (a, b) => b.number_of_transactions - a.number_of_transactions + ) + ); } - } - - const resultArray: Data[] = Object.entries(newArr).map( - ([from, { volume, tx, user, earned }], index) => ({ - user: receiveEnsName(from), - volume: volume.toLocaleString().replace(/\s/g, ","), - tx, - earned: earned.toLocaleString().replace(/\s/g, ","), - rank: index + 1, - }) - ); - - const sortedResult: Data[] = resultArray.sort((a, b) => b.tx - a.tx); - setSortedData(sortedResult); - return resultArray; - } + }, + [sortedData] + ); - const receiveEnsName = async (address: any) => { + useEffect(() => { try { - const ensName = await fetchEnsName({ - address: address, - chainId: 1, + data24Hours.then((res) => { + setLast24HoursTimeData(res.data.leaderboard_ranking); + setSortedData(res.data.leaderboard_ranking); }); - - if (ensName !== null) { - return ensName; - } - - return address; + dataAllTime.then((res) => setAllTimeData(res.data.leaderboard_ranking)); } catch (error) { - console.error("Error fetching ENS name:", error); + console.log(error); } - }; + }, []); const airdropRankRow = (data: any): IRow => { - const address = "0x1cb94adfd3314d48ca8145b2c6983419257c0486"; - const { user, rank, tx, volume, earned } = data; + const addressUser = "0x1cb94adfd3314d48ca8145b2c6983419257c0486"; + const { address, rank, number_of_transactions, volume, yield_earned } = + data; return { - className: `${ - address === user.value ? styles.highlited : styles.table_row + className: `${styles.table_row} ${ + addressUser === address ? styles.highlighted_row : "" }`, RowElement: ({ heading }: { heading: string }) => { switch (heading) { @@ -222,10 +119,11 @@ export default function Home() { - { - address === user.value ? "ME" : user - // trimAddress(user) - } + {addressUser === address ? ( + "ME" + ) : ( + + )} @@ -233,19 +131,19 @@ export default function Home() { case "#TX": return ( - {tx} + {number_of_transactions} ); case "VOLUME (USD)": return ( - {volume} + {toSignificantDecimals(volume, 1)} ); case "YIELD EARNED (USD)": return ( - {earned} + {toSignificantDecimals(yield_earned, 3)} ); default: @@ -329,9 +227,9 @@ export default function Home() {
This leaderboard shows your rank among other users - {filterIndex === 1 ? " per" : " for"} + {filterIndex === 0 ? " per" : " for"}   - {filterIndex === 1 ? ( + {filterIndex === 0 ? ( 24 HOURS ) : ( ALL TIME @@ -343,11 +241,12 @@ export default function Home() {
{ - setFilterIndex(1); - fetch24HoursData(); + setFilterIndex(0); + setSortedData(last24HoursTimeData); + setSortedByItem("number"); }} className={ - filterIndex === 1 + filterIndex === 0 ? `${styles.btn} ${styles.btn_highlited}` : `${styles.btn}` } @@ -356,11 +255,12 @@ export default function Home() { { - setFilterIndex(0); - fetchAllTimeData(); + setFilterIndex(1); + setSortedData(allTimeData); + setSortedByItem("number"); }} className={ - filterIndex === 1 + filterIndex === 0 ? `${styles.btn}` : `${styles.btn} ${styles.btn_highlited}` } @@ -392,6 +292,7 @@ export default function Home() { )}
@@ -428,7 +329,7 @@ export default function Home() { data={sortedData} renderRow={(data) => airdropRankRow(data)} freezeRow={(data) => { - return data.user === userAddress; + return data.address === userAddress; }} onFilter={() => true} activeFilterIndex={0} diff --git a/web/leaderboard/src/app/queries/useLeaderboardRanking.ts b/web/leaderboard/src/app/queries/useLeaderboardRanking.ts new file mode 100644 index 000000000..4c3e81fe8 --- /dev/null +++ b/web/leaderboard/src/app/queries/useLeaderboardRanking.ts @@ -0,0 +1,97 @@ +const gql = String.raw; + +const jsonPost = async < + Req = { query: string }, + Res = { data: Record } +>( + url: string, + body: Req, + headers?: Record +): Promise => { + try { + const res = await fetch(url, { + headers: { + "Content-Type": "application/json", + ...(headers ? headers : {}), + }, + method: "POST", + body: JSON.stringify(body), + }); + + return res.json(); + } catch (e) { + throw new Error(`Could not parse JSON: ${e}`); + } +}; + +const queryLeaderboardRanking24Hours = gql` + query LeaderboardRanking { + leaderboard_ranking( + args: { i: "1 day", network_: "arbitrum" } + limit: 40 + order_by: { number_of_transactions: desc } + ) { + address + number_of_transactions + rank + volume + yield_earned + } + } +`; + +const queryLeaderboardRankingAllTime = gql` + query LeaderboardRanking { + leaderboard_ranking( + args: { network_: "arbitrum" } + limit: 40 + order_by: { number_of_transactions: desc } + ) { + address + number_of_transactions + rank + volume + yield_earned + } + } +`; + +const useLeaderboardRanking24Hours = () => { + const variables = {}; + const url = "https://fluidity.hasura.app/v1/graphql"; + const body = { + variables, + query: queryLeaderboardRanking24Hours, + }; + + return jsonPost( + url, + body, + process.env.FLU_HASURA_SECRET + ? { + "x-hasura-admin-secret": process.env.FLU_HASURA_SECRET, + } + : {} + ); +}; + +const useLeaderboardRankingAllTime = () => { + const variables = {}; + const url = "https://fluidity.hasura.app/v1/graphql"; + const body = { + variables, + query: queryLeaderboardRankingAllTime, + }; + + return jsonPost( + url, + body, + process.env.FLU_HASURA_SECRET + ? { + "x-hasura-admin-secret": process.env.FLU_HASURA_SECRET, + } + : {} + ); +}; + +export { useLeaderboardRanking24Hours, useLeaderboardRankingAllTime }; diff --git a/web/leaderboard/src/app/types.ts b/web/leaderboard/src/app/types.ts index 482380faa..839fe54b5 100644 --- a/web/leaderboard/src/app/types.ts +++ b/web/leaderboard/src/app/types.ts @@ -1,15 +1,3 @@ -export interface Transfer { - value: string; - from: string; - __typename: string; -} - -export interface Reward { - amount: string; - winner: string; - __typename: string; -} - export interface AggregatedData { [from: string]: { rank?: number; @@ -22,8 +10,8 @@ export interface AggregatedData { export interface Data { rank?: number; - user: Promise | string; - volume: string; - tx: number; - earned: string; + address: string; + volume: string | number; + number_of_transactions: number; + yield_earned: string; } diff --git a/web/leaderboard/src/app/utils/client.ts b/web/leaderboard/src/app/utils/client.ts new file mode 100644 index 000000000..c0f1863fc --- /dev/null +++ b/web/leaderboard/src/app/utils/client.ts @@ -0,0 +1,6 @@ +import { ApolloClient, InMemoryCache } from "@apollo/client"; + +export const client = new ApolloClient({ + uri: "https://fluidity.hasura.app/v1/graphql", + cache: new InMemoryCache(), +});