diff --git a/components/OrchestratorVotingList/index.tsx b/components/OrchestratorVotingList/index.tsx new file mode 100644 index 00000000..84de0bdd --- /dev/null +++ b/components/OrchestratorVotingList/index.tsx @@ -0,0 +1,314 @@ +import Table from "@components/Table"; +import { textTruncate } from "@lib/utils"; +import { Badge, Box, Flex, Link as A, Text } from "@livepeer/design-system"; +import { CheckIcon, Cross2Icon, MinusIcon } from "@radix-ui/react-icons"; +import Link from "next/link"; +import numeral from "numeral"; +import QRCode from "qrcode.react"; +import { useMemo } from "react"; + +import { ExplorerTooltip } from "@components/ExplorerTooltip"; +import { useEnsData } from "hooks"; + + + +type VoterSummary = { + id: string; + noOfProposalsVotedOn: number; + noOfVotesCasted: number; + mostRecentVotes: (string | null)[]; + votingTurnout: number; +}; + +const OrchestratorVotingList = ({ initialVoterData, pageSize = 10 }: { initialVoterData?: VoterSummary[], pageSize: number }) => { + + const columns = useMemo( + () => [ + { + Header: ( + + The account which is actively coordinating transcoders and + receiving fees/rewards. + + } + > + + Orchestrator + + + ), + accessor: "id", + Cell: ({ row }) => { + const identity = useEnsData(row.values.id); + + return ( + + + + + {+row.id + 1} + + + + {identity?.avatar ? ( + + ) : ( + + )} + {identity?.name ? ( + + + {textTruncate(identity.name, 20, "…")} + + + {row.values.id.substring(0, 6)} + + + ) : ( + + {row.values.id.replace(row.values.id.slice(7, 37), "…")} + + )} + + + + + ); + }, + }, + { + Header: ( + + The total number of governance proposals this orchestrator has participated in by casting a vote. + + } + > + + Number of Proposals Voted On + + + + ), + accessor: "noOfProposalsVotedOn", + Cell: ({ row }) => ( + + + {numeral(row.values.noOfProposalsVotedOn).format("0,0")} + + + ), + sortType: "number", + }, + { + Header: ( + + The total count of individual votes submitted by this orchestrator across all proposals. + + } + > + + Number of Votes Casted + + + ), + accessor: "noOfVotesCasted", + Cell: ({ row }) => ( + + + {numeral(row.values.noOfVotesCasted).format("0,0")} + + + ), + sortType: "number", + }, + { + Header: ( + + A list of up to 5 of the orchestrator’s most recent votes, marked as [✓] for For, [✗] for Against, and [–] for Abstain. + + } + > + + Most Recent Votes + + + ), + accessor: "mostRecentVotes", + Cell: ({ row }) => ( + + + {row.values.mostRecentVotes?.map((mostRecentVote, index) => { + + let icon = + mostRecentVote == "for" ? ( + + ) : mostRecentVote == "against" ? ( + + ) : mostRecentVote == "abstain" ? ( + + ) : null; + + return ( + + {icon} + + ); + })} + + + ), + }, + { + Header: ( + + The percentage of total governance proposals this orchestrator voted on, showing how actively they participate in protocol decisions. + + } + > + + Voting Turnout + + + + ), + accessor: "votingTurnout", + Cell: ({ row }) => ( + + + {numeral(row.values.votingTurnout).format("0.0%")} + + + ), + sortType: "number", + }, + ], + [] + ); + if (initialVoterData) { + return ( + + ); + } else { + return null; + } +}; + + + +export default OrchestratorVotingList; diff --git a/components/VotingHistoryView/index.tsx b/components/VotingHistoryView/index.tsx new file mode 100644 index 00000000..91eac400 --- /dev/null +++ b/components/VotingHistoryView/index.tsx @@ -0,0 +1,239 @@ +import { + Card as CardBase, + Link as A, + styled, + Flex, + Box, +} from "@livepeer/design-system"; +import { CUBE_TYPE, getCubeData } from "cube/cube-client"; +import { getAccountVotingHistory } from "cube/queryGenrator"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { QuestionMarkCircledIcon } from "@radix-ui/react-icons"; +import Spinner from "@components/Spinner"; +import Image from "next/image"; +import { ExplorerTooltip } from "@components/ExplorerTooltip"; + + + +const Index = () => { + const router = useRouter(); + const query = router.query; + const account = query.account as string; + + const [proposalVotedOn, setProposalVotedOn] = useState(); + const [votingTurnOut, setVotingTurnOut] = useState(); + const [votingData, setVotingData] = useState() + const [isLoading, setIsLoading] = useState(false); + + const getBackgroundColorByStatus = (status: string) => { + let bgColor = "#212322"; + switch (status) { + case "Active": + bgColor = "#16271F" + break; + case "Defeated": + bgColor = "#321C1D"; + break; + case "Executed": + bgColor = "#212322"; + break; + default: + break; + } + return bgColor; + } + + const getTextStyleByStatus = (status: string) => { + const stylesMap: Record = { + Active: { + color: "#51A7FD", + backgroundColor: "#11233E", + maxWidth: 80, + justifyContent: 'center', + display: 'flex', + borderRadius: 8 + }, + Defeated: { + color: "#FF6468", + backgroundColor: "#3C181A", + maxWidth: 80, + justifyContent: 'center', + display: 'flex', + borderRadius: 8 + }, + Executed: { + color: "#4ABF87", + backgroundColor: "#10291E", + maxWidth: 80, + justifyContent: 'center', + display: 'flex', + borderRadius: 8 + }, + }; + + return stylesMap[status] || {}; // Returns styles if status is found, otherwise returns an empty object + }; + + + + function shortenAddress(address: string) { + if (address.length < 10) return address; // Handle short addresses + + const first = address.slice(0, 6); // Get the '0x' + first 4 characters + const last = address.slice(-4); // Get last 4 characters + + return `${first}...${last}`; // Return formatted string + } + + + + const fetchingData = async () => { + setIsLoading(true); + try { + const query = getAccountVotingHistory(account); + const response = await getCubeData(query, { type: CUBE_TYPE.SERVER }); + const data = response[0].data; + if (data.length > 0) { + setVotingTurnOut(data[0]['LivepeerProposalStatus.votingTurnout']); + setProposalVotedOn(data[0]["LivepeerProposalStatus.proposalVotedOn"]); + setVotingData(data); + } + setIsLoading(false) + } catch (error) { + setIsLoading(false) + } + } + + useEffect(() => { + fetchingData(); + }, []) + + const getDateTimeAndRound = (date: string, round: string): string => { + // Parse the date string to a Date object + const dateObj = new Date(date); + + // Function to format the date to "MM/DD/YYYY h:mm:ss a" + const formatDate = (date: Date): string => { + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const hours = date.getHours(); + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + const ampm = hours >= 12 ? 'pm' : 'am'; + + const day = date.getDate(); + const month = months[date.getMonth()]; + const year = date.getFullYear(); + const formattedTime = `${month} ${day}, ${year} ${hours % 12 || 12}:${minutes < 10 ? '0' + minutes : minutes}:${seconds < 10 ? '0' + seconds : seconds} ${ampm}`; + + return formattedTime; + }; + + // Round logic (In case the round value needs transformation, it's done here) + const roundNumber = round.split("-")[0]; // Assuming round is in the format "3466-01-01T00:00:00.000", just using the first part + + // Format date + const formattedDate = formatDate(dateObj); + + // Return the final output in the required format + return `${formattedDate} - Round #${roundNumber}`; + } + + if (isLoading) { + return ( + + + + ) + } + return ( +
+
+ +
+
+
PROPOSALS VOTED ON
+ + The total number of governance proposals this orchestrator has participated in by casting a vote. + + } + > + + + + +
+
{proposalVotedOn}
+
+ +
+
+
VOTING TURNOUT
+ + The percentage of total governance proposals this orchestrator voted on, showing how actively they participate in protocol decisions. + + } + > + + + + +
+
{votingTurnOut}%
+
+ +
+
+ {votingData && + // @ts-ignore + votingData.map((el, index) => { + return ( +
+
{el['LivepeerProposalStatus.nameOfProposal']}
+
{getDateTimeAndRound(el['LivepeerProposalStatus.date'], el['LivepeerProposalStatus.round'])}
+
Proposed by livepeer.eth
+
{el["LivepeerProposalStatus.status"]}
+
+ +
+ {shortenAddress(el['LivepeerProposalStatus.voter'])} +
+ + +
+
+
+ ) + })} +
+
+ ) +}; + +export default Index; + diff --git a/cube/cube-client.ts b/cube/cube-client.ts new file mode 100644 index 00000000..3ef7422a --- /dev/null +++ b/cube/cube-client.ts @@ -0,0 +1,47 @@ +import cubejs, { QueryType } from "@cubejs-client/core"; + +export enum CUBE_TYPE { + SERVER = "SERVER", + CLIENT = "CLIENT", + PUBLIC = "PUBLIC", +} + +const CUBE_BASE_URL ="https://cube.dev.analytics.pyor.xyz" ; +let cubePublicAuthToken = process.env.CUBE_PUBLIC_TOKEN || ""; + +const cubejsApiClient = cubejs("CUBEJS-API-TOKEN", { + apiUrl: `/api/services/cube/cubejs-api/v1`, +}); + +const cubejsApiPublic = cubejs(cubePublicAuthToken, { + apiUrl: `${CUBE_BASE_URL}/cubejs-api/v1`, +}); + +export async function getCubeData( + query: any, + options: { + type: CUBE_TYPE; + headerData?: { token: any }; + } = { + type: CUBE_TYPE.CLIENT, + } +) { + let cubejsApi = + options.type === CUBE_TYPE.CLIENT + ? cubejsApiPublic + : options.type === CUBE_TYPE.SERVER + ? cubejs(options.headerData?.token, { + apiUrl: `${CUBE_BASE_URL}/cubejs-api/v1`, + }) + : cubejsApiPublic; + + try { + const resultSet: any = await cubejsApi.load(query); + const response = resultSet.loadResponse.results; + return response; + } catch (error) { + console.error(error); + } +} + + diff --git a/cube/queryGenrator.ts b/cube/queryGenrator.ts new file mode 100644 index 00000000..3d53d984 --- /dev/null +++ b/cube/queryGenrator.ts @@ -0,0 +1,51 @@ +export const getAccountVotingHistory =(id:string) =>{ + + return `{ + "measures": [ + "LivepeerProposalStatus.count", + "LivepeerProposalStatus.votingTurnout", + "LivepeerProposalStatus.proposalVotedOn" + ], + "order": { + "LivepeerProposalStatus.count": "desc" + }, + "dimensions": [ + "LivepeerProposalStatus.date", + "LivepeerProposalStatus.round", + "LivepeerProposalStatus.eventTxnHash", + "LivepeerProposalStatus.nameOfProposal", + "LivepeerProposalStatus.voteType", + "LivepeerProposalStatus.status", + "LivepeerProposalStatus.proposedBy", + "LivepeerProposalStatus.voter" + ], + "filters": [ + { + "member": "LivepeerProposalStatus.voter", + "operator": "equals", + "values": [ + "${id}" + ] + } + ] +}`} + + +export const getOrchestratorsVotingHistory=()=>{ + return `{ + "measures": [ + "LivepeerVoteProposals.count", + "LivepeerVoteProposals.numOfProposals", + "LivepeerVoteProposals.numOfVoteCasted" + ], + "order": { + "LivepeerVoteProposals.count": "desc" + }, + "dimensions": [ + "LivepeerVoteProposals.date", + "LivepeerVoteProposals.voter", + "LivepeerVoteProposals.eventTxnsHash", + "LivepeerVoteProposals.voteType" + ] +}` +} \ No newline at end of file diff --git a/layouts/account.tsx b/layouts/account.tsx index 5c88a8e9..6b74ab06 100644 --- a/layouts/account.tsx +++ b/layouts/account.tsx @@ -9,6 +9,7 @@ import { useContractRead } from "wagmi"; import BottomDrawer from "@components/BottomDrawer"; import DelegatingView from "@components/DelegatingView"; import HistoryView from "@components/HistoryView"; +import VotingHistoryView from "@components/VotingHistoryView"; import OrchestratingView from "@components/OrchestratingView"; import { checkAddressEquality } from "@lib/utils"; import { @@ -30,6 +31,7 @@ import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { useWindowSize } from "react-use"; import { useAccountAddress, useEnsData, useExplorerStore } from "../hooks"; +import VotingHistory from "pages/accounts/[account]/voting_history"; export interface TabType { name: string; @@ -37,9 +39,9 @@ export interface TabType { isActive?: boolean; } -type TabTypeEnum = "delegating" | "orchestrating" | "history"; +type TabTypeEnum = "delegating" | "orchestrating" | "history" |"voting_history"; -const ACCOUNT_VIEWS: TabTypeEnum[] = ["delegating", "orchestrating", "history"]; +const ACCOUNT_VIEWS: TabTypeEnum[] = ["delegating", "orchestrating", "history", "voting_history"]; const AccountLayout = ({ account, @@ -287,6 +289,7 @@ const AccountLayout = ({ /> )} {view === "history" && } + {view === "voting_history" && } {(isOrchestrator || isMyDelegate || isDelegatingAndIsMyAccountView) && (width > 1020 ? ( @@ -360,6 +363,11 @@ function getTabs( href: `/accounts/${account}/history`, isActive: view === "history", }, + { + name: "Voting History", + href: `/accounts/${account}/voting_history`, + isActive: view === "voting_history", + }, ]; if (isOrchestrator || isMyDelegate) { tabs.unshift({ diff --git a/lib/orchestrartor.ts b/lib/orchestrartor.ts new file mode 100644 index 00000000..9a9ecbe9 --- /dev/null +++ b/lib/orchestrartor.ts @@ -0,0 +1,4 @@ +export enum OrchestratorTabs { + "Yield Overview" = "Yield Overview", + "Voting History" = "Voting History", +} diff --git a/package.json b/package.json index e95e2528..2529f5bb 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,9 @@ "typescript": "5.1.6" }, "dependencies": { + "@apollo/client": "^3.5.8", + "@cubejs-client/core": "^1.2.9", + "@cubejs-client/react": "^1.2.4", "@apollo/client": "^3.13.1", "@graphql-tools/delegate": "8.8.0", "@graphql-tools/links": "^8.3.0", diff --git a/pages/accounts/[account]/voting_history.tsx b/pages/accounts/[account]/voting_history.tsx new file mode 100644 index 00000000..428e32ad --- /dev/null +++ b/pages/accounts/[account]/voting_history.tsx @@ -0,0 +1,70 @@ +import { getLayout } from "@layouts/main"; +import AccountLayout from "@layouts/account"; +import { + AccountQueryResult, + getApollo, + OrchestratorsSortedQueryResult, +} from "apollo"; +import { getAccount, getSortedOrchestrators } from "@lib/api/ssr"; +import { EnsIdentity } from "@lib/api/types/get-ens"; + +type PageProps = { + account: AccountQueryResult["data"]; + sortedOrchestrators: OrchestratorsSortedQueryResult["data"]; + fallback: { [key: string]: EnsIdentity }; +}; + +const VotingHistory = ({ account, sortedOrchestrators }: PageProps) => ( + +); + +VotingHistory.getLayout = getLayout; + +export const getStaticPaths = async () => { + const { sortedOrchestrators } = await getSortedOrchestrators(); + + return { + paths: + sortedOrchestrators?.data?.transcoders?.map((t) => ({ + params: { account: t.id }, + })) ?? [], + fallback: "blocking", + }; +}; + +export const getStaticProps = async (context) => { + try { + const client = getApollo(); + const { account, fallback } = await getAccount( + client, + context.params?.account?.toString().toLowerCase() + ); + + const { sortedOrchestrators, fallback: sortedOrchestratorsFallback } = + await getSortedOrchestrators(client); + + if (!account.data || !sortedOrchestrators.data) { + return null; + } + + const props: PageProps = { + account: account.data, + sortedOrchestrators: sortedOrchestrators.data, + fallback: { + ...sortedOrchestratorsFallback, + ...fallback, + }, + }; + + return { + props, + revalidate: 600, + }; + } catch (e) { + console.error(e); + } + + return null; +}; + +export default VotingHistory; diff --git a/pages/index.tsx b/pages/index.tsx index a99644fa..241f8b40 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -13,6 +13,7 @@ import { Container, Flex, Heading, + Text, } from "@livepeer/design-system"; import { ArrowRightIcon } from "@modulz/radix-icons"; import Link from "next/link"; @@ -30,6 +31,18 @@ import { HomeChartData } from "@lib/api/types/get-chart-data"; import { EnsIdentity } from "@lib/api/types/get-ens"; import { useChartData } from "hooks"; import "react-circular-progressbar/dist/styles.css"; +import OrchestratorVotingList from "@components/OrchestratorVotingList"; +import { OrchestratorTabs } from "@lib/orchestrartor"; +import { getOrchestratorsVotingHistory } from "cube/queryGenrator"; +import { CUBE_TYPE, getCubeData } from "cube/cube-client"; + +import { + Tabs, + TabList, + Tab, + TabPanels, + TabPanel, +} from "@reach/tabs"; const Panel = ({ children }) => ( { +const Home = ({ orchestrators, events, protocol, initialVoterData }: PageProps) => { const allEvents = useMemo( () => events?.transactions @@ -368,19 +382,95 @@ const Home = ({ orchestrators, events, protocol }: PageProps) => { - {!orchestrators?.transcoders || !protocol?.protocol ? ( - - - - ) : ( - - - - )} + + {({ selectedIndex, focusedIndex }) => { + let getTabStyle = (index) => ({ + borderBottom: `4px solid ${selectedIndex === index + ? "#6ec08d" + : focusedIndex === index + ? "#141716" + : "#141716" + }` + , + backgroundColor: '#141716', borderWidth: 0, borderBottomWidth: 1 , + paddingBottom:12 + }); + return ( + <> + + + Yield Overview + + + Voting History + + + + + + + + + + + + + + + + ); + }} + + + {/* + + + Yield Overview + + + Voting History + + + + {!orchestrators?.transcoders || !protocol?.protocol ? ( + + + + ) : ( + + + + )} + + + + + + + */} { ); }; +type VoteProposal = { + "LivepeerVoteProposals.date": string; + "LivepeerVoteProposals.voter": string; + "LivepeerVoteProposals.eventTxnsHash": string; + "LivepeerVoteProposals.voteType": string; + "LivepeerVoteProposals.count": string; + "LivepeerVoteProposals.numOfProposals": string; + "LivepeerVoteProposals.numOfVoteCasted": string; +}; + +type VoterSummary = { + id: string; + noOfProposalsVotedOn: number; + noOfVotesCasted: number; + mostRecentVotes: (string | null)[]; + votingTurnout: number; +}; + + +// Function to get unique voter IDs +const getUniqueVoters = (data: VoteProposal[]): string[] => { + const voterSet = new Set(data.map(proposal => proposal["LivepeerVoteProposals.voter"])); + return Array.from(voterSet); +}; + +// Function to group data by voter +const groupByVoter = (data: VoteProposal[], voterId: string): VoteProposal[] => { + return data.filter(proposal => proposal["LivepeerVoteProposals.voter"] === voterId); +}; + +// Function to process vote proposals and generate voter summary +const processVoteProposals = (proposals: VoteProposal[]): VoterSummary => { + const sortedVotes = proposals.sort((a, b) => + new Date(b["LivepeerVoteProposals.date"]).getTime() - new Date(a["LivepeerVoteProposals.date"]).getTime() + ); + + const mostRecentVotes = sortedVotes.slice(0, 5).map(vote => vote["LivepeerVoteProposals.voteType"] || null); + + const noOfProposalsVotedOn =Number(proposals[0]['LivepeerVoteProposals.numOfProposals'] || 0); + const noOfVotesCasted = Number(proposals[0]['LivepeerVoteProposals.numOfVoteCasted']|| 0); + + const votingTurnout = noOfProposalsVotedOn ? noOfVotesCasted / noOfProposalsVotedOn : 0; + + return { + id: proposals[0]["LivepeerVoteProposals.voter"], + noOfProposalsVotedOn, + noOfVotesCasted, + mostRecentVotes, + votingTurnout, + }; +}; + +// Function to get voter summaries for all unique voters +const getVoterSummaries = (data: VoteProposal[]): VoterSummary[] => { + const uniqueVoters = getUniqueVoters(data); + return uniqueVoters.map(voterId => { + const groupedProposals = groupByVoter(data, voterId); + return processVoteProposals(groupedProposals); + }); +}; + + + + export const getStaticProps = async () => { const errorProps = { props: {}, @@ -442,6 +596,24 @@ export const getStaticProps = async () => { const { events, fallback: eventsFallback } = await getEvents(client); const protocol = await getProtocol(client); + + const query = getOrchestratorsVotingHistory(); + const response = await getCubeData(query, { type: CUBE_TYPE.SERVER }); + + // Log the response to check the structure of the data + + if (!response || !response[0] || !response[0].data) { + return { + props: { + initialVoterData: [], + }, + }; + } + + const data = response[0].data; + + const voterSummaries = getVoterSummaries(data); + if (!orchestrators.data || !events.data || !protocol.data) { return errorProps; } @@ -451,6 +623,7 @@ export const getStaticProps = async () => { events: events.data, protocol: protocol.data, fallback: {}, + initialVoterData:voterSummaries // fallback: { ...fallback, ...eventsFallback }, }; diff --git a/pages/orchestrators.tsx b/pages/orchestrators.tsx index 84d55d7e..721d7b98 100644 --- a/pages/orchestrators.tsx +++ b/pages/orchestrators.tsx @@ -9,6 +9,7 @@ import { Container, Flex, Heading, + Text, } from "@livepeer/design-system"; import { ArrowRightIcon } from "@modulz/radix-icons"; import Head from "next/head"; @@ -18,14 +19,26 @@ import { OrchestratorsQueryResult, ProtocolQueryResult, } from "../apollo"; +import OrchestratorVotingList from "@components/OrchestratorVotingList"; +import { OrchestratorTabs } from "@lib/orchestrartor"; +import { getOrchestratorsVotingHistory } from "cube/queryGenrator"; +import { CUBE_TYPE, getCubeData } from "cube/cube-client"; +import { + Tabs, + TabList, + Tab, + TabPanels, + TabPanel, +} from "@reach/tabs"; type PageProps = { orchestrators: OrchestratorsQueryResult["data"]; protocol: ProtocolQueryResult["data"]; fallback: { [key: string]: EnsIdentity }; + initialVoterData: any }; -const OrchestratorsPage = ({ orchestrators, protocol }: PageProps) => { +const OrchestratorsPage = ({ orchestrators, protocol, initialVoterData }: PageProps) => { return ( <> @@ -48,33 +61,152 @@ const OrchestratorsPage = ({ orchestrators, protocol }: PageProps) => { {(process.env.NEXT_PUBLIC_NETWORK == "MAINNET" || process.env.NEXT_PUBLIC_NETWORK == "ARBITRUM_ONE") && ( - - - - )} + + + + )} - - - + + {({ selectedIndex, focusedIndex }) => { + let getTabStyle = (index) => ({ + borderBottom: `4px solid ${selectedIndex === index + ? "#6ec08d" + : focusedIndex === index + ? "#141716" + : "#141716" + }` + , + backgroundColor: '#141716', borderWidth: 0, borderBottomWidth: 1, + paddingBottom: 12 + }); + return ( + <> + + + Yield Overview + + + Voting History + + + + + + + + + + + + + + + + ); + }} + ); }; +type VoteProposal = { + "LivepeerVoteProposals.date": string; + "LivepeerVoteProposals.voter": string; + "LivepeerVoteProposals.eventTxnsHash": string; + "LivepeerVoteProposals.voteType": string; + "LivepeerVoteProposals.count": string; + "LivepeerVoteProposals.numOfProposals": string; + "LivepeerVoteProposals.numOfVoteCasted": string; +}; + +type VoterSummary = { + id: string; + noOfProposalsVotedOn: number; + noOfVotesCasted: number; + mostRecentVotes: (string | null)[]; + votingTurnout: number; +}; + + +// Function to get unique voter IDs +const getUniqueVoters = (data: VoteProposal[]): string[] => { + const voterSet = new Set(data.map(proposal => proposal["LivepeerVoteProposals.voter"])); + return Array.from(voterSet); +}; + +// Function to group data by voter +const groupByVoter = (data: VoteProposal[], voterId: string): VoteProposal[] => { + return data.filter(proposal => proposal["LivepeerVoteProposals.voter"] === voterId); +}; + +// Function to process vote proposals and generate voter summary +const processVoteProposals = (proposals: VoteProposal[]): VoterSummary => { + const sortedVotes = proposals.sort((a, b) => + new Date(b["LivepeerVoteProposals.date"]).getTime() - new Date(a["LivepeerVoteProposals.date"]).getTime() + ); + + const mostRecentVotes = sortedVotes.slice(0, 5).map(vote => vote["LivepeerVoteProposals.voteType"] || null); + + const noOfProposalsVotedOn =Number(proposals[0]['LivepeerVoteProposals.numOfProposals'] || 0); + const noOfVotesCasted = Number(proposals[0]['LivepeerVoteProposals.numOfVoteCasted']|| 0); + const votingTurnout = noOfProposalsVotedOn ? noOfVotesCasted / noOfProposalsVotedOn : 0; + + return { + id: proposals[0]["LivepeerVoteProposals.voter"], + noOfProposalsVotedOn, + noOfVotesCasted, + mostRecentVotes, + votingTurnout, + }; +}; + +// Function to get voter summaries for all unique voters +const getVoterSummaries = (data: VoteProposal[]): VoterSummary[] => { + const uniqueVoters = getUniqueVoters(data); + return uniqueVoters.map(voterId => { + const groupedProposals = groupByVoter(data, voterId); + return processVoteProposals(groupedProposals); + }); +}; + + + export const getStaticProps = async () => { try { + + const query = getOrchestratorsVotingHistory(); + const response = await getCubeData(query, { type: CUBE_TYPE.SERVER }); + + // Log the response to check the structure of the data + + if (!response || !response[0] || !response[0].data) { + return { + props: { + initialVoterData: [], + }, + }; + } + + const data = response[0].data; + + const voterSummaries = getVoterSummaries(data); + + const client = getApollo(); const { orchestrators, fallback } = await getOrchestrators(client); const protocol = await getProtocol(client); @@ -83,10 +215,12 @@ export const getStaticProps = async () => { return null; } + const props: PageProps = { orchestrators: orchestrators.data, protocol: protocol.data, fallback, + initialVoterData: voterSummaries }; return { diff --git a/public/img/Vector.png b/public/img/Vector.png new file mode 100644 index 00000000..639d88d5 Binary files /dev/null and b/public/img/Vector.png differ