src/pages/governance/_components/ProposalCard.tsx

Summary

Maintainability
B
7 hrs
Test Coverage
import React, { useState } from "react";
import classNames from "classnames";
import { AiFillGithub, AiFillRedditCircle } from "react-icons/ai";
import {
  MdOutlineKeyboardArrowUp,
  MdOutlineKeyboardArrowDown,
} from "react-icons/md";
import { ListProposalsStatus } from "@defichain/jellyfish-api-core/dist/category/governance";
import {
  GovernanceProposal,
  GovernanceProposalStatus,
  GovernanceProposalType,
} from "@defichain/whale-api-client/dist/api/governance";
import { Link } from "@components/commons/link/Link";
import { useNetwork } from "@contexts/NetworkContext";
import {
  isValidOCGGithubUrl,
  isValidOCGRedditUrl,
} from "utils/commons/LinkValidator";
import { NumericFormat } from "react-number-format";
import BigNumber from "bignumber.js";
import { useCycleEndDate } from "../shared/useCycleEndTime";
import { OnChainGovernanceTitles } from "../enum/onChainGovernanceTitles";
import { getSecondsPerBlock } from "../shared/getSecondsPerBlock";
import { EmergencyChip } from "./EmergencyChip";
import { VotingResultMobile } from "./VotingResult";
import { ProposalsVotes } from "../index.page";
import { VoteCount } from "../shared/getVoteCount";

export function ProposalCards({
  proposals,
  proposalsVotes,
  currentBlockHeight,
  currentBlockMedianTime,
  userQueryProposalStatus,
}: {
  proposals: GovernanceProposal[];
  proposalsVotes: ProposalsVotes;
  currentBlockHeight: number;
  currentBlockMedianTime: number;
  userQueryProposalStatus: ListProposalsStatus;
}) {
  return (
    <>
      {proposals.map((proposal: GovernanceProposal, index) => (
        <React.Fragment key={index}>
          <ProposalCard
            proposal={proposal}
            votes={proposalsVotes[proposal.proposalId]}
            currentBlockHeight={currentBlockHeight}
            currentBlockMedianTime={currentBlockMedianTime}
            userQueryProposalStatus={userQueryProposalStatus}
          />
        </React.Fragment>
      ))}
    </>
  );
}

export enum ProposalDisplayName {
  CommunityFundProposal = "CFP",
  VoteOfConfidence = "DFIP",
}

function ProposalCard({
  proposal,
  votes,
  currentBlockHeight,
  currentBlockMedianTime,
  userQueryProposalStatus,
}: {
  proposal: GovernanceProposal;
  votes: VoteCount;
  currentBlockHeight: number;
  currentBlockMedianTime: number;
  userQueryProposalStatus: ListProposalsStatus;
}) {
  const [isViewClicked, setIsViewClicked] = useState(false);
  const [isOpen, setIsOpen] = useState(false);
  const { connection } = useNetwork();
  const secondsPerBlock = getSecondsPerBlock(connection);
  const cycleEndDate = useCycleEndDate(
    proposal.cycleEndHeight,
    currentBlockHeight,
    currentBlockMedianTime,
    secondsPerBlock
  );
  const isEmergencyProposal = proposal.options?.includes("emergency");

  return (
    <div
      className={classNames(
        "border rounded-xl mt-2 border-gray-200 text-gray-500 dark:border-gray-700 dark:bg-gray-800"
      )}
    >
      {/* mobile */}
      <div className="group md:hidden block">
        <div className={classNames("grid py-4 px-4 gap-y-3")}>
          <div className="flex flex-row items-center align-middle w-full gap-x-2">
            <div className="grow">
              {isEmergencyProposal && (
                <EmergencyChip
                  wrapperClassName="py-1"
                  className="text-[10px] leading-3"
                />
              )}
              <div
                className={classNames(
                  "font-semibold text-gray-900 text-sm dark:text-dark-gray-900",
                  { "line-clamp-2": !isOpen }
                )}
              >
                {proposal.title}
              </div>
            </div>
            <div className="flex flex-row gap-x-2">
              <Link
                href={{
                  pathname: `/governance/${proposal.proposalId}`,
                }}
              >
                <a className="contents">
                  <div
                    data-testid="OnChainGovernance.CardView.ViewButton"
                    onClick={() => setIsViewClicked(!isViewClicked)}
                    className={classNames(
                      "border-[0.5px] border-primary-300 rounded text-primary-500 dark:text-dark-primary-500 dark:bg-gray-900 dark:border-dark-primary-300 px-1.5 py-1 text-sm h-min",
                      {
                        "bg-primary-100 dark:bg-dark-primary-100":
                          isViewClicked,
                      }
                    )}
                  >
                    VIEW
                  </div>
                </a>
              </Link>
              <div
                className="text-primary-500 cursor-pointer dark:bg-gray-900 dark:text-dark-primary-500 border-[0.5px] border-primary-300 dark:border-dark-primary-300 rounded h-min"
                onClick={() => setIsOpen(!isOpen)}
                data-testid="OnChainGovernance.CardView.Toggle"
              >
                {!isOpen ? (
                  <MdOutlineKeyboardArrowDown size={28} />
                ) : (
                  <MdOutlineKeyboardArrowUp size={28} />
                )}
              </div>
            </div>
          </div>

          {isOpen && (
            <>
              {proposal.status === GovernanceProposalStatus.VOTING && (
                <div className="flex flex-row align-middle">
                  <VotingResultMobile proposal={proposal} voteCounts={votes} />
                </div>
              )}

              <div className="flex flex-row align-middle">
                <div className="text-sm text-gray-500 grow dark:text-dark-gray-500">
                  {OnChainGovernanceTitles.Discussion}
                </div>
                <a
                  href={proposal.context}
                  target="_blank"
                  onClick={(e) => {
                    e.stopPropagation();
                  }}
                  rel="noreferrer"
                >
                  <div className="flex flex-row font-semibold items-center gap-x-1 text-sm text-gray-900 dark:text-dark-gray-900">
                    {isValidOCGGithubUrl(proposal.context) ? (
                      <>
                        <AiFillGithub size={20} />
                        {OnChainGovernanceTitles.Github}
                      </>
                    ) : isValidOCGRedditUrl(proposal.context) ? (
                      <>
                        <AiFillRedditCircle size={20} />
                        {OnChainGovernanceTitles.Reddit}
                      </>
                    ) : (
                      OnChainGovernanceTitles.Link
                    )}
                  </div>
                </a>
              </div>
              <div className="flex flex-row align-middle">
                <div className="text-sm text-gray-500 grow dark:text-dark-gray-500">
                  {OnChainGovernanceTitles.TypeTitle}
                </div>
                <div className="text-gray-900 text-sm dark:text-dark-gray-900">
                  {ProposalDisplayName[proposal.type]}
                </div>
              </div>

              <div className="flex flex-row align-middle">
                <div className="text-sm text-gray-500 grow dark:text-dark-gray-500">
                  {OnChainGovernanceTitles.RequestedAmount}
                </div>
                <div className="text-gray-900 text-sm 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"
                    />
                  )}
                </div>
              </div>

              <div className="flex flex-row align-middle">
                <div className="text-sm text-gray-500 grow dark:text-dark-gray-500">
                  {OnChainGovernanceTitles.TransactionId}
                </div>
                <Link
                  href={{ pathname: `/transactions/${proposal.proposalId}` }}
                  passHref
                >
                  <a
                    href={`/transactions/${proposal.proposalId}`}
                    className="flex flex-row items-center gap-x-2 text-right text-blue-500 hover:underline break-all text-sm w-[146px]"
                    onClick={(e) => {
                      e.stopPropagation();
                    }}
                  >
                    {proposal.proposalId}
                  </a>
                </Link>
              </div>

              <div className="flex flex-row align-middle">
                <div className="text-sm text-gray-500 grow dark:text-dark-gray-500">
                  {OnChainGovernanceTitles.EndOfVoting}
                </div>
                <div className="flex flex-col w-6/12">
                  <Link
                    href={{
                      pathname:
                        userQueryProposalStatus === ListProposalsStatus.VOTING
                          ? `/blocks/countdown/${proposal.cycleEndHeight}`
                          : `/blocks/${proposal.cycleEndHeight}`,
                    }}
                    passHref
                  >
                    <a
                      className="flex flex-row items-center text-blue-500 hover:underline text-sm w-full justify-end"
                      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 text-xs text-right dark:text-dark-gray-600">
                    {cycleEndDate}
                  </div>
                </div>
              </div>

              {(userQueryProposalStatus === ListProposalsStatus.COMPLETED ||
                userQueryProposalStatus === ListProposalsStatus.REJECTED) && (
                <div className="flex flex-row align-middle">
                  <div className="text-sm text-gray-500 grow dark:text-dark-gray-500">
                    {OnChainGovernanceTitles.Result}
                  </div>
                  <div
                    className={classNames(
                      "text-sm",
                      proposal.status === GovernanceProposalStatus.COMPLETED
                        ? "text-green-600 dark:text-dark-green-600"
                        : "text-red-600 dark:text-dark-red-600"
                    )}
                  >
                    {proposal.status === GovernanceProposalStatus.COMPLETED
                      ? "Approved"
                      : proposal.status}
                  </div>
                </div>
              )}
            </>
          )}
        </div>
      </div>
    </div>
  );
}