From 7dc893c384723d445e9bcbe259b43a0c4a9793cf Mon Sep 17 00:00:00 2001 From: Joanna Dyczka Date: Thu, 21 Mar 2024 11:00:51 +0100 Subject: [PATCH 1/5] [#219] fetch data for DRep details page --- govtool/frontend/src/pages/DRepDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/govtool/frontend/src/pages/DRepDetails.tsx b/govtool/frontend/src/pages/DRepDetails.tsx index f3c084327..ea9685c9d 100644 --- a/govtool/frontend/src/pages/DRepDetails.tsx +++ b/govtool/frontend/src/pages/DRepDetails.tsx @@ -34,7 +34,7 @@ export const DRepDetails = ({ isConnected }: DRepDetailsProps) => { buildVoteDelegationCert, dRepID: myDRepId, pendingTransaction, - stakeKey + stakeKey, } = useCardano(); const { t } = useTranslation(); const navigate = useNavigate(); From d6ff1165b6e0f964340a6b1facdfa53819ddb955 Mon Sep 17 00:00:00 2001 From: Joanna Dyczka Date: Wed, 6 Mar 2024 22:53:52 +0100 Subject: [PATCH 2/5] [#220] add styling for DRepCard in default state --- .../frontend/src/components/atoms/Button.tsx | 1 + .../src/components/atoms/StatusPill.tsx | 57 ++++++++ .../frontend/src/components/atoms/index.ts | 1 + .../src/components/organisms/DRepCard.tsx | 130 ++++++++++++++++++ .../src/components/organisms/index.ts | 1 + govtool/frontend/src/i18n/locales/en.ts | 4 + .../src/pages/DRepDirectoryContent.tsx | 99 ++++++++++++- .../src/stories/StatusPill.stories.ts | 33 +++++ 8 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 govtool/frontend/src/components/atoms/StatusPill.tsx create mode 100644 govtool/frontend/src/components/organisms/DRepCard.tsx create mode 100644 govtool/frontend/src/stories/StatusPill.stories.ts diff --git a/govtool/frontend/src/components/atoms/Button.tsx b/govtool/frontend/src/components/atoms/Button.tsx index 97b9f100e..5ce22bf48 100644 --- a/govtool/frontend/src/components/atoms/Button.tsx +++ b/govtool/frontend/src/components/atoms/Button.tsx @@ -19,6 +19,7 @@ export const Button = ({ sx={{ fontSize: size === "extraLarge" ? 16 : 14, height: buttonHeight, + whiteSpace: "nowrap", ...sx, }} variant={variant} diff --git a/govtool/frontend/src/components/atoms/StatusPill.tsx b/govtool/frontend/src/components/atoms/StatusPill.tsx new file mode 100644 index 000000000..60b70d049 --- /dev/null +++ b/govtool/frontend/src/components/atoms/StatusPill.tsx @@ -0,0 +1,57 @@ +import { Chip, ChipProps, styled } from "@mui/material"; +import { cyan, errorRed, successGreen } from "@/consts"; + +type Status = 'active' | 'retired' | 'inactive'; + +interface StatusPillProps { + status: Status; + label?: string; + size?: 'small' | 'medium'; + sx?: ChipProps['sx']; +} + +export const StatusPill = ({ + status, + label = status, + size = 'small', + sx +}: StatusPillProps) => ( + +); + +const getBgColor = (status: Status): string => { + switch (status) { + case 'active': + return successGreen.c200; + case 'retired': + return errorRed.c100; + case 'inactive': + return cyan.c100; + // no default + } +}; + +const getTextColor = (status: Status): string => { + switch (status) { + case 'active': + return successGreen.c700; + case 'retired': + return errorRed.c500; + case 'inactive': + return cyan.c500; + // no default + } +}; + +const StyledChip = styled(Chip)<{ status: Status }>(({ theme, status }) => ({ + backgroundColor: getBgColor(status), + color: getTextColor(status), + border: `2px solid ${theme.palette.neutralWhite}`, + fontSize: '0.75rem', + textTransform: 'capitalize', +})); diff --git a/govtool/frontend/src/components/atoms/index.ts b/govtool/frontend/src/components/atoms/index.ts index b91dec294..ceb4bc8db 100644 --- a/govtool/frontend/src/components/atoms/index.ts +++ b/govtool/frontend/src/components/atoms/index.ts @@ -22,6 +22,7 @@ export * from "./Radio"; export * from "./ScrollToManage"; export * from "./ScrollToTop"; export * from "./Spacer"; +export * from "./StatusPill"; export * from "./StakeRadio"; export * from "./TextArea"; export * from "./Tooltip"; diff --git a/govtool/frontend/src/components/organisms/DRepCard.tsx b/govtool/frontend/src/components/organisms/DRepCard.tsx new file mode 100644 index 000000000..43ce7a599 --- /dev/null +++ b/govtool/frontend/src/components/organisms/DRepCard.tsx @@ -0,0 +1,130 @@ +import { useNavigate } from "react-router-dom"; +import { Box, ButtonBase, Divider } from "@mui/material"; + +import { useTranslation } from "@hooks"; +import { Button, StatusPill, Typography } from "@atoms"; +import { Card } from "@molecules"; +import { correctAdaFormat } from "@/utils"; +import { ICONS } from "@/consts"; + +export const DRepCard = ({ + isConnected, + name, + id, + votingPower, + status, +}: any) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + + return ( + + + + + + {name} + + + {id} + + + + + + + + + {t("votingPower")} + + + ₳ + {' '} + {correctAdaFormat(votingPower)} + + + ({ borderColor: palette.lightBlue })} + /> + + + {t("status")} + + + + + + + + + + {status === "active" && isConnected && ( + + )} + {status === "active" && !isConnected && ( + + )} + + + + ); +}; + +const ellipsisStyles = { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", +} as const; diff --git a/govtool/frontend/src/components/organisms/index.ts b/govtool/frontend/src/components/organisms/index.ts index 51a90714a..e7a31a843 100644 --- a/govtool/frontend/src/components/organisms/index.ts +++ b/govtool/frontend/src/components/organisms/index.ts @@ -15,6 +15,7 @@ export * from "./DelegateTodRepStepOne"; export * from "./DelegateTodRepStepTwo"; export * from "./Drawer"; export * from "./DrawerMobile"; +export * from "./DRepCard"; export * from "./ExternalLinkModal"; export * from "./Footer"; export * from "./GovernanceActionDetailsCard"; diff --git a/govtool/frontend/src/i18n/locales/en.ts b/govtool/frontend/src/i18n/locales/en.ts index ca69fdec2..993c92855 100644 --- a/govtool/frontend/src/i18n/locales/en.ts +++ b/govtool/frontend/src/i18n/locales/en.ts @@ -239,8 +239,11 @@ export const en = { abstainCardTitle: "Abstain from Every Vote", automatedVotingOptions: "Automated Voting Options", editBtn: "Edit DRep data", + delegationOptions: "Delegation Options", meAsDRep: "This DRep ID is connected to your wallet", + myDelegation: "You have delegated ₳ {{ada}} to:", myDRep: "This is your DRep", + listTitle: "Find a DRep", noConfidenceDescription: "Select this to signal no confidence in the current constitutional committee by voting NO on every proposal and voting YES to no confidence proposals", noConfidenceTitle: "Signal No Confidence on Every Vote", @@ -653,6 +656,7 @@ export const en = { status: "Status", submit: "Submit", thisLink: "this link", + viewDetails: "View details", votingPower: "Voting power", yes: "Yes", }, diff --git a/govtool/frontend/src/pages/DRepDirectoryContent.tsx b/govtool/frontend/src/pages/DRepDirectoryContent.tsx index cb87558cb..b9db91afa 100644 --- a/govtool/frontend/src/pages/DRepDirectoryContent.tsx +++ b/govtool/frontend/src/pages/DRepDirectoryContent.tsx @@ -1,5 +1,8 @@ +import { Box } from "@mui/material"; import { FC } from "react"; -import { AutomatedVotingOptions } from "@organisms"; +import { AutomatedVotingOptions, DRepCard } from "@organisms"; +import { Typography } from "@atoms"; +import { Trans, useTranslation } from "react-i18next"; interface DRepDirectoryContentProps { isConnected?: boolean; @@ -7,4 +10,96 @@ interface DRepDirectoryContentProps { export const DRepDirectoryContent: FC = ({ isConnected, -}) => <>{isConnected && }; +}) => { + const { t } = useTranslation(); + + const ada = 1234567; + + return ( + + {isConnected && ( +
+ + + + +
+ )} + + {isConnected && ( +
+ {t("dRepDirectory.delegationOptions")} + +
+ )} + +
+ {t("dRepDirectory.listTitle")} + + {data.map((dRep) => ( + + + + ))} + +
+
+ ); +}; + +const data = [ + { + name: "DRep 1", + id: "1kejngelrngekngeriogj3io4j3gnd3", + votingPower: 3000000, + status: "active", + }, + { + name: "DRep 2", + id: "1kejrngelrngekngeriogfrej3io4fj3gn3", + votingPower: 10000000, + status: "active", + }, + { + name: "DRep 1", + id: "1kejngelrngekngeriogj3io4j3gnd3", + votingPower: 1000000, + status: "active", + }, + { + name: "DRep 2", + id: "1kejrngelrngekngeriogfrej3io4fj3gn3", + votingPower: 9900000000000000, + status: "active", + }, + { + name: "DRep 1", + id: "1kejngelrngekngeriogj3io4j3gn3", + votingPower: 12345678, + status: "active", + }, + { + name: "DRep 2", + id: "1kejrngelrngekngeriogfrej3io4j3gn3", + votingPower: 1234567800, + status: "active", + }, + { + name: "DRep 4", + id: "1kejrngelkngeriogj3io4j3gn3", + votingPower: 12345678000000, + status: "retired", + }, + { + name: "DRep 3", + id: "1kejrngelrngekngeriogj3io4j3gn2", + votingPower: 123456, + status: "active", + }, + { + name: "Some dreps can have a very long name and it will be displayed correctly", + id: "1kejrngelrngekngeriogj3io4j3gn3", + votingPower: 123456, + status: "inactive", + }, +]; diff --git a/govtool/frontend/src/stories/StatusPill.stories.ts b/govtool/frontend/src/stories/StatusPill.stories.ts new file mode 100644 index 000000000..a26e09dc7 --- /dev/null +++ b/govtool/frontend/src/stories/StatusPill.stories.ts @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { StatusPill } from "@atoms"; + +const meta = { + title: "Example/StatusPill", + component: StatusPill, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const StatusPillActive: Story = { + args: { + status: "active", + }, +}; + +export const StatusPillInactive: Story = { + args: { + status: "inactive", + }, +}; + +export const StatusPillRetired: Story = { + args: { + status: "retired", + }, +}; From fd4e4e50b59732eaad16e7c60e3d86e620b05f6b Mon Sep 17 00:00:00 2001 From: Joanna Dyczka Date: Thu, 21 Mar 2024 17:04:17 +0100 Subject: [PATCH 3/5] [#220] fetch data for drep list --- .../src/components/atoms/StatusPill.tsx | 22 +- .../src/components/organisms/DRepCard.tsx | 56 +++- govtool/frontend/src/hooks/index.ts | 2 + .../src/hooks/queries/useGetDRepListQuery.ts | 9 +- .../frontend/src/hooks/useDelegateToDrep.ts | 45 +++ govtool/frontend/src/pages/DRepDetails.tsx | 301 +++++++++--------- .../src/pages/DRepDirectoryContent.tsx | 117 +++---- .../src/stories/StatusPill.stories.ts | 6 +- govtool/frontend/src/utils/dRep.ts | 11 + govtool/frontend/src/utils/index.ts | 1 + 10 files changed, 317 insertions(+), 253 deletions(-) create mode 100644 govtool/frontend/src/hooks/useDelegateToDrep.ts create mode 100644 govtool/frontend/src/utils/dRep.ts diff --git a/govtool/frontend/src/components/atoms/StatusPill.tsx b/govtool/frontend/src/components/atoms/StatusPill.tsx index 60b70d049..ff08844d2 100644 --- a/govtool/frontend/src/components/atoms/StatusPill.tsx +++ b/govtool/frontend/src/components/atoms/StatusPill.tsx @@ -1,7 +1,7 @@ import { Chip, ChipProps, styled } from "@mui/material"; import { cyan, errorRed, successGreen } from "@/consts"; -type Status = 'active' | 'retired' | 'inactive'; +type Status = 'Active' | 'Inactive' | 'Retired'; interface StatusPillProps { status: Status; @@ -26,25 +26,25 @@ export const StatusPill = ({ const getBgColor = (status: Status): string => { switch (status) { - case 'active': + case 'Active': return successGreen.c200; - case 'retired': - return errorRed.c100; - case 'inactive': + case 'Inactive': return cyan.c100; - // no default + case 'Retired': + return errorRed.c100; + // no default } }; const getTextColor = (status: Status): string => { switch (status) { - case 'active': + case 'Active': return successGreen.c700; - case 'retired': - return errorRed.c500; - case 'inactive': + case 'Inactive': return cyan.c500; - // no default + case 'Retired': + return errorRed.c500; + // no default } }; diff --git a/govtool/frontend/src/components/organisms/DRepCard.tsx b/govtool/frontend/src/components/organisms/DRepCard.tsx index 43ce7a599..c7c6cf4d8 100644 --- a/govtool/frontend/src/components/organisms/DRepCard.tsx +++ b/govtool/frontend/src/components/organisms/DRepCard.tsx @@ -5,20 +5,44 @@ import { useTranslation } from "@hooks"; import { Button, StatusPill, Typography } from "@atoms"; import { Card } from "@molecules"; import { correctAdaFormat } from "@/utils"; -import { ICONS } from "@/consts"; +import { ICONS, PATHS } from "@/consts"; +import { DRepData } from "@/models"; + +type DRepCardProps = { + dRep: DRepData; + isConnected: boolean; + isInProgress?: boolean; + isMe?: boolean; + onDelegate?: () => void; +} export const DRepCard = ({ + dRep: { + status, + type, + view, + votingPower, + }, isConnected, - name, - id, - votingPower, - status, -}: any) => { + isInProgress, + isMe, + onDelegate, +}: DRepCardProps) => { const navigate = useNavigate(); const { t } = useTranslation(); return ( - + - {name} + {type} - {id} + {view} @@ -108,13 +132,19 @@ export const DRepCard = ({ }, }} > - - {status === "active" && isConnected && ( - + {status === "Active" && isConnected && onDelegate && ( + )} - {status === "active" && !isConnected && ( + {status === "Active" && !isConnected && ( )} diff --git a/govtool/frontend/src/hooks/index.ts b/govtool/frontend/src/hooks/index.ts index 126c2d0c9..9befd259f 100644 --- a/govtool/frontend/src/hooks/index.ts +++ b/govtool/frontend/src/hooks/index.ts @@ -1,4 +1,6 @@ export { useTranslation } from "react-i18next"; + +export * from "./useDelegateToDrep"; export * from "./useFetchNextPageDetector"; export * from "./useOutsideClick"; export * from "./useSaveScrollPosition"; diff --git a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts index 59021a24c..b00932f1d 100644 --- a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts @@ -1,21 +1,24 @@ -import { useQuery } from "react-query"; +import { UseQueryOptions, useQuery } from "react-query"; import { QUERY_KEYS } from "@consts"; import { useCardano } from "@context"; import { getDRepList } from "@services"; +import { DRepData } from "@/models"; -export const useGetDRepListQuery = (dRepView?: string) => { +export const useGetDRepListQuery = (dRepView?: string, options?: UseQueryOptions) => { const { pendingTransaction } = useCardano(); - const { data, isLoading } = useQuery({ + const { data, isLoading } = useQuery({ queryKey: [ QUERY_KEYS.useGetDRepListKey, (pendingTransaction.registerAsSoleVoter || pendingTransaction.registerAsDrep || pendingTransaction.retireAsSoleVoter || pendingTransaction.retireAsDrep)?.transactionHash, + dRepView ], queryFn: () => getDRepList(dRepView), + ...options }); return { data, isLoading }; diff --git a/govtool/frontend/src/hooks/useDelegateToDrep.ts b/govtool/frontend/src/hooks/useDelegateToDrep.ts new file mode 100644 index 000000000..0183e82de --- /dev/null +++ b/govtool/frontend/src/hooks/useDelegateToDrep.ts @@ -0,0 +1,45 @@ +import { useCallback, useState } from "react"; +import { useTranslation } from "@hooks"; +import { useCardano, useSnackbar } from "@/context"; + +export const useDelegateTodRep = () => { + const { + buildSignSubmitConwayCertTx, + buildVoteDelegationCert, + } = useCardano(); + const { t } = useTranslation(); + const { addSuccessAlert, addErrorAlert } = useSnackbar(); + + const [isDelegating, setIsDelegating] = useState(false); + + const delegate = useCallback(async (dRepId: string | undefined) => { + if (!dRepId) return; + setIsDelegating(true); + try { + const certBuilder = await buildVoteDelegationCert(dRepId); + const result = await buildSignSubmitConwayCertTx({ + certBuilder, + type: "delegate", + resourceId: dRepId, + }); + if (result) { + addSuccessAlert(t("alerts.delegate.success")); + } + } catch (error) { + addErrorAlert(t("alerts.delegate.failed")); + } finally { + setIsDelegating(false); + } + }, [ + addErrorAlert, + addSuccessAlert, + buildSignSubmitConwayCertTx, + buildVoteDelegationCert, + t, + ]); + + return { + delegate, + isDelegating, + }; +}; diff --git a/govtool/frontend/src/pages/DRepDetails.tsx b/govtool/frontend/src/pages/DRepDetails.tsx index ea9685c9d..1d0783358 100644 --- a/govtool/frontend/src/pages/DRepDetails.tsx +++ b/govtool/frontend/src/pages/DRepDetails.tsx @@ -1,20 +1,24 @@ -import { PropsWithChildren, useState } from "react"; +import { PropsWithChildren } from "react"; import { Navigate, useNavigate, useParams } from "react-router-dom"; import { Box, ButtonBase, Chip, CircularProgress } from "@mui/material"; -import { Button, LoadingButton, Typography } from "@atoms"; -import { Card, Share } from "@molecules"; +import { + Button, LoadingButton, StatusPill, Typography +} from "@atoms"; +import { Card, LinkWithIcon, Share } from "@molecules"; import { ICONS, PATHS } from "@consts"; import { + useDelegateTodRep, useGetAdaHolderCurrentDelegationQuery, useGetDRepListQuery, useScreenDimension, useTranslation } from "@hooks"; import { correctAdaFormat, openInNewTab } from "@utils"; -import { useCardano, useModal, useSnackbar } from "@/context"; +import { useCardano, useModal } from "@/context"; +import { isSameDRep } from "@/utils"; const LINKS = [ "darlenelonglink1.DRepwebsiteorwhatever.com", @@ -30,8 +34,6 @@ type DRepDetailsProps = { export const DRepDetails = ({ isConnected }: DRepDetailsProps) => { const { - buildSignSubmitConwayCertTx, - buildVoteDelegationCert, dRepID: myDRepId, pendingTransaction, stakeKey, @@ -39,11 +41,10 @@ export const DRepDetails = ({ isConnected }: DRepDetailsProps) => { const { t } = useTranslation(); const navigate = useNavigate(); const { openModal } = useModal(); - const { addSuccessAlert, addErrorAlert } = useSnackbar(); const { screenWidth } = useScreenDimension(); const { dRepId: dRepParam } = useParams(); - const [isDelegating, setIsDelegating] = useState(false); + const { delegate, isDelegating } = useDelegateTodRep(); const { currentDelegation } = useGetAdaHolderCurrentDelegationQuery(stakeKey); const { data, isLoading } = useGetDRepListQuery(dRepParam); @@ -54,166 +55,152 @@ export const DRepDetails = ({ isConnected }: DRepDetailsProps) => { if (!dRep) return ; const { - drepId, view, status, votingPower, type + view, status, votingPower, type } = dRep; - const isMe = drepId === myDRepId || view === myDRepId; - const isMyDrep = drepId === currentDelegation || view === currentDelegation; - const inProgressDelegation = pendingTransaction.delegate?.resourceId; - const isMyDrepInProgress = drepId === inProgressDelegation || view === inProgressDelegation; - - const delegate = async () => { - setIsDelegating(true); - try { - const certBuilder = await buildVoteDelegationCert(drepId); - const result = await buildSignSubmitConwayCertTx({ - certBuilder, - type: "delegate", - resourceId: drepId, - }); - if (result) { - addSuccessAlert(t("alerts.delegate.success")); - } - } catch (error) { - addErrorAlert(t("alerts.delegate.failed")); - } finally { - setIsDelegating(false); - } - }; + const isMe = isSameDRep(dRep, myDRepId); + const isMyDrep = isSameDRep(dRep, currentDelegation); + const isMyDrepInProgress = isSameDRep(dRep, pendingTransaction.delegate?.resourceId); return ( - - {(isMe || isMyDrep) && ( - theme.shadows[2], - color: (theme) => theme.palette.text.primary, - mb: 1.5, - px: 2, - py: 0.5, - width: '100%', - }} - /> - )} - + navigate(isConnected ? PATHS.dashboardDRepDirectory : PATHS.dRepDirectory)} + sx={{ mb: 2 }} + /> + - theme.shadows[2], + color: (theme) => theme.palette.text.primary, + mb: 1.5, + px: 2, + py: 0.5, + width: '100%', + }} + /> + )} + - {type} - - {isMe && ( - - )} - - - - - - {view} - - - {/* TODO: add status pill */} - {/* */} - {status} - - - - {'₳ '} - {correctAdaFormat(votingPower)} + {type} - - {/* TODO: fetch metadata, add views for metadata errors */} - - - - - - {LINKS.map((link) => ( - - ))} - - - + {isMe && ( + + )} + + - - {isConnected ? ( - - {t("delegate")} - - ) : ( - - )} - + + + {view} + + + + + + + {'₳ '} + {correctAdaFormat(votingPower)} + + + {/* TODO: fetch metadata, add views for metadata errors */} + + + + + + {LINKS.map((link) => ( + + ))} + + + - {t("about")} - - {/* TODO replace with actual data */} - I am the Cardano crusader carving his path in the blockchain - battleground. With a mind sharper than a Ledger Nano X, this fearless - crypto connoisseur fearlessly navigates the volatile seas of Cardano, - turning code into currency. Armed with a keyboard and a heart pumping - with blockchain beats, Mister Big Bad fearlessly champions - decentralization, smart contracts, and the Cardano community. His - Twitter feed is a mix of market analysis that rivals CNBC and memes that - could break the internet. - - + + {(isConnected && status === 'Active' && !isMyDrep) && ( + delegate(dRep.view)} + size="extraLarge" + sx={{ width: "100%" }} + variant="contained" + > + {t("delegate")} + + )} + {!isConnected && ( + + )} + + + {t("about")} + + {/* TODO replace with actual data */} + I am the Cardano crusader carving his path in the blockchain + battleground. With a mind sharper than a Ledger Nano X, this fearless + crypto connoisseur fearlessly navigates the volatile seas of Cardano, + turning code into currency. Armed with a keyboard and a heart pumping + with blockchain beats, Mister Big Bad fearlessly champions + decentralization, smart contracts, and the Cardano community. His + Twitter feed is a mix of market analysis that rivals CNBC and memes that + could break the internet. + + + ); }; @@ -267,7 +254,7 @@ type LinkWithIconProps = { navTo: string; }; -const LinkWithIcon = ({ label, navTo }: LinkWithIconProps) => { +const MoreInfoLink = ({ label, navTo }: LinkWithIconProps) => { const openLink = () => openInNewTab(navTo); return ( diff --git a/govtool/frontend/src/pages/DRepDirectoryContent.tsx b/govtool/frontend/src/pages/DRepDirectoryContent.tsx index b9db91afa..d1cc38b4c 100644 --- a/govtool/frontend/src/pages/DRepDirectoryContent.tsx +++ b/govtool/frontend/src/pages/DRepDirectoryContent.tsx @@ -1,8 +1,16 @@ -import { Box } from "@mui/material"; +import { Box, CircularProgress } from "@mui/material"; import { FC } from "react"; import { AutomatedVotingOptions, DRepCard } from "@organisms"; import { Typography } from "@atoms"; import { Trans, useTranslation } from "react-i18next"; +import { useCardano } from "@/context"; +import { + useDelegateTodRep, + useGetAdaHolderCurrentDelegationQuery, + useGetAdaHolderVotingPowerQuery, + useGetDRepListQuery +} from "@/hooks"; +import { correctAdaFormat, formHexToBech32, isSameDRep } from "@/utils"; interface DRepDirectoryContentProps { isConnected?: boolean; @@ -11,18 +19,44 @@ interface DRepDirectoryContentProps { export const DRepDirectoryContent: FC = ({ isConnected, }) => { + const { + dRepID: myDRepId, + isEnableLoading, + pendingTransaction, + stakeKey, + } = useCardano(); const { t } = useTranslation(); - const ada = 1234567; + const { delegate } = useDelegateTodRep(); + + const { currentDelegation } = useGetAdaHolderCurrentDelegationQuery(stakeKey); + const inProgressDelegation = pendingTransaction.delegate?.resourceId; + + const { data: myDRepList, isLoading: isMyDRepLoading } = useGetDRepListQuery( + currentDelegation?.startsWith('drep') ? currentDelegation : formHexToBech32(currentDelegation), + { enabled: !!inProgressDelegation || !!currentDelegation } + ); + const myDrep = myDRepList?.[0]; + const { data: dRepList, isLoading: isDRepListLoading } = useGetDRepListQuery(); + + const { votingPower } = useGetAdaHolderVotingPowerQuery(); + + if (isEnableLoading || isMyDRepLoading || isDRepListLoading) return ; + + const ada = correctAdaFormat(votingPower); return ( - {isConnected && ( + {myDrep && (
- +
)} @@ -36,70 +70,21 @@ export const DRepDirectoryContent: FC = ({
{t("dRepDirectory.listTitle")} - {data.map((dRep) => ( - - - - ))} + {dRepList?.map((dRep) => (isSameDRep(dRep, myDrep?.view) + ? null + : ( + + delegate(dRep.drepId)} + /> + + )))}
); }; - -const data = [ - { - name: "DRep 1", - id: "1kejngelrngekngeriogj3io4j3gnd3", - votingPower: 3000000, - status: "active", - }, - { - name: "DRep 2", - id: "1kejrngelrngekngeriogfrej3io4fj3gn3", - votingPower: 10000000, - status: "active", - }, - { - name: "DRep 1", - id: "1kejngelrngekngeriogj3io4j3gnd3", - votingPower: 1000000, - status: "active", - }, - { - name: "DRep 2", - id: "1kejrngelrngekngeriogfrej3io4fj3gn3", - votingPower: 9900000000000000, - status: "active", - }, - { - name: "DRep 1", - id: "1kejngelrngekngeriogj3io4j3gn3", - votingPower: 12345678, - status: "active", - }, - { - name: "DRep 2", - id: "1kejrngelrngekngeriogfrej3io4j3gn3", - votingPower: 1234567800, - status: "active", - }, - { - name: "DRep 4", - id: "1kejrngelkngeriogj3io4j3gn3", - votingPower: 12345678000000, - status: "retired", - }, - { - name: "DRep 3", - id: "1kejrngelrngekngeriogj3io4j3gn2", - votingPower: 123456, - status: "active", - }, - { - name: "Some dreps can have a very long name and it will be displayed correctly", - id: "1kejrngelrngekngeriogj3io4j3gn3", - votingPower: 123456, - status: "inactive", - }, -]; diff --git a/govtool/frontend/src/stories/StatusPill.stories.ts b/govtool/frontend/src/stories/StatusPill.stories.ts index a26e09dc7..0ca27c631 100644 --- a/govtool/frontend/src/stories/StatusPill.stories.ts +++ b/govtool/frontend/src/stories/StatusPill.stories.ts @@ -16,18 +16,18 @@ type Story = StoryObj; export const StatusPillActive: Story = { args: { - status: "active", + status: "Active", }, }; export const StatusPillInactive: Story = { args: { - status: "inactive", + status: "Inactive", }, }; export const StatusPillRetired: Story = { args: { - status: "retired", + status: "Retired", }, }; diff --git a/govtool/frontend/src/utils/dRep.ts b/govtool/frontend/src/utils/dRep.ts new file mode 100644 index 000000000..2135677a1 --- /dev/null +++ b/govtool/frontend/src/utils/dRep.ts @@ -0,0 +1,11 @@ +import { DRepData } from '@/models'; + +export const isSameDRep = ( + { drepId, view }: DRepData, + dRepIdOrView: string | undefined, +) => { + if (!dRepIdOrView) { + return false; + } + return drepId === dRepIdOrView || view === dRepIdOrView; +}; diff --git a/govtool/frontend/src/utils/index.ts b/govtool/frontend/src/utils/index.ts index 60d8abe0c..a1577ccff 100644 --- a/govtool/frontend/src/utils/index.ts +++ b/govtool/frontend/src/utils/index.ts @@ -5,6 +5,7 @@ export * from "./callAll"; export * from "./canonizeJSON"; export * from "./checkIsMaintenanceOn"; export * from "./checkIsWalletConnected"; +export * from "./dRep"; export * from "./formatDate"; export * from "./generateAnchor"; export * from "./generateJsonld"; From 1ab56a21272b802b1816cbcb220e2d9f5869e621 Mon Sep 17 00:00:00 2001 From: Joanna Dyczka Date: Fri, 22 Mar 2024 12:14:09 +0100 Subject: [PATCH 4/5] [#216] add logic for automated voting options --- .../molecules/AutomatedVotingCard.tsx | 58 ++++++++++++++----- .../src/components/molecules/types.ts | 3 + .../organisms/AutomatedVotingOptions.tsx | 53 ++++++++++++++--- .../src/components/organisms/DRepCard.tsx | 4 +- govtool/frontend/src/i18n/locales/en.ts | 1 + .../src/pages/DRepDirectoryContent.tsx | 47 ++++++++++----- 6 files changed, 128 insertions(+), 38 deletions(-) diff --git a/govtool/frontend/src/components/molecules/AutomatedVotingCard.tsx b/govtool/frontend/src/components/molecules/AutomatedVotingCard.tsx index c566011a5..dd17fa3cf 100644 --- a/govtool/frontend/src/components/molecules/AutomatedVotingCard.tsx +++ b/govtool/frontend/src/components/molecules/AutomatedVotingCard.tsx @@ -1,31 +1,46 @@ import { Box, Divider } from "@mui/material"; -import { Button, Spacer, Typography } from "@atoms"; +import { Button, Typography } from "@atoms"; import { useScreenDimension, useTranslation } from "@hooks"; import { AutomatedVotingCardProps } from "./types"; +import { Card } from "./Card"; +import { primaryBlue } from "@/consts"; +import { useModal } from "@/context"; export const AutomatedVotingCard = ({ description, + inProgress, + isConnected, + isSelected, onClickDelegate, onClickInfo, title, votingPower, }: AutomatedVotingCardProps) => { const { isMobile, screenWidth } = useScreenDimension(); + const { openModal } = useModal(); const { t } = useTranslation(); return ( - `${theme.palette.neutralWhite}40`, + boxShadow: `0px 4px 15px 0px ${primaryBlue.c100}`, display: "flex", flex: 1, flexDirection: screenWidth < 1440 ? "column" : "row", justifyContent: "space-between", - padding: "18px 24px", + mt: inProgress || isSelected ? 2 : 0, + py: 2.25, }} > - - + {!isConnected + ? ( + + ) + : !isSelected && ( + + )} - + ); }; diff --git a/govtool/frontend/src/components/molecules/types.ts b/govtool/frontend/src/components/molecules/types.ts index 62576a774..d2fa9d75f 100644 --- a/govtool/frontend/src/components/molecules/types.ts +++ b/govtool/frontend/src/components/molecules/types.ts @@ -18,6 +18,9 @@ export type StepProps = { export type AutomatedVotingCardProps = { description: string; + inProgress?: boolean; + isConnected?: boolean; + isSelected?: boolean; onClickDelegate: () => void; onClickInfo: () => void; title: string; diff --git a/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx b/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx index ff7503e28..f0482abab 100644 --- a/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx +++ b/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx @@ -3,19 +3,39 @@ import { AccordionDetails, AccordionSummary, Box, + Chip, } from "@mui/material"; import { Typography } from "@atoms"; import { ICONS } from "@consts"; import { useTranslation } from "@hooks"; import { AutomatedVotingCard } from "@molecules"; +import { useState } from "react"; -export const AutomatedVotingOptions = () => { +type AutomatedVotingOptionsProps = { + currentDelegation: string | undefined; + delegate: (delegateTo: string) => void; + delegationInProgress?: string; + isConnected?: boolean; + votingPower: string; +}; + +export const AutomatedVotingOptions = ({ + currentDelegation, + delegate, + delegationInProgress, + isConnected, + votingPower, +}: AutomatedVotingOptionsProps) => { const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + return ( setIsOpen(isExpanded)} sx={(theme) => ({ bgcolor: `${theme.palette.lightBlue}80`, border: `1px solid ${theme.palette.neutralWhite}`, @@ -26,6 +46,19 @@ export const AutomatedVotingOptions = () => { sx={{ borderRadius: 3, px: { xxs: 2, md: 3 } }} > {t("dRepDirectory.automatedVotingOptions")} + {currentDelegation && !isOpen && ( + // TODO this Chip is temporary, since there were no design for this case + theme.palette.neutralWhite, + fontWeight: 400, + ml: 2, + textTransform: 'uppercase', + }} + /> + )} { > {}} - onClickInfo={() => {}} + inProgress={delegationInProgress === "abstain"} + isConnected={isConnected} + isSelected={currentDelegation === "drep_always_abstain"} + onClickDelegate={() => delegate("abstain")} + onClickInfo={() => { }} title={t("dRepDirectory.abstainCardTitle")} - votingPower="99,111,111" + votingPower={votingPower} /> {}} - onClickInfo={() => {}} + inProgress={delegationInProgress === "no confidence"} + isConnected={isConnected} + isSelected={currentDelegation === "drep_always_no_confidence"} + onClickDelegate={() => delegate("no confidence")} + onClickInfo={() => { }} title={t("dRepDirectory.noConfidenceTitle")} - votingPower="99,111,111" + votingPower={votingPower} /> diff --git a/govtool/frontend/src/components/organisms/DRepCard.tsx b/govtool/frontend/src/components/organisms/DRepCard.tsx index c7c6cf4d8..ffc0107cc 100644 --- a/govtool/frontend/src/components/organisms/DRepCard.tsx +++ b/govtool/frontend/src/components/organisms/DRepCard.tsx @@ -35,11 +35,11 @@ export const DRepCard = ({ diff --git a/govtool/frontend/src/i18n/locales/en.ts b/govtool/frontend/src/i18n/locales/en.ts index 993c92855..6be777e67 100644 --- a/govtool/frontend/src/i18n/locales/en.ts +++ b/govtool/frontend/src/i18n/locales/en.ts @@ -659,5 +659,6 @@ export const en = { viewDetails: "View details", votingPower: "Voting power", yes: "Yes", + yourself: "Yourself", }, }; diff --git a/govtool/frontend/src/pages/DRepDirectoryContent.tsx b/govtool/frontend/src/pages/DRepDirectoryContent.tsx index d1cc38b4c..58b8b3daf 100644 --- a/govtool/frontend/src/pages/DRepDirectoryContent.tsx +++ b/govtool/frontend/src/pages/DRepDirectoryContent.tsx @@ -21,7 +21,7 @@ export const DRepDirectoryContent: FC = ({ }) => { const { dRepID: myDRepId, - isEnableLoading, + isEnabled, pendingTransaction, stakeKey, } = useCardano(); @@ -29,19 +29,20 @@ export const DRepDirectoryContent: FC = ({ const { delegate } = useDelegateTodRep(); + const { votingPower } = useGetAdaHolderVotingPowerQuery(); const { currentDelegation } = useGetAdaHolderCurrentDelegationQuery(stakeKey); const inProgressDelegation = pendingTransaction.delegate?.resourceId; - const { data: myDRepList, isLoading: isMyDRepLoading } = useGetDRepListQuery( + const { data: myDRepList } = useGetDRepListQuery( currentDelegation?.startsWith('drep') ? currentDelegation : formHexToBech32(currentDelegation), { enabled: !!inProgressDelegation || !!currentDelegation } ); const myDrep = myDRepList?.[0]; - const { data: dRepList, isLoading: isDRepListLoading } = useGetDRepListQuery(); - - const { votingPower } = useGetAdaHolderVotingPowerQuery(); + const { data: dRepList } = useGetDRepListQuery(); - if (isEnableLoading || isMyDRepLoading || isDRepListLoading) return ; + if (!isEnabled || votingPower === undefined || !dRepList) { + return ; + } const ada = correctAdaFormat(votingPower); @@ -62,18 +63,35 @@ export const DRepDirectoryContent: FC = ({ {isConnected && (
- {t("dRepDirectory.delegationOptions")} - + + {t("dRepDirectory.delegationOptions")} + +
)}
- {t("dRepDirectory.listTitle")} + + {t('dRepDirectory.listTitle')} + - {dRepList?.map((dRep) => (isSameDRep(dRep, myDrep?.view) - ? null - : ( - + {dRepList?.map((dRep) => + (isSameDRep(dRep, myDrep?.view) ? null : ( + = ({ onDelegate={() => delegate(dRep.drepId)} /> - )))} + )), + )}
From 7a28c3bb565e520d380f7c87887e66152dca1749 Mon Sep 17 00:00:00 2001 From: Joanna Dyczka Date: Wed, 27 Mar 2024 01:19:34 +0100 Subject: [PATCH 5/5] [#220] changes after CR - refactor determining StatusPill colors - add Sentry.captureException on delegate error --- .../src/components/atoms/StatusPill.tsx | 32 ++++++------------- .../frontend/src/hooks/useDelegateToDrep.ts | 2 ++ 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/govtool/frontend/src/components/atoms/StatusPill.tsx b/govtool/frontend/src/components/atoms/StatusPill.tsx index ff08844d2..b09fcebf9 100644 --- a/govtool/frontend/src/components/atoms/StatusPill.tsx +++ b/govtool/frontend/src/components/atoms/StatusPill.tsx @@ -24,33 +24,21 @@ export const StatusPill = ({ /> ); -const getBgColor = (status: Status): string => { - switch (status) { - case 'Active': - return successGreen.c200; - case 'Inactive': - return cyan.c100; - case 'Retired': - return errorRed.c100; - // no default - } +const bgColor = { + Active: successGreen.c200, + Inactive: cyan.c100, + Retired: errorRed.c100, }; -const getTextColor = (status: Status): string => { - switch (status) { - case 'Active': - return successGreen.c700; - case 'Inactive': - return cyan.c500; - case 'Retired': - return errorRed.c500; - // no default - } +const textColor = { + Active: successGreen.c700, + Inactive: cyan.c500, + Retired: errorRed.c500, }; const StyledChip = styled(Chip)<{ status: Status }>(({ theme, status }) => ({ - backgroundColor: getBgColor(status), - color: getTextColor(status), + backgroundColor: bgColor[status], + color: textColor[status], border: `2px solid ${theme.palette.neutralWhite}`, fontSize: '0.75rem', textTransform: 'capitalize', diff --git a/govtool/frontend/src/hooks/useDelegateToDrep.ts b/govtool/frontend/src/hooks/useDelegateToDrep.ts index 0183e82de..db43104a5 100644 --- a/govtool/frontend/src/hooks/useDelegateToDrep.ts +++ b/govtool/frontend/src/hooks/useDelegateToDrep.ts @@ -1,4 +1,5 @@ import { useCallback, useState } from "react"; +import * as Sentry from "@sentry/react"; import { useTranslation } from "@hooks"; import { useCardano, useSnackbar } from "@/context"; @@ -26,6 +27,7 @@ export const useDelegateTodRep = () => { addSuccessAlert(t("alerts.delegate.success")); } } catch (error) { + Sentry.captureException(error); addErrorAlert(t("alerts.delegate.failed")); } finally { setIsDelegating(false);