src/pages/governance/[proposalId]/index.page.tsx

Summary

Maintainability
B
5 hrs
Test Coverage
File `index.page.tsx` has 354 lines of code (exceeds 250 allowed). Consider refactoring.
import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
import { Container } from "@components/commons/Container";
import { getWhaleApiClient } from "@contexts/WhaleContext";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
import {
VoteDecision,
MasternodeType,
} from "@defichain/jellyfish-api-core/dist/category/governance";
import {
GovernanceProposalType,
ProposalVotesResult,
} from "@defichain/whale-api-client/dist/api/governance";
import { ApiPagedResponse } from "@defichain/whale-api-client";
import * as LosslessJSON from "lossless-json";
import { Head } from "@components/commons/Head";
import { Breadcrumb } from "@components/commons/Breadcrumb";
import classNames from "classnames";
import { EmptySection } from "@components/commons/sections/EmptySection";
import {
CursorPage,
CursorPagination,
} from "@components/commons/CursorPagination";
import { useNetwork } from "@contexts/NetworkContext";
import { getVoteCount } from "../shared/getVoteCount";
import { VoteCards, VotesTable } from "../_components/VotesTable";
import { VotingResult } from "../_components/VotingResult";
import { ProposalDetail } from "../_components/ProposalDetail";
import { ConfirmVoteDialog } from "../_components/ConfirmVoteDialog";
import { getSecondsPerBlock } from "../shared/getSecondsPerBlock";
import { formatMedianTime } from "../shared/dateHelper";
import { VoteStages } from "../enum/VoteStages";
import { TotalVotesCards } from "../_components/TotalVotesCards";
import { CfpVotingResultCycleTab } from "../enum/CfpVotingResultCycleTab";
import { useCycleEndDate } from "../shared/useCycleEndTime";
 
Function `ProposalDetailPage` has a Cognitive Complexity of 8 (exceeds 5 allowed). Consider refactoring.
export default function ProposalDetailPage({
proposal,
proposalVotes,
proposalCreationDate,
currentBlockHeight,
currentBlockMedianTime,
totalVotes,
pages,
voteCounts,
}: InferGetServerSidePropsType<typeof getServerSideProps>): JSX.Element {
const { connection } = useNetwork();
const secondsPerBlock = getSecondsPerBlock(connection);
const cycleEndDate = useCycleEndDate(
proposal.cycleEndHeight,
currentBlockHeight,
currentBlockMedianTime,
secondsPerBlock
);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [isChangeVoteClicked, setIsChangeVoteClicked] = useState(false);
const [userConfirmedSelectedVote, setUserConfirmedSelectedVote] =
useState<VoteDecision>();
const [voteCommand, setVoteCommand] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [voteStage, setVoteStage] = useState<VoteStages>(
VoteStages.VoteProposal
);
 
const [cfpVotingResultTabChoice, setCfpVotingResultTabChoice] = useState(
CfpVotingResultCycleTab.Current
);
 
useEffect(() => {
if (isChangeVoteClicked) {
setIsChangeVoteClicked(false);
setVoteStage(VoteStages.VoteProposal);
}
}, [isChangeVoteClicked]);
 
return (
<>
<Head title="Proposal Details" />
<Container className="mt-[40px] md:mt-[44px]">
<Breadcrumb
items={[
{
path: "/governance",
name: "Proposal",
},
{
path: `/governance/${proposal.proposalId}`,
name: "Proposal Details",
canonical: true,
isCurrentPath: true,
},
]}
/>
<div className="flex flex-col lg:mt-6 md:mt-4 mt-2 lg:flex-row space-y-6 lg:space-y-0 space-x-0 lg:space-x-6">
<div className="w-full lg:w-8/12">
<ProposalDetail
proposal={proposal}
proposalCreationDate={formatMedianTime(
proposalCreationDate,
"MMM dd, yyyy"
)}
proposalEndDate={cycleEndDate}
/>
{proposal.type === GovernanceProposalType.COMMUNITY_FUND_PROPOSAL &&
proposal.totalCycles > 1 && (
<div className="hidden lg:block mt-6">
<CfpVotingResultFilterTab
cfpVotingResultTabChoice={cfpVotingResultTabChoice}
setCfpVotingResultTabChoice={setCfpVotingResultTabChoice}
currenCycle={proposal.currentCycle}
/>
</div>
)}
 
{cfpVotingResultTabChoice === CfpVotingResultCycleTab.Current ||
proposal.type === GovernanceProposalType.VOTE_OF_CONFIDENCE ? (
<div
className={classNames(
"hidden lg:block",
proposal.type ===
GovernanceProposalType.COMMUNITY_FUND_PROPOSAL
? "mt-4"
: "mt-6"
)}
>
<VotesList
proposalVotes={proposalVotes}
pages={pages}
proposalId={proposal.proposalId}
/>
</div>
) : (
<div className="hidden lg:block mt-4">
<TotalVotesCards totalVotes={totalVotes} proposal={proposal} />
</div>
)}
</div>
<div className="w-full lg:w-4/12">
<VotingResult
proposal={proposal}
userSelectedVote={userConfirmedSelectedVote}
voteCommand={voteCommand}
isLoading={isLoading}
setIsChangeVoteClicked={setIsChangeVoteClicked}
voteCounts={voteCounts}
status={proposal.status}
onSubmitVote={() => {
setIsDialogOpen(true);
}}
/>
</div>
 
<div className="space-y-0">
<div
className={classNames("flex flex-row mb-2 md:mb-4 items-center", {
"md:mb-0":
proposal.type === GovernanceProposalType.VOTE_OF_CONFIDENCE,
})}
>
<div className="grow md:hidden ml-4 ">
<span className="text-gray-900 dark:text-gray-100 font-semibold">
Votes
</span>
</div>
 
{proposal.type ===
GovernanceProposalType.COMMUNITY_FUND_PROPOSAL &&
proposal.totalCycles > 1 && (
<div className="block lg:hidden">
<CfpVotingResultFilterTab
cfpVotingResultTabChoice={cfpVotingResultTabChoice}
setCfpVotingResultTabChoice={setCfpVotingResultTabChoice}
currenCycle={proposal.currentCycle}
/>
</div>
)}
</div>
{cfpVotingResultTabChoice === CfpVotingResultCycleTab.Current ||
proposal.type === GovernanceProposalType.VOTE_OF_CONFIDENCE ? (
<div className="lg:hidden w-full">
<VotesList
proposalVotes={proposalVotes}
pages={pages}
proposalId={proposal.proposalId}
/>
</div>
) : (
<div className="lg:hidden items-center">
<TotalVotesCards totalVotes={totalVotes} proposal={proposal} />
</div>
)}
</div>
</div>
</Container>
<ConfirmVoteDialog
voteStage={voteStage}
setVoteStage={setVoteStage}
setUserConfirmedSelectedVote={setUserConfirmedSelectedVote}
userConfirmedSelectedVote={userConfirmedSelectedVote}
setVoteCommand={setVoteCommand}
voteCommand={voteCommand}
isLoading={isLoading}
setIsLoading={setIsLoading}
proposalId={proposal.proposalId}
isOpen={isDialogOpen}
onClose={() => setIsDialogOpen(false)}
/>
</>
);
}
 
function VotesList({
proposalVotes,
pages,
proposalId,
}: {
proposalVotes: ApiPagedResponse<ProposalVotesResult>;
pages: CursorPage[];
proposalId: string;
}): JSX.Element {
return (
<>
{proposalVotes.length === 0 ? (
<EmptySection message="No votes posted yet" className="mt-0" />
) : (
<>
<div className="md:hidden">
<VoteCards votes={proposalVotes} />
</div>
<div className="hidden md:block">
<VotesTable votes={proposalVotes} />
</div>
</>
)}
 
<div className="flex justify-end mt-8">
<CursorPagination pages={pages} path={`/governance/${proposalId}`} />
</div>
</>
);
}
 
function CfpVotingResultFilterTab({
setCfpVotingResultTabChoice,
cfpVotingResultTabChoice,
currenCycle,
}: {
setCfpVotingResultTabChoice: Dispatch<
SetStateAction<CfpVotingResultCycleTab>
>;
cfpVotingResultTabChoice: CfpVotingResultCycleTab;
currenCycle: number;
}) {
return (
<div className="flex flex-row">
<button
type="button"
data-testid="OnChainGovernance.VotingFlow.NoVote"
className={classNames(
"md:px-5 rounded-l border border-r-[0.5px] px-3 py-[6px] md:text-sm text-xs font-medium text-gray-500",
cfpVotingResultTabChoice === CfpVotingResultCycleTab.Current
? "border-transparent bg-primary-500 dark:bg-dark-primary-500 text-white dark:text-dark-gray-0"
: "dark:border-dark-gray-300 dark:text-dark-gray-900 dark:bg-dark-gray-200 border-gray-300"
)}
onClick={() => {
setCfpVotingResultTabChoice(CfpVotingResultCycleTab.Current);
}}
>
<span className="hidden md:block">Current cycle</span>
<span className="md:hidden">Current</span>
</button>
<button
type="button"
data-testid="OnChainGovernance.VotingFlow.YesVote"
disabled={currenCycle === 1}
className={classNames(
"md:px-5 border border-l-[0.5px] rounded-r px-3 py-[6px] md:text-sm text-xs font-medium text-gray-500 tracking-[0.0025em] disabled:text-gray-500 disabled:border-gray-200 disabled:opacity-30",
cfpVotingResultTabChoice === CfpVotingResultCycleTab.Previous
? "border-transparent bg-primary-500 dark:bg-dark-primary-500 text-white dark:text-dark-gray-0"
: "dark:border-dark-gray-300 dark:text-dark-gray-900 dark:bg-dark-gray-200 border-gray-300"
)}
onClick={() => {
setCfpVotingResultTabChoice(CfpVotingResultCycleTab.Previous);
}}
>
<span className="hidden md:block">Previous cycle</span>
<span className="md:hidden">Previous</span>
</button>
</div>
);
}
 
function getAllCycleVotes(
allCycleProposalVotes: ApiPagedResponse<ProposalVotesResult>
): {} {
const totalVotes = {};
for (let i = 0; i < allCycleProposalVotes.length; i += 1) {
const vote = allCycleProposalVotes[i];
const voteCycle = vote.cycle;
if (voteCycle in totalVotes) {
totalVotes[voteCycle] += 1;
} else {
totalVotes[voteCycle] = 1;
}
}
return totalVotes;
}
 
export async function getServerSideProps(context: GetServerSidePropsContext) {
const proposalId = context.params?.proposalId?.toString().trim() as string;
const api = getWhaleApiClient(context);
const next = CursorPagination.getNext(context);
 
try {
const proposal = await api.governance.getGovProposal(proposalId);
if (proposal.amount) {
proposal.amount = LosslessJSON.parse(
LosslessJSON.stringify(proposal.amount)
);
}
const proposalVotes = await api.governance.listGovProposalVotes({
id: proposalId,
masternode: MasternodeType.ALL,
cycle: proposal.currentCycle,
size: 10,
next: next,
});
const pages = CursorPagination.getPages(
context,
proposalVotes as ApiPagedResponse<any>
);
 
const currentBlockHeight = await api.blocks
.list(1)
.then((blocks) => blocks[0].height);
const currentBlockMedianTime = await api.blocks
.get(currentBlockHeight.toString())
.then((block) => block.medianTime);
const proposalCreationDate = await api.blocks
.get(proposal.creationHeight.toString())
.then((block) => block.medianTime);
 
// All votes to get statistics breakdown
const allCycleProposalVotes = await api.governance.listGovProposalVotes({
id: proposalId,
masternode: MasternodeType.ALL,
cycle: -1,
all: true,
});
 
const totalVotes = getAllCycleVotes(allCycleProposalVotes);
// calculate stats (yes/no/neutral) for current cycle
const voteCounts = getVoteCount(
allCycleProposalVotes.filter(
(each) => each.cycle === proposal.currentCycle
)
);
return {
props: {
proposal,
proposalVotes,
proposalCreationDate,
currentBlockHeight,
currentBlockMedianTime,
totalVotes,
pages,
voteCounts,
},
};
} catch {
return { notFound: true };
}
}