From f019f8f0c18f4310c49e17539374f6b6eee584a5 Mon Sep 17 00:00:00 2001 From: "tai.truong" Date: Mon, 15 Apr 2024 10:05:51 +0700 Subject: [PATCH] feat: MET-2061 add flag conway era --- docker-compose.yml | 1 + env.global.tmp.js | 1 + src/Routers.tsx | 2 + src/commons/menus.ts | 1 + src/commons/routers.ts | 1 + src/commons/utils/api.ts | 5 +- src/commons/utils/constants.ts | 2 + .../DelegationPool/DelegationList/index.tsx | 21 +- .../Dreps/DrepsList/DelegationList.test.tsx | 50 ++++ src/components/Dreps/DrepsList/index.tsx | 163 +++++++++++++ src/components/Dreps/DrepsList/styles.ts | 137 +++++++++++ .../DrepsOverview/DelegationOverview.test.tsx | 49 ++++ src/components/Dreps/DrepsOverview/index.tsx | 227 ++++++++++++++++++ src/components/Dreps/DrepsOverview/styles.ts | 114 +++++++++ .../Layout/Sidebar/SidebarMenu/index.tsx | 96 ++++---- src/locales/en/translation.json | 13 + src/pages/DelegationDetail/index.tsx | 9 +- src/pages/DrepDetail/index.tsx | 7 +- src/pages/Dreps/Dreps.test.tsx | 76 ++++++ src/pages/Dreps/index.tsx | 26 ++ src/pages/Dreps/styles.ts | 14 ++ src/types/drep.d.ts | 31 +++ vite.config.ts | 3 +- 23 files changed, 995 insertions(+), 54 deletions(-) create mode 100644 src/components/Dreps/DrepsList/DelegationList.test.tsx create mode 100644 src/components/Dreps/DrepsList/index.tsx create mode 100644 src/components/Dreps/DrepsList/styles.ts create mode 100644 src/components/Dreps/DrepsOverview/DelegationOverview.test.tsx create mode 100644 src/components/Dreps/DrepsOverview/index.tsx create mode 100644 src/components/Dreps/DrepsOverview/styles.ts create mode 100644 src/pages/Dreps/Dreps.test.tsx create mode 100644 src/pages/Dreps/index.tsx create mode 100644 src/pages/Dreps/styles.ts diff --git a/docker-compose.yml b/docker-compose.yml index c189a2bf88..b5cf6f90f6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,5 +26,6 @@ services: - REACT_APP_ADA_HANDLE_API=${REACT_APP_ADA_HANDLE_API} - REACT_APP_BOLNISI_NAME_API=${REACT_APP_BOLNISI_NAME_API} - REACT_APP_API_URL_COIN_GECKO=${REACT_APP_API_URL_COIN_GECKO} + - REACT_APP_FF_GLOBAL_IS_CONWAY_ERA=${REACT_APP_FF_GLOBAL_IS_CONWAY_ERA} ports: - "${PORT}:80" diff --git a/env.global.tmp.js b/env.global.tmp.js index 2b2c9f9838..70866ba03b 100644 --- a/env.global.tmp.js +++ b/env.global.tmp.js @@ -20,3 +20,4 @@ window.env.REACT_APP_CARDANO_NEWS_URL = `$REACT_APP_CARDANO_NEWS_URL`; window.env.REACT_APP_ADA_HANDLE_API = `$REACT_APP_ADA_HANDLE_API`; window.env.REACT_APP_BOLNISI_NAME_API = `$REACT_APP_BOLNISI_NAME_API`; window.env.REACT_APP_API_URL_COIN_GECKO = `$REACT_APP_API_URL_COIN_GECKO`; +window.env.REACT_APP_FF_GLOBAL_IS_CONWAY_ERA = `$REACT_APP_FF_GLOBAL_IS_CONWAY_ERA`; diff --git a/src/Routers.tsx b/src/Routers.tsx index 5c2f7c1a9f..45b9f20883 100644 --- a/src/Routers.tsx +++ b/src/Routers.tsx @@ -52,6 +52,7 @@ import TransactionList from "./pages/TransactionList"; import VerifyEmail from "./pages/VerifyEmail"; import NativeScriptsDetailPage from "./pages/NativeScriptDetail"; import DrepDetail from "./pages/DrepDetail"; +import Dreps from "./pages/Dreps"; const StakeAddressRegistration = () => ; const StakeAddressDeregistration = () => ; @@ -88,6 +89,7 @@ const Routes: React.FC = () => { + diff --git a/src/commons/menus.ts b/src/commons/menus.ts index 2b61f1b95a..3c89621de0 100644 --- a/src/commons/menus.ts +++ b/src/commons/menus.ts @@ -49,6 +49,7 @@ export const menus: Menu[] = [ href: details.nativeScriptsAndSC() }, { title: "Pools", key: "head.page.pool", href: routers.DELEGATION_POOLS, isSpecialPath: true }, + { title: "Delegated Representatives", key: "head.page.drep", href: routers.DREPS, isSpecialPath: true }, { title: "Top ADA Holders", key: "glossary.topAdaHolder", href: routers.ADDRESS_LIST } ] }, diff --git a/src/commons/routers.ts b/src/commons/routers.ts index 37951fd1fb..d4601e1a3d 100644 --- a/src/commons/routers.ts +++ b/src/commons/routers.ts @@ -12,6 +12,7 @@ export const routers = { EPOCH_LIST: "/epochs", EPOCH_DETAIL: "/epoch/:epochId", DELEGATION_POOLS: "/pools", + DREPS: "/dreps", DELEGATION_POOL_DETAIL: "/pool/:poolId", POOL_CERTIFICATE: "/pool-certificates", POOL_DEREGISTRATION: "/pool-de-registrations", diff --git a/src/commons/utils/api.ts b/src/commons/utils/api.ts index f6119257e4..8f1fcbac87 100644 --- a/src/commons/utils/api.ts +++ b/src/commons/utils/api.ts @@ -40,7 +40,10 @@ export const API = { DREP_OVERVIEW: "dreps/:drepId/drep-details", DREP_OVERVIEW_CHART: "dreps/:drepId/vote-procedure-chart", DREP_DELEGATOR: "dreps/:drepId/get-delegation", - + DREPS_LIST: { + DREPS_LIST: "dreps/filter", + DREPS_OVERVIEW: "dreps/overview" + }, TOKEN: { LIST: "tokens", TOKEN_TRX: "tokens/:tokenId/txs", diff --git a/src/commons/utils/constants.ts b/src/commons/utils/constants.ts index 1d6406018e..6cf3958640 100644 --- a/src/commons/utils/constants.ts +++ b/src/commons/utils/constants.ts @@ -134,6 +134,8 @@ export const EXT_ADA_PRICE_URL = process.env.REACT_APP_EXT_ADA_PRICE_URL || get(window, "env.REACT_APP_EXT_ADA_PRICE_URL"); export const BOLNISI_NAME_API = process.env.REACT_APP_BOLNISI_NAME_API || get(window, "env.REACT_APP_BOLNISI_NAME_API"); export const API_GECKO = process.env.REACT_APP_API_URL_COIN_GECKO || get(window, "env.REACT_APP_API_URL_COIN_GECKO"); +export const IS_CONWAY_ERA = + (process.env.REACT_APP_FF_GLOBAL_IS_CONWAY_ERA || get(window, "env.REACT_APP_FF_GLOBAL_IS_CONWAY_ERA")) === "true"; export enum ACCOUNT_ERROR { UNKNOWN_ERROR = "CC_1", diff --git a/src/components/DelegationPool/DelegationList/index.tsx b/src/components/DelegationPool/DelegationList/index.tsx index c8fd9147c9..0cbdc6847c 100644 --- a/src/components/DelegationPool/DelegationList/index.tsx +++ b/src/components/DelegationPool/DelegationList/index.tsx @@ -15,6 +15,7 @@ import CustomTooltip from "src/components/commons/CustomTooltip"; import Table, { Column } from "src/components/commons/Table"; import CustomIcon from "src/components/commons/CustomIcon"; import usePageInfo from "src/commons/hooks/usePageInfo"; +import { IS_CONWAY_ERA } from "src/commons/utils/constants"; import { AntSwitch, @@ -150,16 +151,23 @@ const DelegationLists: React.FC = () => { } }, { - title: t("votingPower") + " ", + title: t("votingPower"), key: "votingPower", minWidth: "120px", - render: (r) => (r.votingPower != null ? `${r.votingPower}` : t("common.N/A")), + render: (r) => + r.votingPower != null ? ( + + {formatPercent(r.votingPower)} + + ) : ( + t("common.N/A") + ), sort: ({ columnKey, sortValue }) => { sortValue ? setSort(`${columnKey},${sortValue}`) : setSort(""); } }, { - title: t("governanceParticipationRate") + " ", + title: t("governanceParticipationRate"), key: "governanceParticipationRate", minWidth: "120px", render: (r) => @@ -216,7 +224,12 @@ const DelegationLists: React.FC = () => { { + if ((col.key === "governanceParticipationRate" || col.key === "votingPower") && !IS_CONWAY_ERA) { + return false; + } + return true; + })} total={{ count: fetchData.total, title: "Total", isDataOverSize: fetchData.isDataOverSize }} onClickRow={(_, r: Delegators) => history.push(details.delegation(r.poolId), { fromPath })} pagination={{ diff --git a/src/components/Dreps/DrepsList/DelegationList.test.tsx b/src/components/Dreps/DrepsList/DelegationList.test.tsx new file mode 100644 index 0000000000..436776485e --- /dev/null +++ b/src/components/Dreps/DrepsList/DelegationList.test.tsx @@ -0,0 +1,50 @@ +import { Router } from "react-router-dom"; +import { createBrowserHistory } from "history"; + +import { fireEvent, render, screen } from "src/test-utils"; +import useFetchList from "src/commons/hooks/useFetchList"; +import { details } from "src/commons/routers"; + +import DelegationLists from "."; + +const mockData: Delegators = { + poolId: "mock-pool-id", + poolName: "Mock Pool", + poolSize: 100, + reward: 500, + feePercent: 2, + feeAmount: 10, + pledge: 1000, + saturation: 0.8, + stakeLimit: undefined, + numberDelegators: 50, + lifetimeBlock: 10000, + lifetimeRos: 5, + epochBlock: 100 +}; + +jest.mock("src/commons/hooks/useFetchList"); + +describe("DelegationList component", () => { + beforeEach(() => { + (useFetchList as jest.Mock).mockReturnValue({ + data: [mockData] + }); + }); + it("should component render", () => { + render(); + expect(screen.getByTestId("search-icon")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: /mock pool/i })).toBeInTheDocument(); + }); + + it("should goto detail page button clicked", () => { + const history = createBrowserHistory(); + render( + + + + ); + fireEvent.click(screen.getByText(/mock pool/i)); + expect(history.location.pathname).toBe(details.delegation(mockData.poolId)); + }); +}); diff --git a/src/components/Dreps/DrepsList/index.tsx b/src/components/Dreps/DrepsList/index.tsx new file mode 100644 index 0000000000..afc1192d72 --- /dev/null +++ b/src/components/Dreps/DrepsList/index.tsx @@ -0,0 +1,163 @@ +import { Box, Button } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useRef, useState } from "react"; +import { useHistory } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { stringify } from "qs"; + +import useFetchList from "src/commons/hooks/useFetchList"; +import { details } from "src/commons/routers"; +import { API } from "src/commons/utils/api"; +import { formatADAFull, formatDateTimeLocal, formatPercent, getShortHash } from "src/commons/utils/helper"; +import CustomTooltip from "src/components/commons/CustomTooltip"; +import Table, { Column } from "src/components/commons/Table"; +import usePageInfo from "src/commons/hooks/usePageInfo"; +import { StakeKeyStatus } from "src/components/commons/DetailHeader/styles"; +import ADAicon from "src/components/commons/ADAIcon"; +import { ActionMetadataModalConfirm } from "src/components/GovernanceVotes"; + +import { PoolName } from "./styles"; + +const DrepsList: React.FC = () => { + const { t } = useTranslation(); + const history = useHistory(); + const { pageInfo } = usePageInfo(); + const [metadataUrl, setMetadataUrl] = useState(""); + const tableRef = useRef(null); + const blockKey = useSelector(({ system }: RootState) => system.blockKey); + + const fetchData = useFetchList(API.DREPS_LIST.DREPS_LIST, { ...pageInfo }, false, blockKey); + + const columns: Column[] = [ + { + title: t("dreps.id"), + key: "id", + minWidth: "100px", + render: (r) => ( + + + + {`${getShortHash(r.drepId)}`} + + + + ) + }, + { + title: {t("dreps.anchorLink")}, + key: "anchorLink", + minWidth: "100px", + render: (r) => ( + + 20 ? "inline-block" : "inline"} + width={"150px"} + textOverflow={"ellipsis"} + whiteSpace={"nowrap"} + overflow={"hidden"} + color={(theme) => `${theme.palette.primary.main} !important`} + onClick={() => setMetadataUrl(r.anchorUrl)} + disableRipple={true} + sx={{ ":hover": { background: "none" } }} + > + {`${r.anchorUrl || ""}`} + + + ) + }, + { + title: {t("dreps.anchorHash")}, + key: "pu.anchorHash", + minWidth: "120px", + render: (r) => ( + + + {r.anchorHash} + + + ) + }, + { + title: ( + + {t("glossary.activeStake")} () + + ), + key: "pu.activeStake", + minWidth: "120px", + render: (r) => ( + {r.activeVoteStake != null ? formatADAFull(r.activeVoteStake) : t("common.N/A")} + ) + }, + { + title: t("dreps.votingPower"), + minWidth: "120px", + key: "votingPower", + render: (r) => + r.votingPower != null ? ( + + {formatPercent(r.votingPower / 100) || `0%`} + + ) : ( + t("common.N/A") + ) + }, + { + title: t("common.status"), + key: "status", + minWidth: "120px", + render: (r) => ( + + {r.status === "ACTIVE" + ? t("status.active") + : r.status === "INACTIVE" + ? t("status.inActive") + : t("status.retired")} + + ) + }, + { + title: t("dreps.registrationDate"), + minWidth: "100px", + key: "registrationDate", + render: (r) => {formatDateTimeLocal(r.createdAt)} + }, + { + title: t("dreps.lastUpdated"), + key: "lastUpdated", + minWidth: "120px", + render: (r) => {formatDateTimeLocal(r.updatedAt)} + } + ]; + return ( + <> +
history.push(details.drep(r.drepId))} + pagination={{ + ...pageInfo, + total: fetchData.total, + onChange: (page, size) => { + history.replace({ search: stringify({ ...pageInfo, page, size }) }); + tableRef.current?.scrollIntoView(); + } + }} + /> + setMetadataUrl("")} open={!!metadataUrl} anchorUrl={metadataUrl} /> + + ); +}; + +export default DrepsList; diff --git a/src/components/Dreps/DrepsList/styles.ts b/src/components/Dreps/DrepsList/styles.ts new file mode 100644 index 0000000000..c6c5a4df53 --- /dev/null +++ b/src/components/Dreps/DrepsList/styles.ts @@ -0,0 +1,137 @@ +import { styled, Button, LinearProgress, Box, Switch } from "@mui/material"; +import { Link } from "react-router-dom"; + +export const StyledLinearProgress = styled(LinearProgress)<{ saturation: number }>` + display: inline-block; + width: 150px; + height: 8px; + border-radius: 34px; + background: ${(props) => props.theme.palette.primary[200]}; + margin-left: 8px; + & > .MuiLinearProgress-barColorPrimary { + border-radius: 34px; + background: ${({ theme, saturation }) => + saturation > 100 ? theme.palette.error[700] : theme.palette.primary.main}; + } +`; + +export const PoolName = styled(Link)` + font-family: var(--font-family-text) !important; + color: ${({ theme }) => theme.palette.primary.main} !important; +`; + +export const SearchContainer = styled("div")(({ theme }) => ({ + display: "flex", + justifyContent: "start", + alignItems: "center", + width: "100%", + maxWidth: 360, + background: theme.palette.secondary[0], + padding: "0 12px", + borderRadius: 8, + height: 35, + border: `1.5px solid ${theme.mode === "light" ? theme.palette.primary[200] : theme.palette.secondary[700]}`, + "&:focus-within": { + borderColor: theme.palette.secondary.light + }, + [theme.breakpoints.down("sm")]: { + width: "unset", + maxWidth: "unset" + } +})); + +export const StyledInput = styled("input")` + border: none; + width: 100%; + background: ${({ theme }) => theme.palette.secondary[0]}; + color: ${({ theme }) => theme.palette.secondary.main}; + font-size: var(--font-size-text-small); + border-radius: 8px; +`; + +export const SubmitButton = styled(Button)` + display: flex; + justify-content: center; + align-items: center; + border: none; + box-shadow: none; + border-radius: 12.5%; + min-width: 35px; + width: 35px; + height: 35px; +`; +export const Image = styled("img")` + width: 20px; + height: 20px; +`; + +export const TopSearchContainer = styled(Box)` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + ${({ theme }) => theme.breakpoints.down("sm")} { + flex-direction: column; + align-items: flex-start; + gap: 15px; + } +`; + +export const ShowRetiredPools = styled(Box)` + font-size: 14px; + font-weight: 700; + display: flex; + align-items: center; + color: ${({ theme }) => theme.palette.secondary.light}; + gap: 12px; + ${({ theme }) => theme.breakpoints.down("sm")} { + width: 100%; + justify-content: flex-end; + } +`; + +export const AntSwitch = styled(Switch)(({ theme }) => ({ + width: 48, + height: 24, + padding: 0, + display: "flex", + borderRadius: 77, + "&:active": { + "& .MuiSwitch-thumb": { + width: 18 + }, + "& .MuiSwitch-switchBase.Mui-checked": { + transform: "translateX(23px)" + } + }, + "& .MuiSwitch-switchBase": { + padding: 2, + transform: "translateX(2px)", + "&.Mui-checked": { + transform: "translateX(23px)", + color: "#fff", + "& + .MuiSwitch-track": { + opacity: 1, + backgroundColor: theme.palette.primary[200] + }, + "& .MuiSwitch-thumb": { + backgroundColor: theme.palette.primary.main + } + } + }, + "& .MuiSwitch-thumb": { + boxShadow: "0 2px 4px 0 rgb(0 35 11 / 20%)", + width: 19, + height: 19, + borderRadius: 9, + transition: theme.transitions.create(["width", "background"], { + duration: 200 + }) + }, + "& .MuiSwitch-track": { + borderRadius: 19 / 2, + opacity: 1, + backgroundColor: theme.palette.secondary[600], + boxSizing: "border-box" + } +})); diff --git a/src/components/Dreps/DrepsOverview/DelegationOverview.test.tsx b/src/components/Dreps/DrepsOverview/DelegationOverview.test.tsx new file mode 100644 index 0000000000..e2cf670427 --- /dev/null +++ b/src/components/Dreps/DrepsOverview/DelegationOverview.test.tsx @@ -0,0 +1,49 @@ +import moment from "moment"; +import { Router } from "react-router-dom"; +import { createBrowserHistory } from "history"; + +import { fireEvent, render, screen } from "src/test-utils"; +import useFetch from "src/commons/hooks/useFetch"; +import { details } from "src/commons/routers"; + +import OverViews from "."; + +const mockData: OverViewDelegation = { + countDownEndTime: 1626088800, + delegators: 1000, + epochNo: 50, + epochSlotNo: 500, + liveStake: 1000000, + activePools: 200, + retiredPools: 50 +}; + +jest.mock("src/commons/hooks/useFetch"); + +describe("DelegationOverview component", () => { + beforeEach(() => { + (useFetch as jest.Mock).mockReturnValue({ + data: mockData, + loading: false, + lastUpdated: moment(new Date()).format("DD/MM/YYYY") + }); + }); + it("should component render", () => { + render(); + expect(screen.getByRole("heading", { name: "Pools" })).toBeInTheDocument(); + expect(screen.getByText(/delegators/i)).toBeInTheDocument(); + expect(screen.getByText(/active pools/i)).toBeInTheDocument(); + }); + + it("should app go to detail page ", () => { + const history = createBrowserHistory(); + render( + + + + ); + + fireEvent.click(screen.getAllByRole("link", { name: /50/i })[0]); + expect(history.location.pathname).toBe(details.epoch(mockData.epochNo)); + }); +}); diff --git a/src/components/Dreps/DrepsOverview/index.tsx b/src/components/Dreps/DrepsOverview/index.tsx new file mode 100644 index 0000000000..c7cf1dd7ea --- /dev/null +++ b/src/components/Dreps/DrepsOverview/index.tsx @@ -0,0 +1,227 @@ +import React from "react"; +import { Box, Grid, useTheme } from "@mui/material"; +import moment from "moment"; +import { useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; + +import { + CurentEpochPool, + CurentEpochPoolDark, + LiveStakeDarkIcon, + LiveStakeIcon, + RocketPoolDarkIcon, + RocketPoolIcon, + TotalPoolDarkIcon, + TotalPoolIcon +} from "src/commons/resources"; +import { details } from "src/commons/routers"; +import { API } from "src/commons/utils/api"; +import { MAX_SLOT_EPOCH } from "src/commons/utils/constants"; +import { formatADA, formatADAFull, numberWithCommas } from "src/commons/utils/helper"; +import useFetch from "src/commons/hooks/useFetch"; +import Card from "src/components/commons/Card"; +import FormNowMessage from "src/components/commons/FormNowMessage"; +import CustomTooltip from "src/components/commons/CustomTooltip"; +import ADAicon from "src/components/commons/ADAIcon"; + +import { + PoolTitle, + PoolValue, + StyledCard, + StyledCustomIcon, + StyledImg, + StyledLinearProgress, + StyledSkeleton, + TimeDuration +} from "./styles"; + +const DrepsOverview: React.FC = () => { + const { t } = useTranslation(); + const { currentEpoch, blockNo } = useSelector(({ system }: RootState) => system); + const theme = useTheme(); + const { data, loading, lastUpdated } = useFetch( + API.DREPS_LIST.DREPS_OVERVIEW, + undefined, + false, + blockNo + ); + + if (loading) { + return ( + + + + + + + + + + {" "} + + + + + ); + } + + const slot = moment(`${currentEpoch?.endTime} GMT+0000`).isAfter(moment().utc()) + ? (currentEpoch?.slot || 0) % MAX_SLOT_EPOCH + : MAX_SLOT_EPOCH; + const countdown = MAX_SLOT_EPOCH - slot; + const duration = moment.duration(countdown ? countdown : 0, "second"); + const days = duration.days(); + const hours = duration.hours(); + const minutes = duration.minutes(); + const seconds = duration.seconds(); + + return ( + + {t("head.page.dreps")} + + {t("head.page.dreps.des")} + + + } + > + + + + + + + + + + {t("glossary.epoch")} + + {data?.epochNo || t("common.N/A")} + + theme.palette.secondary.light, textAlign: "left" }}> + {t("common.endIn")}:{" "} + + {`${days} ${days > 1 ? t("common.days") : t("common.day")} `} + {`${hours} ${hours > 1 ? t("common.hours") : t("common.hour")} `} + {`${minutes} ${minutes > 1 ? t("common.minutes") : t("common.minute")} `} + {`${seconds} ${seconds > 1 ? t("common.seconds") : t("common.second")}`} + + + + + + + + + theme.palette.secondary[0]} + boxShadow={(theme) => theme.shadow.card} + borderRadius="12px" + height={"100%"} + > + + + {t("glossary.slot")} + + {moment(`${currentEpoch?.endTime} GMT+0000`).isAfter(moment().utc()) + ? (currentEpoch?.slot || 0) % MAX_SLOT_EPOCH + : MAX_SLOT_EPOCH} + theme.palette.secondary.light, fontWeight: "400" }} + > + / {MAX_SLOT_EPOCH} + + + + + + + + + + + + + + + + {t("glossary.activeStake")} () + + + + {data?.liveStake ? formatADA(data?.activeStake) : t("common.N/A")} + + + + + {t("glossary.delegators")} + {numberWithCommas(data?.delegators)} + + + + + + + + + + {t("glossary.totalDreps")} + {data?.totalDReps || 0} + theme.palette.secondary.light, textAlign: "left" }} + display={"flex"} + alignItems={"center"} + width={"100%"} + flexWrap={"wrap"} + gap={1} + > + + {t("glossary.activeDReps")} + {data?.activeDReps || 0} + + + {t("glossary.inactiveDReps")} + {data?.inactiveDReps || 0} + + + {t("glossary.retiredDReps")} + {data?.retiredDReps || 0} + + + + + + + + + + + + + + + + ); +}; + +export default DrepsOverview; diff --git a/src/components/Dreps/DrepsOverview/styles.ts b/src/components/Dreps/DrepsOverview/styles.ts new file mode 100644 index 0000000000..8ef5e38e8b --- /dev/null +++ b/src/components/Dreps/DrepsOverview/styles.ts @@ -0,0 +1,114 @@ +import { Box, LinearProgress, alpha, styled } from "@mui/material"; +import { Link } from "react-router-dom"; + +import CustomIcon from "src/components/commons/CustomIcon"; +import { CommonSkeleton } from "src/components/commons/CustomSkeleton"; + +export const StyledSkeleton = styled(CommonSkeleton)` + border-radius: var(--border-radius); + min-height: 150px; +`; + +export const StyledLinearProgress = styled(LinearProgress)` + margin-top: 11px; + width: 100%; + height: 10px; + border-radius: 34px; + background: ${(props) => props.theme.palette.primary[200]}; + + & > .MuiLinearProgress-barColorPrimary { + border-radius: 34px; + background: ${(props) => props.theme.palette.primary.main}; + } +`; + +export const StyledImg = styled("img")` + width: 35px; + height: 35px; + position: absolute; + top: 10px; + right: 10px; +`; + +export const StyledCustomIcon = styled(CustomIcon)` + width: 35px; + height: 35px; + position: absolute; + top: 10px; + right: 10px; +`; + +export const StyledCard = { + Container: styled("div")` + height: 100%; + background: ${(props) => props.theme.palette.secondary[0]}; + border-radius: 12px; + box-shadow: ${(props) => props.theme.shadow.card}; + position: relative; + display: flex; + `, + ClickAble: styled(Link)` + height: 100%; + background: ${(props) => props.theme.palette.secondary[0]}; + border-radius: 12px; + box-shadow: ${(props) => props.theme.shadow.card}; + position: relative; + display: flex; + transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + cursor: pointer; + &:hover { + box-shadow: ${({ theme }) => "1px 2px 15px 0px " + alpha(theme.palette.secondary.light, 0.25)}; + } + `, + Content: styled("div")` + width: 100%; + padding: 30px; + display: flex; + flex-direction: column; + align-items: flex-start; + `, + Title: styled("span")` + color: ${(props) => props.theme.palette.secondary.light}; + font-weight: var(--font-weight-bold); + margin-bottom: 15px; + white-space: nowrap; + `, + Value: styled("span")` + font-weight: var(--font-weight-bold); + font-size: var(--font-size-title); + margin-bottom: 8px; + color: ${(props) => props.theme.palette.secondary.main}; + `, + Link: styled(Link)` + font-weight: var(--font-weight-bold); + font-size: var(--font-size-title); + margin-bottom: 8px; + font-family: var(--font-family-text) !important; + color: ${(props) => props.theme.palette.secondary.main} !important; + `, + Comment: styled("span")` + font-weight: var(--font-weight-bold); + color: ${(props) => props.theme.palette.secondary.main}; + ` +}; + +export const TimeDuration = styled("small")<{ mobile?: number }>(({ theme, mobile }) => ({ + color: theme.palette.secondary.light, + display: mobile ? "none" : "block", + textAlign: "left", + padding: `${theme.spacing(2)} 0`, + [theme.breakpoints.down("md")]: { + display: mobile ? "block" : "none", + paddingTop: 0, + marginBottom: 20 + } +})); +export const PoolTitle = styled(Box)(({ theme }) => ({ + fontSize: "12px", + color: theme.palette.secondary.light +})); +export const PoolValue = styled(Box)(({ theme }) => ({ + fontSize: "14px", + color: theme.palette.secondary.main, + fontWeight: "bold" +})); diff --git a/src/components/commons/Layout/Sidebar/SidebarMenu/index.tsx b/src/components/commons/Layout/Sidebar/SidebarMenu/index.tsx index 97176d772d..1161b8faba 100644 --- a/src/components/commons/Layout/Sidebar/SidebarMenu/index.tsx +++ b/src/components/commons/Layout/Sidebar/SidebarMenu/index.tsx @@ -10,6 +10,7 @@ import { footerMenus, menus } from "src/commons/menus"; import { isExternalLink } from "src/commons/utils/helper"; import { RootState } from "src/stores/types"; import { setSidebar } from "src/stores/user"; +import { IS_CONWAY_ERA } from "src/commons/utils/constants"; import FooterMenu from "../FooterMenu"; import { @@ -177,51 +178,56 @@ const SidebarMenu: React.FC = ({ history }) => { {children?.length ? ( - {children.map((subItem, subIndex) => { - const { href, icon, isSpecialPath, key } = subItem; - const title = t(key || ""); - return href ? ( - ({ - ...itemStyle(theme, sidebar), - ...(isActiveMenu(href, isSpecialPath) - ? { - backgroundColor: (theme) => `${theme.palette.primary[200]} !important`, - color: (theme) => `${theme.palette.secondary.main} !important` - } - : { color: (theme) => theme.palette.secondary.light }), - paddingLeft: "70px", - [theme.breakpoints.down("md")]: { - paddingLeft: "60px" - }, - ":hover": isActiveMenu(href, isSpecialPath) - ? { - color: `#fff !important` - } - : { - backgroundColor: (theme) => `${theme.palette.primary[200]} !important` - } - })} - > - {icon ? ( - - ) : null} - - - ) : null; - })} + {children + .filter((i) => { + if (i.key === "head.page.drep" && !IS_CONWAY_ERA) return false; + return true; + }) + .map((subItem, subIndex) => { + const { href, icon, isSpecialPath, key } = subItem; + const title = t(key || ""); + return href ? ( + ({ + ...itemStyle(theme, sidebar), + ...(isActiveMenu(href, isSpecialPath) + ? { + backgroundColor: (theme) => `${theme.palette.primary[200]} !important`, + color: (theme) => `${theme.palette.secondary.main} !important` + } + : { color: (theme) => theme.palette.secondary.light }), + paddingLeft: "70px", + [theme.breakpoints.down("md")]: { + paddingLeft: "60px" + }, + ":hover": isActiveMenu(href, isSpecialPath) + ? { + color: `#fff !important` + } + : { + backgroundColor: (theme) => `${theme.palette.primary[200]} !important` + } + })} + > + {icon ? ( + + ) : null} + + + ) : null; + })} ) : null} diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 04b3c0fe14..e41d52bc2f 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -261,12 +261,25 @@ "glossary.value": "Value", "common.result": "Result", "head.page.pool": "Pools", + "head.page.drep": "Delegated Representatives", + "head.page.dreps": "DReps", + "head.page.dreps.des": "Delegated Representatives (DReps)", "glossary.Slot": "Slot", "glossary.delegators": "Delegators", "glossary.totalPools": "Total Pools", + "glossary.totalDreps": "Total DReps", "glossary.endDateTime": "End in: {{d}} days {{h}} hours {{min}} minutes {{s}} seconds", "glossary.activePools": "Active Pools", "glossary.retiredPools": "Retired Pools", + "glossary.activeDReps": "Active DReps", + "glossary.retiredDReps": "Retired DReps", + "glossary.inactiveDReps": "Inactive DReps", + "dreps.id": "DRep ID", + "dreps.anchorLink": "Anchor Link", + "dreps.anchorHash": "Anchor Hash", + "dreps.votingPower": "Voting Power", + "dreps.registrationDate": "Registration Date", + "dreps.lastUpdated": "Last Updated Date", "common.searchPools": "Search Pools", "glossary.poolSize": "Pool size", "glossary.declaredPledge": "Declared Pledge ", diff --git a/src/pages/DelegationDetail/index.tsx b/src/pages/DelegationDetail/index.tsx index c2238d5f85..5639900b85 100644 --- a/src/pages/DelegationDetail/index.tsx +++ b/src/pages/DelegationDetail/index.tsx @@ -13,7 +13,7 @@ import useFetchList from "src/commons/hooks/useFetchList"; import { StakeKeyHistoryIcon, StakingDelegators, TimelineIconComponent, VotesIcon } from "src/commons/resources"; import { routers } from "src/commons/routers"; import { API } from "src/commons/utils/api"; -import { VOTE_TYPE, POOL_STATUS, STATUS_VOTE } from "src/commons/utils/constants"; +import { VOTE_TYPE, POOL_STATUS, STATUS_VOTE, IS_CONWAY_ERA } from "src/commons/utils/constants"; import { getPageInfo } from "src/commons/utils/helper"; import DelegationDetailChart from "src/components/DelegationDetail/DelegationDetailChart"; import DelegationDetailInfo from "src/components/DelegationDetail/DelegationDetailInfo"; @@ -180,7 +180,12 @@ const DelegationDetail: React.FC = () => { ) } - ]; + ].filter((tab) => { + if (tab.key === "governanceVotes" && !IS_CONWAY_ERA) { + return false; + } + return true; + }); const indexExpand = tabs.findIndex((item) => item.key === tab); const needBorderRadius = (currentKey: string) => { diff --git a/src/pages/DrepDetail/index.tsx b/src/pages/DrepDetail/index.tsx index 14272b68fe..e01255f000 100644 --- a/src/pages/DrepDetail/index.tsx +++ b/src/pages/DrepDetail/index.tsx @@ -53,10 +53,11 @@ import { CommonSkeleton } from "src/components/commons/CustomSkeleton"; import { TruncateSubTitleContainer } from "src/components/share/styled"; import DynamicEllipsisText from "src/components/DynamicEllipsisText"; import { useScreen } from "src/commons/hooks/useScreen"; -import { VOTE_TYPE } from "src/commons/utils/constants"; +import { IS_CONWAY_ERA, VOTE_TYPE } from "src/commons/utils/constants"; import DelegationGovernanceVotes, { ActionMetadataModalConfirm } from "src/components/GovernanceVotes"; import { StyledContainer, StyledMenuItem, StyledSelect, TimeDuration, TitleCard, TitleTab, ValueCard } from "./styles"; +import NotFound from "../NotFound"; const voteOption = [ { title: "Action Type", value: "Default" }, @@ -265,6 +266,10 @@ const DrepDetail = () => { ) } ]; + + if (!IS_CONWAY_ERA) { + return ; + } if (loading) { return ( diff --git a/src/pages/Dreps/Dreps.test.tsx b/src/pages/Dreps/Dreps.test.tsx new file mode 100644 index 0000000000..27fce1ca1d --- /dev/null +++ b/src/pages/Dreps/Dreps.test.tsx @@ -0,0 +1,76 @@ +import { screen, cleanup, fireEvent } from "@testing-library/react"; +import { createMemoryHistory } from "history"; +import { Router } from "react-router"; + +import { render } from "src/test-utils"; +import Table, { Column } from "src/components/commons/Table"; +import useFetchList from "src/commons/hooks/useFetchList"; +import DelegationLists from "src/components/DelegationPool/DelegationList"; +import { details } from "src/commons/routers"; + +const mockData = { + data: [ + { + poolName: "StakeNuts", + poolId: "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", + poolSize: 4176968837615 + } + ] +}; + +jest.mock("src/commons/hooks/useFetchList"); + +describe("Delegation pools view", () => { + afterEach(() => { + cleanup(); + }); + + it("should render Delegation pools page", async () => { + const mockUseFetch = useFetchList as jest.Mock; + await mockUseFetch.mockReturnValue({ data: [] }); + render(); + expect(useFetchList).toBeCalled(); + const inputField: HTMLInputElement = screen.getByPlaceholderText("Search Pools"); + expect(inputField).toBeInTheDocument(); + fireEvent.change(inputField, { target: { value: "test" } }); + expect(inputField?.value).toBe("test"); + }); + + it("renders the table with given column and data", () => { + const columns: Column<{ test: string }>[] = [ + { + title: "Test Column", + key: "test", + render: (r) =>
{r.test}
+ } + ]; + + const data = [ + { + test: "Test Data" + } + ]; + render(
); + + expect(screen.getByText("Test Column")).toBeInTheDocument(); + expect(screen.getByText("Test Data")).toBeInTheDocument(); + }); + + it("should navigate to the correct route when button is clicked", async () => { + const mockUseFetchList = useFetchList as jest.Mock; + mockUseFetchList.mockReturnValue(mockData); + const history = createMemoryHistory(); + + render( + + + + ); + + const DelegationItem = screen.getByText("StakeNuts"); + fireEvent.click(DelegationItem); + expect(history.location.pathname).toBe( + details.delegation("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + ); + }); +}); diff --git a/src/pages/Dreps/index.tsx b/src/pages/Dreps/index.tsx new file mode 100644 index 0000000000..a12666ad34 --- /dev/null +++ b/src/pages/Dreps/index.tsx @@ -0,0 +1,26 @@ +import { useEffect } from "react"; + +import OverViews from "src/components/Dreps/DrepsOverview"; +import DelegationLists from "src/components/Dreps/DrepsList"; +import { IS_CONWAY_ERA } from "src/commons/utils/constants"; + +import { Horizon, StyledContainer } from "./styles"; +import NotFound from "../NotFound"; + +const Dreps = () => { + useEffect(() => { + document.title = `Delegated Representative | Cardano Blockchain Explorer`; + }, []); + if (!IS_CONWAY_ERA) { + return ; + } + return ( + + + + + + ); +}; + +export default Dreps; diff --git a/src/pages/Dreps/styles.ts b/src/pages/Dreps/styles.ts new file mode 100644 index 0000000000..6e24615606 --- /dev/null +++ b/src/pages/Dreps/styles.ts @@ -0,0 +1,14 @@ +import { Container, styled } from "@mui/material"; + +export const StyledContainer = styled(Container)(({ theme }) => ({ + [theme.breakpoints.down("sm")]: { + padding: "0px 16px 20px" + } +})); + +export const Horizon = styled("div")` + margin: 40px 0 20px; + width: 100%; + border: 1px solid ${(props) => props.theme.palette.secondary.main}; + opacity: 0.07; +`; diff --git a/src/types/drep.d.ts b/src/types/drep.d.ts index 8aae2b0e8e..86be8f4834 100644 --- a/src/types/drep.d.ts +++ b/src/types/drep.d.ts @@ -27,3 +27,34 @@ interface DrepOverviewChart { numberOfNoVotes: number | null; numberOfYesVote: number | null; } + +interface OverViewDreps { + countDownEndTime: number; + delegators: number; + epochNo: number; + epochSlotNo: number; + liveStake: number; + activePools: number; + retiredPools: number; + activeStake: number; + totalDReps: number; + activeDReps: number; + inactiveDReps: number; + retiredDReps: number; + abstainDReps: number; + noConfidenceDReps: number; + registeredDReps: number; + totalAdaStaked: number; +} + +interface Drep { + activeVoteStake: number; + anchorHash: string; + anchorUrl: string; + createdAt: string; + drepHash: string; + drepId: string; + status: "ACTIVE" | "INACTIVE" | "RETIRED"; + updatedAt: string; + votingPower: number; +} diff --git a/vite.config.ts b/vite.config.ts index 5dfb94de9e..ecaef5f52a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -20,7 +20,8 @@ export default defineConfig(({ mode }) => { "process.env.REACT_APP_SANCHONET_APP_URL": JSON.stringify(env.REACT_APP_SANCHONET_APP_URL), "process.env.REACT_APP_BOLNISI_NAME_API": JSON.stringify(env.REACT_APP_BOLNISI_NAME_API), "process.env.REACT_APP_ADA_HANDLE_API": JSON.stringify(env.REACT_APP_ADA_HANDLE_API), - "process.env.REACT_APP_API_URL_COIN_GECKO": JSON.stringify(env.REACT_APP_API_URL_COIN_GECKO) + "process.env.REACT_APP_API_URL_COIN_GECKO": JSON.stringify(env.REACT_APP_API_URL_COIN_GECKO), + "process.env.REACT_APP_FF_GLOBAL_IS_CONWAY_ERA": JSON.stringify(env.REACT_APP_FF_GLOBAL_IS_CONWAY_ERA) }, optimizeDeps: { esbuildOptions: {