src/pages/governance/_components/ProposalTable.tsx

Summary

Maintainability
C
7 hrs
Test Coverage
import React, { useState } from "react";
import { useRouter } from "next/router";
import { OverflowTable } from "@components/commons/OverflowTable";
import { AiFillGithub, AiFillRedditCircle } from "react-icons/ai";
import classNames from "classnames";
import { ListProposalsStatus } from "@defichain/jellyfish-api-core/dist/category/governance";
import {
  GovernanceProposal,
  GovernanceProposalStatus,
  GovernanceProposalType,
} from "@defichain/whale-api-client/dist/api/governance";
import { getEnvironment } from "@contexts/Environment";
import { useNetwork } from "@contexts/NetworkContext";
import { Link } from "@components/commons/link/Link";
import {
  isValidOCGGithubUrl,
  isValidOCGRedditUrl,
} from "utils/commons/LinkValidator";
import { NumericFormat } from "react-number-format";
import BigNumber from "bignumber.js";
// import { HoverPopover } from "@components/commons/popover/HoverPopover";
import { ProposalDisplayName } from "./ProposalCard";
import { VoteModal } from "./VoteModal";
import { useCycleEndDate } from "../shared/useCycleEndTime";
import { OnChainGovernanceTitles } from "../enum/onChainGovernanceTitles";
import { getSecondsPerBlock } from "../shared/getSecondsPerBlock";
import { EmergencyChip } from "./EmergencyChip";
import { Progress } from "./VotingResult";
import { getVotePercentage } from "../shared/getTotalVotes";
import { ProposalsVotes } from "../index.page";
import { VoteCount } from "../shared/getVoteCount";

export function ProposalTable({
  proposals,
  proposalsVotes,
  currentBlockHeight,
  currentBlockMedianTime,
  userQueryProposalStatus,
}: {
  proposals: GovernanceProposal[];
  proposalsVotes: ProposalsVotes;
  currentBlockHeight: number;
  currentBlockMedianTime: number;
  userQueryProposalStatus: ListProposalsStatus;
}) {
  const [displayVoteModal, setDisplayVoteModal] = useState(false);

  return (
    <div>
      <OverflowTable
        className={classNames({
          "rounded-b-none": proposals === null || proposals.length === 0,
        })}
      >
        <OverflowTable.Header>
          <OverflowTable.Head
            title={OnChainGovernanceTitles.NameOfProposalTitle}
          />
          <OverflowTable.Head title={OnChainGovernanceTitles.TypeTitle} />
          <OverflowTable.Head title={OnChainGovernanceTitles.RequestedAmount} />
          <OverflowTable.Head title={OnChainGovernanceTitles.TransactionId} />
          <OverflowTable.Head title={OnChainGovernanceTitles.EndOfVoting} />
          {userQueryProposalStatus === ListProposalsStatus.VOTING && (
            <OverflowTable.Head title={OnChainGovernanceTitles.CurrentVotes} />
          )}
          {(userQueryProposalStatus === ListProposalsStatus.COMPLETED ||
            userQueryProposalStatus === ListProposalsStatus.REJECTED) && (
            <OverflowTable.Head title={OnChainGovernanceTitles.Result} />
          )}
        </OverflowTable.Header>

        {proposals.map((proposal: GovernanceProposal) => (
          <Link
            href={{ pathname: `/governance/${proposal.proposalId}` }}
            key={proposal.proposalId}
          >
            <a className="contents">
              <ProposalRow
                proposal={proposal}
                votes={proposalsVotes[proposal.proposalId]}
                currentBlockHeight={currentBlockHeight}
                currentBlockMedianTime={currentBlockMedianTime}
                userQueryProposalStatus={userQueryProposalStatus}
              />
              {displayVoteModal && (
                <VoteModal
                  proposalId={proposal.proposalId}
                  onClose={() => {
                    setDisplayVoteModal(false);
                  }}
                />
              )}
            </a>
          </Link>
        ))}
      </OverflowTable>
    </div>
  );
}

function ProposalRow({
  proposal,
  votes,
  currentBlockHeight,
  currentBlockMedianTime,
  userQueryProposalStatus,
}: {
  proposal: GovernanceProposal;
  votes: VoteCount;
  currentBlockHeight: number;
  currentBlockMedianTime: number;
  userQueryProposalStatus: ListProposalsStatus;
}) {
  const router = useRouter();
  const { connection } = useNetwork();
  const secondsPerBlock = getSecondsPerBlock(connection);
  const cycleEndDate = useCycleEndDate(
    proposal.cycleEndHeight,
    currentBlockHeight,
    currentBlockMedianTime,
    secondsPerBlock
  );
  const isEmergencyProposal = proposal.options?.includes("emergency");
  const { percYes, percNo } = getVotePercentage(
    votes.yes,
    votes.no,
    votes.neutral
  );

  return (
    <OverflowTable.Row
      onClick={() => {
        router.push({
          pathname: `/governance/${proposal.proposalId}`,
          query: getEnvironment().isDefaultConnection(connection)
            ? {}
            : { network: connection },
        });
      }}
      className={classNames(
        "hover:text-primary-500 dark:hover:text-gray-100 cursor-pointer"
      )}
    >
      <OverflowTable.Cell className="align-middle font-semibold text-gray-900 dark:text-gray-100 w-[320px]">
        {isEmergencyProposal && (
          <EmergencyChip
            wrapperClassName="py-1"
            className="text-[10px] leading-3"
          />
        )}
        <div className="line-clamp-2 text-gray-900 dark:text-dark-gray-900">
          {proposal.title}
        </div>

        <a
          href={proposal.context}
          target="_blank"
          onClick={(e) => {
            e.stopPropagation();
          }}
          rel="noreferrer"
        >
          {isValidOCGGithubUrl(proposal.context) ? (
            <>
              <AiFillGithub
                size={24}
                className="text-gray-900 dark:text-dark-gray-900"
              />
            </>
          ) : isValidOCGRedditUrl(proposal.context) ? (
            <>
              <AiFillRedditCircle
                size={24}
                className="text-gray-900 dark:text-dark-gray-900"
              />
            </>
          ) : (
            <span className="pl-1">{OnChainGovernanceTitles.Link}</span>
          )}
        </a>
      </OverflowTable.Cell>
      <OverflowTable.Cell className="align-middle text-gray-900 dark:text-dark-gray-900">
        {ProposalDisplayName[proposal.type]}
      </OverflowTable.Cell>
      <OverflowTable.Cell className="align-middle text-gray-900 dark:text-dark-gray-900">
        {proposal.type === GovernanceProposalType.VOTE_OF_CONFIDENCE ? (
          "N/A"
        ) : (
          <NumericFormat
            value={new BigNumber(proposal.amount ?? 0).toFixed(2)}
            thousandSeparator=","
            displayType="text"
          />
        )}
      </OverflowTable.Cell>
      <OverflowTable.Cell className="align-middle break-all">
        <Link
          href={{ pathname: `/transactions/${proposal.proposalId}` }}
          passHref
        >
          <a
            href={`/transactions/${proposal.proposalId}`}
            className="flex flex-row items-center gap-x-2 text-blue-500 hover:underline line-clamp-2"
            onClick={(e) => {
              e.stopPropagation();
            }}
          >
            {proposal.proposalId}
          </a>
        </Link>
      </OverflowTable.Cell>
      <OverflowTable.Cell className="align-middle">
        <div className="flex flex-col w-max">
          <Link
            href={{
              pathname:
                userQueryProposalStatus === ListProposalsStatus.VOTING
                  ? `/blocks/countdown/${proposal.cycleEndHeight}`
                  : `/blocks/${proposal.cycleEndHeight}`,
            }}
            passHref
          >
            <a
              className="flex flex-row items-center gap-x-2 text-blue-500 hover:underline"
              onClick={(e) => {
                e.stopPropagation();
              }}
              href={
                userQueryProposalStatus === ListProposalsStatus.VOTING
                  ? `/blocks/countdown/${proposal.cycleEndHeight}`
                  : `/blocks/${proposal.cycleEndHeight}`
              }
            >
              {`Block ${proposal.cycleEndHeight}`}
            </a>
          </Link>
          <div className="text-gray-600 dark:text-dark-gray-600 text-xs break-words">
            {cycleEndDate}
          </div>
        </div>
      </OverflowTable.Cell>
      {userQueryProposalStatus === ListProposalsStatus.VOTING && (
        <OverflowTable.Cell className="align-middle">
          {/* <HoverPopover
            className="cursor-pointer group"
            popover={<VotePopover proposal={proposal} votes={votes} />}
            placement="top"
          > */}
          <div className="flex flex-col w-48">
            <Progress
              yesValue={percYes.toNumber()}
              noValue={percNo.toNumber()}
              approvalThreshold={Number(
                proposal.approvalThreshold.replace("%", "")
              )}
              containerClass="bg-gray-100 dark:bg-dark-gray-200"
            />
            <div className="flex flex-row pt-2">
              <div className="flex flex-col grow">
                <span
                  className={classNames(
                    percYes > percNo
                      ? "text-green-600 dark:text-[#21E529] font-semibold"
                      : "text-gray-900 dark:text-dark-gray-900"
                  )}
                >
                  {percYes.toFixed(2)}%
                </span>
              </div>
              <div className="flex flex-col">
                <span
                  className={classNames(
                    "grow text-right",
                    percNo > percYes
                      ? "text-red-600 dark:text-[#FF483D] font-semibold"
                      : "text-gray-900 dark:text-dark-gray-900"
                  )}
                >
                  {percNo.toFixed(2)}%
                </span>
              </div>
            </div>
          </div>
          {/* </HoverPopover> */}
        </OverflowTable.Cell>
      )}

      {(userQueryProposalStatus === ListProposalsStatus.COMPLETED ||
        userQueryProposalStatus === ListProposalsStatus.REJECTED) && (
        <OverflowTable.Cell className="align-middle">
          <div
            className={classNames(
              "py-1 px-3 rounded-[32px] w-fit",
              proposal.status === GovernanceProposalStatus.COMPLETED
                ? "bg-green-100 text-green-600 dark:bg-[#21E529] dark:text-dark-green-600 dark:bg-opacity-25"
                : "bg-red-100 text-red-600 dark:bg-[#FF483D] dark:text-dark-red-600 dark:bg-opacity-20"
            )}
          >
            {proposal.status === GovernanceProposalStatus.COMPLETED
              ? "Approved"
              : proposal.status}
          </div>
        </OverflowTable.Cell>
      )}
    </OverflowTable.Row>
  );
}