diff --git a/CHANGELOG.md b/CHANGELOG.md index c555e2791..cb3c92587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ changes. - Added `isRegisteredAsSoleVoter` and `wasRegisteredAsSoleVoter` fields to the drep/info response [Issue 212](https://github.com/IntersectMBO/govtool/issues/212) - Abandoning registration as DRep [Issue 151](https://github.com/IntersectMBO/govtool/issues/151) - Abandoning GA creation [Issue 359](https://github.com/IntersectMBO/govtool/issues/359) +- Choose GA type - GA Submiter [Issue 358](https://github.com/IntersectMBO/govtool/issues/358) +- Create Automated Voting Options component [Issue 216](https://github.com/IntersectMBO/govtool/issues/216) +- Change step 3 components [Issue 152](https://github.com/intersectMBO/govtool/issues/152) +- Add possibility to vote on behalf of myself - Sole Voter [Issue 119](https://github.com/IntersectMBO/govtool/issues/119) - Create DRep registration page about roles [Issue 205](https://github.com/IntersectMBO/govtool/issues/205) - Create Checkbox component. Improve Field and ControlledField [Issue 177](https://github.com/IntersectMBO/govtool/pull/177) - Vitest unit tests added for utils functions [Issue 81](https://github.com/IntersectMBO/govtool/issues/81) diff --git a/govtool/frontend/public/icons/DRepDirectory.svg b/govtool/frontend/public/icons/DRepDirectory.svg new file mode 100644 index 000000000..b6cf0553d --- /dev/null +++ b/govtool/frontend/public/icons/DRepDirectory.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/govtool/frontend/public/icons/DRepDirectoryActive.svg b/govtool/frontend/public/icons/DRepDirectoryActive.svg new file mode 100644 index 000000000..7e012314e --- /dev/null +++ b/govtool/frontend/public/icons/DRepDirectoryActive.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/govtool/frontend/src/App.tsx b/govtool/frontend/src/App.tsx index c73ca3bfb..ceb2bda75 100644 --- a/govtool/frontend/src/App.tsx +++ b/govtool/frontend/src/App.tsx @@ -16,6 +16,9 @@ import { Dashboard, DashboardGovernanceActionsCategory, DelegateTodRep, + DRepDetails, + DRepDirectory, + DRepDirectoryContent, ErrorPage, GovernanceActionDetails, GovernanceActions, @@ -104,6 +107,26 @@ export default () => { path={PATHS.dashboardGovernanceActionsCategory} element={} /> + }> + } + /> + } + /> + + + }> + } + /> + } + /> = ({ children, ...props }) => ( + + {children} + +); diff --git a/govtool/frontend/src/components/atoms/PagePaddingBox.tsx b/govtool/frontend/src/components/atoms/PagePaddingBox.tsx new file mode 100644 index 000000000..72f0abc4c --- /dev/null +++ b/govtool/frontend/src/components/atoms/PagePaddingBox.tsx @@ -0,0 +1,8 @@ +import { Box, BoxProps } from "@mui/material"; +import { FC } from "react"; + +export const PagePaddingBox: FC = ({ children, ...props }) => ( + + {children} + +); diff --git a/govtool/frontend/src/components/atoms/StakeRadio.tsx b/govtool/frontend/src/components/atoms/StakeRadio.tsx index 61dd45b39..ab054286a 100644 --- a/govtool/frontend/src/components/atoms/StakeRadio.tsx +++ b/govtool/frontend/src/components/atoms/StakeRadio.tsx @@ -71,6 +71,7 @@ export const StakeRadio: FC = ({ ...props }) => { {t("votingPower")} + : {powerIsLoading ? ( diff --git a/govtool/frontend/src/components/atoms/StatusPill.tsx b/govtool/frontend/src/components/atoms/StatusPill.tsx new file mode 100644 index 000000000..e8fbf0377 --- /dev/null +++ b/govtool/frontend/src/components/atoms/StatusPill.tsx @@ -0,0 +1,44 @@ +import { Chip, ChipProps, styled } from "@mui/material"; +import { cyan, errorRed, successGreen } from "@/consts"; +import { DRepStatus } from "@/models"; + +interface StatusPillProps { + status: DRepStatus; + label?: string; + size?: 'small' | 'medium'; + sx?: ChipProps['sx']; +} + +export const StatusPill = ({ + status, + label = status, + size = 'small', + sx +}: StatusPillProps) => ( + +); + +const bgColor = { + [DRepStatus.Active]: successGreen.c200, + [DRepStatus.Inactive]: cyan.c100, + [DRepStatus.Retired]: errorRed.c100, +}; + +const textColor = { + [DRepStatus.Active]: successGreen.c700, + [DRepStatus.Inactive]: cyan.c500, + [DRepStatus.Retired]: errorRed.c500, +}; + +const StyledChip = styled(Chip)<{ status: DRepStatus }>(({ theme, status }) => ({ + backgroundColor: bgColor[status], + color: textColor[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 186752f80..ceb4bc8db 100644 --- a/govtool/frontend/src/components/atoms/index.ts +++ b/govtool/frontend/src/components/atoms/index.ts @@ -2,6 +2,7 @@ export * from "./ActionRadio"; export * from "./Background"; export * from "./Button"; export * from "./Checkbox"; +export * from "./ContentBox"; export * from "./CopyButton"; export * from "./DrawerLink"; export * from "./ExternalModalButton"; @@ -16,10 +17,12 @@ export * from "./modal/Modal"; export * from "./modal/ModalContents"; export * from "./modal/ModalHeader"; export * from "./modal/ModalWrapper"; +export * from "./PagePaddingBox"; 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/molecules/AutomatedVotingCard.tsx b/govtool/frontend/src/components/molecules/AutomatedVotingCard.tsx new file mode 100644 index 000000000..dd17fa3cf --- /dev/null +++ b/govtool/frontend/src/components/molecules/AutomatedVotingCard.tsx @@ -0,0 +1,128 @@ +import { Box, Divider } from "@mui/material"; + +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", + mt: inProgress || isSelected ? 2 : 0, + py: 2.25, + }} + > + + {title} + + {description} + + + + + + {t("dRepDirectory.votingPower")} + + + {'₳ '} + {votingPower} + + + + + + {!isConnected + ? ( + + ) + : !isSelected && ( + + )} + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/PageTitle.tsx b/govtool/frontend/src/components/molecules/PageTitle.tsx new file mode 100644 index 000000000..6105f2f61 --- /dev/null +++ b/govtool/frontend/src/components/molecules/PageTitle.tsx @@ -0,0 +1,24 @@ +import { useScreenDimension } from "@hooks"; +import { FC } from "react"; +import { Typography, PagePaddingBox, ContentBox } from "@/components/atoms"; + +interface PageTitleProps { + title: string; +} + +export const PageTitle: FC = ({ title }) => { + const { isMobile } = useScreenDimension(); + + return ( + `1px solid ${theme.palette.neutralWhite}`} + py={3} + > + + + {title} + + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/Share.tsx b/govtool/frontend/src/components/molecules/Share.tsx index a57e7763d..39a712842 100644 --- a/govtool/frontend/src/components/molecules/Share.tsx +++ b/govtool/frontend/src/components/molecules/Share.tsx @@ -1,5 +1,5 @@ -import { useState } from "react"; -import { Box, Popover } from "@mui/material"; +import { MouseEvent, useState } from "react"; +import { Box, ButtonBase, Popover } from "@mui/material"; import { Typography } from "@atoms"; import { ICONS } from "@consts"; @@ -9,20 +9,22 @@ import { useTranslation } from "@hooks"; export const Share = ({ link }: { link: string }) => { const { addSuccessAlert } = useSnackbar(); const { t } = useTranslation(); - const [anchorEl, setAnchorEl] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); + const [isActive, setIsActive] = useState(true); - const handleClick = (e: React.MouseEvent) => { - setAnchorEl(e.currentTarget); + const handleClick = (event: MouseEvent) => { + setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; - const onCopy = (e: React.MouseEvent) => { + const onCopy = (event: MouseEvent) => { navigator.clipboard.writeText(link); addSuccessAlert(t("alerts.copiedToClipboard")); - e.stopPropagation(); + setIsActive(false); + event.stopPropagation(); }; const open = Boolean(anchorEl); @@ -30,29 +32,33 @@ export const Share = ({ link }: { link: string }) => { return ( <> - ({ alignItems: "center", - bgcolor: "#F7F9FB", + bgcolor: open ? "#F7F9FB" : "transparent", borderRadius: 50, - boxShadow: "2px 2px 15px 0px #2F62DC47", + boxShadow: open ? theme.shadows[1] : "none", cursor: "pointer", display: "flex", justifyContent: "center", padding: 1.5, - }} + transition: 'all 0.3s', + '&:hover': { + boxShadow: theme.shadows[1], + bgcolor: "#F7F9FB", + } + })} > - share icon - + + { {t("share")} - theme.shadows[1], cursor: "pointer", display: "flex", height: 48, @@ -88,8 +98,8 @@ export const Share = ({ link }: { link: string }) => { }} > link - - {t("clickToCopyLink")} + + {isActive ? t("clickToCopyLink") : t("linkCopied")} diff --git a/govtool/frontend/src/components/molecules/index.ts b/govtool/frontend/src/components/molecules/index.ts index dad2697b0..2bfb3f8f9 100644 --- a/govtool/frontend/src/components/molecules/index.ts +++ b/govtool/frontend/src/components/molecules/index.ts @@ -1,4 +1,5 @@ export * from "./ActionCard"; +export * from "./AutomatedVotingCard"; export * from "./Breadcrumbs"; export * from "./Card"; export * from "./CenteredBoxBottomButtons"; @@ -31,6 +32,8 @@ export * from "./SliderArrow"; export * from "./SliderArrows"; export * from "./SoleVoterAction"; export * from "./Step"; +export * from "./PageTitle"; +export * from "./Share"; export * from "./VoteActionForm"; export * from "./VotesSubmitted"; export * from "./WalletInfoCard"; diff --git a/govtool/frontend/src/components/molecules/types.ts b/govtool/frontend/src/components/molecules/types.ts index f67f05ba1..624129859 100644 --- a/govtool/frontend/src/components/molecules/types.ts +++ b/govtool/frontend/src/components/molecules/types.ts @@ -26,3 +26,14 @@ export type EmptyStateGovernanceActionsCategoryProps = { category?: string; isSearch?: boolean; }; + +export type AutomatedVotingCardProps = { + description: string; + inProgress?: boolean; + isConnected?: boolean; + isSelected?: boolean; + onClickDelegate: () => void; + onClickInfo: () => void; + title: string; + votingPower: string | number; +}; diff --git a/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx b/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx new file mode 100644 index 000000000..f0482abab --- /dev/null +++ b/govtool/frontend/src/components/organisms/AutomatedVotingOptions.tsx @@ -0,0 +1,95 @@ +import { + Accordion, + 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"; + +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}`, + })} + > + } + 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', + }} + /> + )} + + + + delegate("abstain")} + onClickInfo={() => { }} + title={t("dRepDirectory.abstainCardTitle")} + votingPower={votingPower} + /> + delegate("no confidence")} + onClickInfo={() => { }} + title={t("dRepDirectory.noConfidenceTitle")} + votingPower={votingPower} + /> + + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/DRepCard.tsx b/govtool/frontend/src/components/organisms/DRepCard.tsx new file mode 100644 index 000000000..786969754 --- /dev/null +++ b/govtool/frontend/src/components/organisms/DRepCard.tsx @@ -0,0 +1,167 @@ +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, PATHS } from "@/consts"; +import { DRepData } from "@/models"; +import { useSnackbar } from "@/context"; + +type DRepCardProps = { + dRep: DRepData; + isConnected: boolean; + isInProgress?: boolean; + isMe?: boolean; + onDelegate?: () => void; +} + +export const DRepCard = ({ + dRep: { + status, + type, + view, + votingPower, + }, + isConnected, + isInProgress, + isMe, + onDelegate, +}: DRepCardProps) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + const { addSuccessAlert } = useSnackbar(); + + return ( + + + + + + {type} + { + navigator.clipboard.writeText(view); + addSuccessAlert(t("alerts.copiedToClipboard")); + e.stopPropagation(); + }} + sx={{ + gap: 1, + maxWidth: "100%", + "&:hover": { + opacity: 0.6, + transition: "opacity 0.3s", + }, + }} + > + + {view} + + + + + + + + + {t("votingPower")} + + + ₳ + {' '} + {correctAdaFormat(votingPower)} + + + ({ borderColor: palette.lightBlue })} + /> + + + {t("status")} + + + + + + + + + + {status === "Active" && isConnected && onDelegate && ( + + )} + {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 f4931d142..6f12d1e6f 100644 --- a/govtool/frontend/src/components/organisms/index.ts +++ b/govtool/frontend/src/components/organisms/index.ts @@ -1,3 +1,4 @@ +export * from "./AutomatedVotingOptions"; export * from "./BgCard"; export * from "./ChooseStakeKeyPanel"; export * from "./ChooseWalletModal"; @@ -14,6 +15,7 @@ export * from "./DelegateTodRepStepOne"; export * from "./DelegateTodRepStepTwo"; export * from "./Drawer"; export * from "./DrawerMobile"; +export * from "./DRepCard"; export * from "./EditDRepInfoSteps"; export * from "./ExternalLinkModal"; export * from "./Footer"; diff --git a/govtool/frontend/src/consts/dRepDirectory/filters.ts b/govtool/frontend/src/consts/dRepDirectory/filters.ts new file mode 100644 index 000000000..f6a089f8e --- /dev/null +++ b/govtool/frontend/src/consts/dRepDirectory/filters.ts @@ -0,0 +1,6 @@ +import { DRepStatus } from "@/models"; + +export const DREP_DIRECTORY_FILTERS = Object.values(DRepStatus).map((status) => ({ + key: status, + label: status, +})); diff --git a/govtool/frontend/src/consts/dRepDirectory/index.ts b/govtool/frontend/src/consts/dRepDirectory/index.ts new file mode 100644 index 000000000..93e21800d --- /dev/null +++ b/govtool/frontend/src/consts/dRepDirectory/index.ts @@ -0,0 +1,2 @@ +export * from "./filters"; +export * from "./sorting"; diff --git a/govtool/frontend/src/consts/dRepDirectory/sorting.ts b/govtool/frontend/src/consts/dRepDirectory/sorting.ts new file mode 100644 index 000000000..ab5b531bb --- /dev/null +++ b/govtool/frontend/src/consts/dRepDirectory/sorting.ts @@ -0,0 +1,14 @@ +export const DREP_DIRECTORY_SORTING = [ + { + key: "NewestRegistered", + label: "Newest registered", + }, + { + key: "VotingPower", + label: "Voting power", + }, + { + key: "Status", + label: "Status", + }, +]; diff --git a/govtool/frontend/src/consts/icons.ts b/govtool/frontend/src/consts/icons.ts index dfabf260d..02c6d5740 100644 --- a/govtool/frontend/src/consts/icons.ts +++ b/govtool/frontend/src/consts/icons.ts @@ -15,6 +15,8 @@ export const ICONS = { dashboardIcon: "/icons/Dashboard.svg", download: "/icons/Download.svg", drawerIcon: "/icons/DrawerIcon.svg", + dRepDirectoryActiveIcon: "/icons/DRepDirectoryActive.svg", + dRepDirectoryIcon: "/icons/DRepDirectory.svg", externalLinkIcon: "/icons/ExternalLink.svg", faqsActiveIcon: "/icons/FaqsActive.svg", faqsIcon: "/icons/Faqs.svg", diff --git a/govtool/frontend/src/consts/index.ts b/govtool/frontend/src/consts/index.ts index 22a9c3bbc..87b876b2e 100644 --- a/govtool/frontend/src/consts/index.ts +++ b/govtool/frontend/src/consts/index.ts @@ -1,6 +1,7 @@ export * from "./externalDataModalConfig"; export * from "./colors"; export * from "./dRepActions"; +export * from "./dRepDirectory"; export * from "./governanceAction"; export * from "./icons"; export * from "./images"; diff --git a/govtool/frontend/src/consts/navItems.ts b/govtool/frontend/src/consts/navItems.ts index 1c3d0ec37..4c6922b32 100644 --- a/govtool/frontend/src/consts/navItems.ts +++ b/govtool/frontend/src/consts/navItems.ts @@ -1,3 +1,4 @@ +import i18n from "@/i18n"; import { ICONS } from "./icons"; import { PATHS } from "./paths"; @@ -5,25 +6,30 @@ export const NAV_ITEMS = [ { dataTestId: "dashboard-link", navTo: PATHS.home, - label: "Dashboard", + label: i18n.t("dashboard.title"), newTabLink: null, }, + { + dataTestId: "drep-directory-link", + navTo: PATHS.dRepDirectory, + label: i18n.t("dRepDirectory.title"), + }, { dataTestId: "governance-actions-link", navTo: PATHS.governanceActions, - label: "Governance Actions", + label: i18n.t("govActions.title"), newTabLink: null, }, { dataTestId: "guides-link", navTo: "", - label: "Guides", + label: i18n.t("menu.guides"), newTabLink: "https://docs.sanchogov.tools/about/what-is-sanchonet-govtool", }, { dataTestId: "faqs-link", navTo: "", - label: "FAQs", + label: i18n.t("menu.faqs"), newTabLink: "https://docs.sanchogov.tools/faqs", }, ]; @@ -31,15 +37,22 @@ export const NAV_ITEMS = [ export const CONNECTED_NAV_ITEMS = [ { dataTestId: "dashboard-link", - label: "Dashboard", + label: i18n.t("dashboard.title"), navTo: PATHS.dashboard, activeIcon: ICONS.dashboardActiveIcon, icon: ICONS.dashboardIcon, newTabLink: null, }, + { + dataTestId: "drep-directory-link", + label: i18n.t("dRepDirectory.title"), + navTo: PATHS.dashboardDRepDirectory, + activeIcon: ICONS.dRepDirectoryActiveIcon, + icon: ICONS.dRepDirectoryIcon, + }, { dataTestId: "governance-actions-link", - label: "Governance Actions", + label: i18n.t("govActions.title"), navTo: PATHS.dashboardGovernanceActions, activeIcon: ICONS.governanceActionsActiveIcon, icon: ICONS.governanceActionsIcon, @@ -47,7 +60,7 @@ export const CONNECTED_NAV_ITEMS = [ }, { dataTestId: "guides-link", - label: "Guides", + label: i18n.t("menu.guides"), navTo: "", activeIcon: ICONS.guidesActiveIcon, icon: ICONS.guidesIcon, @@ -55,7 +68,7 @@ export const CONNECTED_NAV_ITEMS = [ }, { dataTestId: "faqs-link", - label: "FAQs", + label: i18n.t("menu.faqs"), navTo: "", activeIcon: ICONS.faqsActiveIcon, icon: ICONS.faqsIcon, diff --git a/govtool/frontend/src/consts/paths.ts b/govtool/frontend/src/consts/paths.ts index 3361f31a3..7fb2d92f4 100644 --- a/govtool/frontend/src/consts/paths.ts +++ b/govtool/frontend/src/consts/paths.ts @@ -5,7 +5,11 @@ export const PATHS = { dashboardGovernanceActionsAction: "/connected/governance_actions/:proposalId", dashboardGovernanceActionsCategory: "/connected/governance_actions/category/:category", + dashboardDRepDirectory: "/connected/drep_directory", + dashboardDRepDirectoryDRep: "/connected/drep_directory/:dRepId", delegateTodRep: "/delegate", + dRepDirectory: "/drep_directory", + dRepDirectoryDRep: "/drep_directory/:dRepId", editDrepMetadata: "/edit_drep", error: "/error", faqs: "/faqs", diff --git a/govtool/frontend/src/hooks/index.ts b/govtool/frontend/src/hooks/index.ts index da6b8743b..8fdb31e31 100644 --- a/govtool/frontend/src/hooks/index.ts +++ b/govtool/frontend/src/hooks/index.ts @@ -2,6 +2,7 @@ export { useTranslation } from "react-i18next"; export * from "./useDataActionsBar"; export * from "./useDebounce"; +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 806d55072..2d5882fe8 100644 --- a/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetDRepListQuery.ts @@ -1,22 +1,35 @@ -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 { GetDRepListParams, getDRepList } from "@services"; +import { DRepData } from "@/models"; -export const useGetDRepListQuery = () => { +export const useGetDRepListQuery = ( + params?: GetDRepListParams, + options?: UseQueryOptions +) => { + const { drepView, sort, status } = params || {}; const { pendingTransaction } = useCardano(); - const { data, isLoading } = useQuery({ + const { data, isLoading, isPreviousData } = useQuery({ queryKey: [ QUERY_KEYS.useGetDRepListKey, (pendingTransaction.registerAsSoleVoter || pendingTransaction.registerAsDrep || pendingTransaction.retireAsSoleVoter || pendingTransaction.retireAsDrep)?.transactionHash, + drepView, + sort, + status, ], - queryFn: getDRepList, + queryFn: () => getDRepList({ + ...(drepView && { drepView }), + ...(sort && { sort }), + ...(status && { status }), + }), + ...options }); - return { data, isLoading }; + return { data, isLoading, isPreviousData }; }; diff --git a/govtool/frontend/src/hooks/useDelegateToDrep.ts b/govtool/frontend/src/hooks/useDelegateToDrep.ts new file mode 100644 index 000000000..db43104a5 --- /dev/null +++ b/govtool/frontend/src/hooks/useDelegateToDrep.ts @@ -0,0 +1,47 @@ +import { useCallback, useState } from "react"; +import * as Sentry from "@sentry/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) { + Sentry.captureException(error); + addErrorAlert(t("alerts.delegate.failed")); + } finally { + setIsDelegating(false); + } + }, [ + addErrorAlert, + addSuccessAlert, + buildSignSubmitConwayCertTx, + buildVoteDelegationCert, + t, + ]); + + return { + delegate, + isDelegating, + }; +}; diff --git a/govtool/frontend/src/i18n/locales/en.ts b/govtool/frontend/src/i18n/locales/en.ts index 1188ff912..8debf7007 100644 --- a/govtool/frontend/src/i18n/locales/en.ts +++ b/govtool/frontend/src/i18n/locales/en.ts @@ -249,6 +249,25 @@ export const en = { title: "Delegate to myself", }, }, + dRepDirectory: { + abstainCardDescription: "Select this to vote ABSTAIN to every vote.", + abstainCardTitle: "Abstain from Every Vote", + automatedVotingOptions: "Automated Voting Options", + editBtn: "Edit DRep data", + delegationOptions: "Delegation Options", + filterTitle: "DRep Status", + 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", + noResultsForTheSearchTitle: "No DReps found", + noResultsForTheSearchDescription: "Please try a different search", + title: "DRep Directory", + votingPower: "Voting Power", + }, errorPage: { backToDashboard: "Back to dashboard", backToHomepage: "Back to homepage", @@ -450,8 +469,6 @@ export const en = { faqs: "FAQs", guides: "Guides", help: "Help", - dashboard: "Dashboard", - viewGovActions: "View Governance Actions", }, metadataUpdate: { description: @@ -696,6 +713,7 @@ export const en = { usingUnregisteredStakeKeys: "Warning, no registered stake keys, using unregistered stake keys", }, + about: "About", abstain: "Abstain", addLink: "+ Add link", back: "Back", @@ -706,15 +724,22 @@ export const en = { clickToCopyLink: "Click to copy link", close: "Close", confirm: "Confirm", + connectToDelegate: "Connect to delegate", continue: "Continue", + copiedLink: "Copied link", delegate: "Delegate", + drepId: "DRep ID", + email: "Email", filter: "Filter", goBack: "Go back", here: "here", - inProgress: "In Progress", + inProgress: "In progress", + info: "Info", learnMore: "Learn more", + linkCopied: "Link copied", loading: "Loading...", - myDRepId: "My dRep ID:", + moreInformation: "More information", + myDRepId: "My DRep ID:", nextStep: "Next step", no: "No", ok: "Ok", @@ -729,9 +754,12 @@ export const en = { skip: "Skip", sort: "Sort", sortBy: "Sort by", + status: "Status", submit: "Submit", thisLink: "this link", - votingPower: "Voting power:", + viewDetails: "View details", + votingPower: "Voting power", yes: "Yes", + yourself: "Yourself", }, }; diff --git a/govtool/frontend/src/models/api.ts b/govtool/frontend/src/models/api.ts index 7f9b24e9a..0686601b7 100644 --- a/govtool/frontend/src/models/api.ts +++ b/govtool/frontend/src/models/api.ts @@ -8,11 +8,21 @@ export interface VoterInfo { deposit: number; } +export enum DRepStatus { + Active = "Active", + Inactive = "Inactive", + Retired = "Retired", +} + export interface DRepData { drepId: string; + view: string; url: string; metadataHash: string; deposit: number; + votingPower: number; + status: DRepStatus; + type: 'DRep' | 'SoleVoter'; } export type Vote = "yes" | "no" | "abstain"; diff --git a/govtool/frontend/src/pages/DRepDetails.tsx b/govtool/frontend/src/pages/DRepDetails.tsx new file mode 100644 index 000000000..690634bcc --- /dev/null +++ b/govtool/frontend/src/pages/DRepDetails.tsx @@ -0,0 +1,287 @@ +import { PropsWithChildren } from "react"; +import { Navigate, useNavigate, useParams } from "react-router-dom"; +import { Box, ButtonBase, Chip, CircularProgress } from "@mui/material"; + +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 } from "@/context"; +import { isSameDRep } from "@/utils"; + +const LINKS = [ + "darlenelonglink1.DRepwebsiteorwhatever.com", + "darlenelonglink2.DRepwebsiteorwhatever.com", + "darlenelonglink3.DRepwebsiteorwhatever.com", + "darlenelonglink4.DRepwebsiteorwhatever.com", + "darlenelonglink5.DRepwebsiteorwhatever.com", +]; + +type DRepDetailsProps = { + isConnected?: boolean; +}; + +export const DRepDetails = ({ isConnected }: DRepDetailsProps) => { + const { dRepID: myDRepId, pendingTransaction, stakeKey } = useCardano(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const { openModal } = useModal(); + const { screenWidth } = useScreenDimension(); + const { dRepId: dRepParam } = useParams(); + + const { delegate, isDelegating } = useDelegateTodRep(); + + const { currentDelegation } = useGetAdaHolderCurrentDelegationQuery(stakeKey); + const { data, isLoading } = useGetDRepListQuery({ drepView: dRepParam }); + const dRep = data?.[0]; + + if (!dRep && isLoading) + return ; + + if (!dRep) return ; + + const { view, status, votingPower, type } = dRep; + + const isMe = isSameDRep(dRep, myDRepId); + const isMyDrep = isSameDRep(dRep, currentDelegation); + const isMyDrepInProgress = isSameDRep( + dRep, + pendingTransaction.delegate?.resourceId, + ); + + return ( + <> + + navigate( + isConnected ? PATHS.dashboardDRepDirectory : PATHS.dRepDirectory, + ) + } + sx={{ mb: 2 }} + /> + + {(isMe || isMyDrep) && ( + theme.shadows[2], + color: (theme) => theme.palette.text.primary, + mb: 1.5, + px: 2, + py: 0.5, + width: "100%", + }} + /> + )} + + + {type} + + {isMe && ( + + )} + + + + + + {view} + + + + + + + {"₳ "} + {correctAdaFormat(votingPower)} + + + {/* TODO: fetch metadata, add views for metadata errors */} + + + + + + {LINKS.map((link) => ( + + ))} + + + + + + {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. + + + + ); +}; + +const ellipsisStyles = { + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", +}; + +type DrepDetailsInfoItemProps = PropsWithChildren & { + label: string; +}; + +const DRepDetailsInfoItem = ({ children, label }: DrepDetailsInfoItemProps) => ( + <> + + + {label} + + + + {children} + + +); + +const DRepId = ({ children }: PropsWithChildren) => ( + + + {children} + + + +); + +type LinkWithIconProps = { + label: string; + navTo: string; +}; + +const MoreInfoLink = ({ label, navTo }: LinkWithIconProps) => { + const openLink = () => openInNewTab(navTo); + + return ( + + link + + {label} + + + ); +}; diff --git a/govtool/frontend/src/pages/DRepDirectory.tsx b/govtool/frontend/src/pages/DRepDirectory.tsx new file mode 100644 index 000000000..d3569f848 --- /dev/null +++ b/govtool/frontend/src/pages/DRepDirectory.tsx @@ -0,0 +1,34 @@ +import { useTranslation } from "@hooks"; +import { Outlet } from "react-router-dom"; +import { checkIsWalletConnected } from "@/utils"; +import { Background, PagePaddingBox, ContentBox } from "@/components/atoms"; +import { TopNav } from "@/components/organisms"; +import { PageTitle } from "@/components/molecules"; + +export const DRepDirectory = () => { + const { t } = useTranslation(); + + const isConnected = !checkIsWalletConnected(); + + if (isConnected) { + return ( + + + + ); + } + + return ( + + + + + + + + + + + + ); +}; diff --git a/govtool/frontend/src/pages/DRepDirectoryContent.tsx b/govtool/frontend/src/pages/DRepDirectoryContent.tsx new file mode 100644 index 000000000..08c14283d --- /dev/null +++ b/govtool/frontend/src/pages/DRepDirectoryContent.tsx @@ -0,0 +1,154 @@ +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 { Card, DataActionsBar } from "@molecules"; +import { useCardano } from "@/context"; +import { + useDataActionsBar, + useDelegateTodRep, + useGetAdaHolderCurrentDelegationQuery, + useGetAdaHolderVotingPowerQuery, + useGetDRepListQuery +} from "@/hooks"; +import { correctAdaFormat, formHexToBech32, isSameDRep } from "@/utils"; +import { DREP_DIRECTORY_FILTERS, DREP_DIRECTORY_SORTING } from "@/consts"; + +interface DRepDirectoryContentProps { + isConnected?: boolean; +} + +export const DRepDirectoryContent: FC = ({ + isConnected, +}) => { + const { + dRepID: myDRepId, + pendingTransaction, + stakeKey, + } = useCardano(); + const { t } = useTranslation(); + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); + const { chosenFilters, chosenSorting } = dataActionsBarProps; + + const { delegate } = useDelegateTodRep(); + + const { votingPower } = useGetAdaHolderVotingPowerQuery(); + const { currentDelegation } = useGetAdaHolderCurrentDelegationQuery(stakeKey); + const inProgressDelegation = pendingTransaction.delegate?.resourceId; + + const { data: myDRepList } = useGetDRepListQuery( + { drepView: currentDelegation?.startsWith('drep') + ? currentDelegation + : formHexToBech32(currentDelegation) }, + { enabled: !!inProgressDelegation || !!currentDelegation } + ); + const myDrep = myDRepList?.[0]; + const { data: dRepList, isPreviousData } = useGetDRepListQuery({ + drepView: debouncedSearchText, + sort: chosenSorting, + status: chosenFilters, + }, { + keepPreviousData: true, + }); + + if (stakeKey && votingPower === undefined) { + return ; + } + + const ada = correctAdaFormat(votingPower); + + return ( + + {/* My delegation */} + {myDrep && ( +
+ + + + +
+ )} + + {/* Automated voting options */} + {isConnected && ( +
+ + {t("dRepDirectory.delegationOptions")} + + +
+ )} + + {/* DRep list */} +
+ + {t('dRepDirectory.listTitle')} + + + + {dRepList?.length === 0 && ( + + {t('dRepDirectory.noResultsForTheSearchTitle')} + {t('dRepDirectory.noResultsForTheSearchDescription')} + + )} + {dRepList?.map((dRep) => + (isSameDRep(dRep, myDrep?.view) ? null : ( + + delegate(dRep.drepId)} + /> + + )), + )} + +
+
+ ); +}; diff --git a/govtool/frontend/src/pages/Dashboard.tsx b/govtool/frontend/src/pages/Dashboard.tsx index 80c9797b1..818165757 100644 --- a/govtool/frontend/src/pages/Dashboard.tsx +++ b/govtool/frontend/src/pages/Dashboard.tsx @@ -3,7 +3,7 @@ import { useLocation, Outlet, useNavigate } from "react-router-dom"; import { Box } from "@mui/material"; import { Background, ScrollToManage } from "@atoms"; -import { PATHS } from "@consts"; +import { CONNECTED_NAV_ITEMS, PATHS } from "@consts"; import { useCardano } from "@context"; import { useScreenDimension, useTranslation } from "@hooks"; import { DashboardTopNav, Drawer, Footer } from "@organisms"; @@ -18,13 +18,11 @@ export const Dashboard = () => { const { t } = useTranslation(); const getPageTitle = (path: string) => { - if (path === PATHS.dashboard) { - return t("dashboard.title"); - } - if (path.includes(PATHS.dashboardGovernanceActions)) { - return t("govActions.title"); - } - return ""; + if (path === PATHS.dashboard) return t("dashboard.title"); + return ( + Object.values(CONNECTED_NAV_ITEMS).find(({ navTo }) => pathname.startsWith(navTo)) + ?.label ?? "" + ); }; useEffect(() => { diff --git a/govtool/frontend/src/pages/index.ts b/govtool/frontend/src/pages/index.ts index d1878f196..2507933ef 100644 --- a/govtool/frontend/src/pages/index.ts +++ b/govtool/frontend/src/pages/index.ts @@ -1,14 +1,19 @@ export * from "./ChooseStakeKey"; export * from "./CreateGovernanceAction"; +export * from "./DRepDetails"; +export * from "./DRepDirectory"; +export * from "./DRepDirectoryContent"; export * from "./Dashboard"; export * from "./DashboardGovernanceActionsCategory"; export * from "./DelegateTodRep"; +export * from "./DRepDirectory"; export * from "./EditDRepMetadata"; export * from "./ErrorPage"; export * from "./GovernanceActionDetails"; export * from "./GovernanceActions"; export * from "./GovernanceActionsCategory"; export * from "./Home"; +export * from "./RegisterAsSoleVoter"; export * from "./RegisterAsdRep"; export * from "./RegisterAsSoleVoter"; export * from "./RetireAsDrep"; diff --git a/govtool/frontend/src/services/requests/getDRepList.ts b/govtool/frontend/src/services/requests/getDRepList.ts index 39ab28ecb..c4c0af028 100644 --- a/govtool/frontend/src/services/requests/getDRepList.ts +++ b/govtool/frontend/src/services/requests/getDRepList.ts @@ -1,7 +1,13 @@ import type { DRepData } from "@models"; import { API } from "../API"; -export const getDRepList = async () => { - const response = await API.get("/drep/list"); +export type GetDRepListParams = { + drepView?: string; + sort?: string; + status?: string[]; +}; + +export const getDRepList = async (params: GetDRepListParams) => { + const response = await API.get("/drep/list", { params }); return response.data; }; diff --git a/govtool/frontend/src/stories/StatusPill.stories.ts b/govtool/frontend/src/stories/StatusPill.stories.ts new file mode 100644 index 000000000..1a8467cad --- /dev/null +++ b/govtool/frontend/src/stories/StatusPill.stories.ts @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { StatusPill } from "@atoms"; +import { DRepStatus } from "@/models"; + +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: DRepStatus.Active, + }, +}; + +export const StatusPillInactive: Story = { + args: { + status: DRepStatus.Inactive, + }, +}; + +export const StatusPillRetired: Story = { + args: { + status: DRepStatus.Retired, + }, +}; diff --git a/govtool/frontend/src/theme.ts b/govtool/frontend/src/theme.ts index 7b9bd2331..910447fab 100644 --- a/govtool/frontend/src/theme.ts +++ b/govtool/frontend/src/theme.ts @@ -1,12 +1,6 @@ import { createTheme } from "@mui/material/styles"; import { - cyan, - errorRed, - fadedPurple, - orange, - primaryBlue, - progressYellow, - successGreen, + cyan, errorRed, orange, primaryBlue, progressYellow, successGreen, } from "./consts"; export type Theme = typeof theme; @@ -23,6 +17,13 @@ export const theme = createTheme({ }, }, components: { + MuiAccordion: { + styleOverrides: { + root: { + borderRadius: `12px !important`, + } + } + }, MuiInputBase: { styleOverrides: { root: { @@ -51,7 +52,7 @@ export const theme = createTheme({ { props: { color: "default", variant: "filled" }, style: { - backgroundColor: fadedPurple.c100, + backgroundColor: primaryBlue.c50 }, }, { @@ -106,6 +107,11 @@ export const theme = createTheme({ }, }, }, + MuiPopover: { + defaultProps: { + elevation: 2, + } + }, }, typography: { fontFamily: "Poppins, Arial", 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 71d82b13f..22dc4a84f 100644 --- a/govtool/frontend/src/utils/index.ts +++ b/govtool/frontend/src/utils/index.ts @@ -6,6 +6,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";