From 7f9e84cc44316f3266cf363548f1001524182c1b Mon Sep 17 00:00:00 2001 From: Joanna Dyczka Date: Tue, 26 Mar 2024 15:54:17 +0100 Subject: [PATCH 01/26] [#536] add BE search for governance actions --- .../organisms/DashboardGovernanceActions.tsx | 41 +++---------- govtool/frontend/src/hooks/index.ts | 3 + .../queries/useGetProposalsInfiniteQuery.ts | 7 ++- .../src/hooks/queries/useGetProposalsQuery.ts | 51 +++++++--------- .../frontend/src/hooks/useDataActionsBar.tsx | 58 +++++++++++++++++++ govtool/frontend/src/hooks/useDebounce.ts | 17 ++++++ .../DashboardGovernanceActionsCategory.tsx | 34 +++-------- .../frontend/src/pages/GovernanceActions.tsx | 39 +++---------- .../src/pages/GovernanceActionsCategory.tsx | 38 +++--------- .../src/services/requests/getProposals.ts | 33 +++++------ govtool/frontend/src/types/global.d.ts | 3 + 11 files changed, 151 insertions(+), 173 deletions(-) create mode 100644 govtool/frontend/src/hooks/useDataActionsBar.tsx create mode 100644 govtool/frontend/src/hooks/useDebounce.ts diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx index f9b22bc1e..1ce96990d 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx @@ -1,10 +1,11 @@ -import { useState, useCallback, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Box, CircularProgress, Tab, Tabs, styled } from "@mui/material"; import { useLocation } from "react-router-dom"; import { GOVERNANCE_ACTIONS_FILTERS } from "@consts"; import { useCardano } from "@context"; import { + useDataActionsBar, useGetProposalsQuery, useGetVoterInfo, useScreenDimension, @@ -64,11 +65,8 @@ const StyledTab = styled((props: StyledTabProps) => ( })); export const DashboardGovernanceActions = () => { - const [searchText, setSearchText] = useState(""); - const [filtersOpen, setFiltersOpen] = useState(false); - const [chosenFilters, setChosenFilters] = useState([]); - const [sortOpen, setSortOpen] = useState(false); - const [chosenSorting, setChosenSorting] = useState(""); + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); + const { chosenFilters, chosenSorting } = dataActionsBarProps; const { voter } = useGetVoterInfo(); const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -80,7 +78,7 @@ export const DashboardGovernanceActions = () => { const { proposals, isProposalsLoading } = useGetProposalsQuery({ filters: queryFilters, sorting: chosenSorting, - searchPhrase: searchText, + searchPhrase: debouncedSearchText, }); const { state } = useLocation(); @@ -92,14 +90,6 @@ export const DashboardGovernanceActions = () => { setContent(newValue); }; - const closeFilters = useCallback(() => { - setFiltersOpen(false); - }, [setFiltersOpen]); - - const closeSorts = useCallback(() => { - setSortOpen(false); - }, [setSortOpen]); - useEffect(() => { window.history.replaceState({}, document.title); }, []); @@ -113,22 +103,7 @@ export const DashboardGovernanceActions = () => { flexDirection="column" > <> - + {!proposals || !voter || isEnableLoading || isProposalsLoading ? ( { @@ -185,7 +160,7 @@ export const DashboardGovernanceActions = () => { diff --git a/govtool/frontend/src/hooks/index.ts b/govtool/frontend/src/hooks/index.ts index 126c2d0c9..da6b8743b 100644 --- a/govtool/frontend/src/hooks/index.ts +++ b/govtool/frontend/src/hooks/index.ts @@ -1,4 +1,7 @@ export { useTranslation } from "react-i18next"; + +export * from "./useDataActionsBar"; +export * from "./useDebounce"; export * from "./useFetchNextPageDetector"; export * from "./useOutsideClick"; export * from "./useSaveScrollPosition"; diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts index 7e141842e..fee1e9482 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsInfiniteQuery.ts @@ -2,13 +2,14 @@ import { useInfiniteQuery } from "react-query"; import { QUERY_KEYS } from "@consts"; import { useCardano } from "@context"; -import { getProposals, getProposalsArguments } from "@services"; +import { getProposals, GetProposalsArguments } from "@services"; export const useGetProposalsInfiniteQuery = ({ filters = [], pageSize = 10, + searchPhrase, sorting = "", -}: getProposalsArguments) => { +}: GetProposalsArguments) => { const { dRepID, isEnabled, pendingTransaction } = useCardano(); const fetchProposals = ({ pageParam = 0 }) => @@ -17,6 +18,7 @@ export const useGetProposalsInfiniteQuery = ({ filters, page: pageParam, pageSize, + searchPhrase, sorting, }); @@ -34,6 +36,7 @@ export const useGetProposalsInfiniteQuery = ({ filters, isEnabled, pendingTransaction.vote?.transactionHash, + searchPhrase, sorting, ], fetchProposals, diff --git a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts index 242add713..981cd0b55 100644 --- a/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts +++ b/govtool/frontend/src/hooks/queries/useGetProposalsQuery.ts @@ -2,20 +2,19 @@ import { useQuery } from "react-query"; import { QUERY_KEYS } from "@consts"; import { useCardano } from "@context"; -import { getProposals, getProposalsArguments } from "@services"; -import { getFullGovActionId } from "@utils"; +import { getProposals, GetProposalsArguments } from "@services"; export const useGetProposalsQuery = ({ filters = [], - sorting, searchPhrase, -}: getProposalsArguments) => { + sorting, +}: GetProposalsArguments) => { const { dRepID, pendingTransaction } = useCardano(); const fetchProposals = async (): Promise => { const allProposals = await Promise.all( filters.map((filter) => - getProposals({ dRepID, filters: [filter], sorting }), + getProposals({ dRepID, filters: [filter], searchPhrase, sorting }), ), ); @@ -26,6 +25,7 @@ export const useGetProposalsQuery = ({ [ QUERY_KEYS.useGetProposalsKey, filters, + searchPhrase, sorting, dRepID, pendingTransaction.vote?.transactionHash, @@ -33,38 +33,27 @@ export const useGetProposalsQuery = ({ fetchProposals, ); - const mappedData = Object.values( - (groupedByType( - data?.filter((i) => - getFullGovActionId(i.txHash, i.index) - .toLowerCase() - .includes(searchPhrase.toLowerCase())) - ) ?? []) as ToVoteDataType + const proposals = Object.values( + (groupByType(data) ?? []) ); return { isProposalsLoading: isLoading, - proposals: mappedData, + proposals, }; }; -const groupedByType = (data?: ActionType[]) => data?.reduce((groups, item) => { - const itemType = item.type; +const groupByType = (data?: ActionType[]) => + data?.reduce>>((groups, item) => { + const itemType = item.type; - // TODO: Provide better typing for groups - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - if (!groups[itemType]) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - groups[itemType] = { - title: itemType, - actions: [], - }; - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - groups[itemType].actions.push(item); + if (!groups[itemType]) { + groups[itemType] = { + title: itemType, + actions: [], + }; + } + groups[itemType].actions.push(item); - return groups; -}, {}); + return groups; + }, {}); diff --git a/govtool/frontend/src/hooks/useDataActionsBar.tsx b/govtool/frontend/src/hooks/useDataActionsBar.tsx new file mode 100644 index 000000000..61723d0ea --- /dev/null +++ b/govtool/frontend/src/hooks/useDataActionsBar.tsx @@ -0,0 +1,58 @@ +import { useState, useCallback, Dispatch, SetStateAction } from "react"; + +import { + useDebounce, +} from "@hooks"; + +type UseDataActionsBarReturnType = { + chosenFilters: string[]; + chosenFiltersLength: number; + chosenSorting: string; + closeFilters: () => void; + closeSorts: () => void; + debouncedSearchText: string; + filtersOpen: boolean; + searchText: string; + setChosenFilters: Dispatch>; + setChosenSorting: Dispatch>; + setFiltersOpen: Dispatch>; + setSearchText: Dispatch>; + setSortOpen: Dispatch>; + sortingActive: boolean; + sortOpen: boolean; +}; + +export const useDataActionsBar = (): UseDataActionsBarReturnType => { + const [searchText, setSearchText] = useState(""); + const debouncedSearchText = useDebounce(searchText, 300); + const [filtersOpen, setFiltersOpen] = useState(false); + const [chosenFilters, setChosenFilters] = useState([]); + const [sortOpen, setSortOpen] = useState(false); + const [chosenSorting, setChosenSorting] = useState(""); + + const closeFilters = useCallback(() => { + setFiltersOpen(false); + }, [setFiltersOpen]); + + const closeSorts = useCallback(() => { + setSortOpen(false); + }, [setSortOpen]); + + return { + chosenFilters, + chosenFiltersLength: chosenFilters.length, + chosenSorting, + closeFilters, + closeSorts, + debouncedSearchText, + filtersOpen, + searchText, + setChosenFilters, + setChosenSorting, + setFiltersOpen, + setSearchText, + setSortOpen, + sortingActive: Boolean(chosenSorting), + sortOpen, + }; +}; diff --git a/govtool/frontend/src/hooks/useDebounce.ts b/govtool/frontend/src/hooks/useDebounce.ts new file mode 100644 index 000000000..335b5cf26 --- /dev/null +++ b/govtool/frontend/src/hooks/useDebounce.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +export function useDebounce(value: T, delay: number) { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timerID = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(timerID); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx b/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx index 47384da36..8ae9f49e7 100644 --- a/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx +++ b/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from "react"; +import { useMemo, useRef } from "react"; import { generatePath, useNavigate, useParams } from "react-router-dom"; import { Box, CircularProgress, Link } from "@mui/material"; @@ -7,6 +7,7 @@ import { ICONS, PATHS } from "@consts"; import { useCardano } from "@context"; import { DataActionsBar, GovernanceActionCard } from "@molecules"; import { + useDataActionsBar, useFetchNextPageDetector, useGetProposalsInfiniteQuery, useGetVoterInfo, @@ -23,9 +24,8 @@ import { export const DashboardGovernanceActionsCategory = () => { const { category } = useParams(); - const [searchText, setSearchText] = useState(""); - const [sortOpen, setSortOpen] = useState(false); - const [chosenSorting, setChosenSorting] = useState(""); + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); + const { chosenSorting } = dataActionsBarProps; const { isMobile, screenWidth } = useScreenDimension(); const navigate = useNavigate(); const { pendingTransaction, isEnableLoading } = useCardano(); @@ -42,7 +42,7 @@ export const DashboardGovernanceActionsCategory = () => { } = useGetProposalsInfiniteQuery({ filters: [category?.replace(/ /g, "") ?? ""], sorting: chosenSorting, - searchPhrase: searchText, + searchPhrase: debouncedSearchText, }); const loadNextPageRef = useRef(null); @@ -57,25 +57,12 @@ export const DashboardGovernanceActionsCategory = () => { isProposalsFetching, ); - const mappedData = useMemo(() => { - const uniqueProposals = removeDuplicatedProposals(proposals); - - return uniqueProposals?.filter((i) => - getFullGovActionId(i.txHash, i.index) - .toLowerCase() - .includes(searchText.toLowerCase()), - ); - }, [ + const mappedData = useMemo(() => removeDuplicatedProposals(proposals), [ proposals, voter?.isRegisteredAsDRep, - searchText, isProposalsFetchingNextPage, ]); - const closeSorts = useCallback(() => { - setSortOpen(false); - }, [setSortOpen]); - return ( { { - const [searchText, setSearchText] = useState(""); - const [filtersOpen, setFiltersOpen] = useState(false); - const [chosenFilters, setChosenFilters] = useState([]); - const [sortOpen, setSortOpen] = useState(false); - const [chosenSorting, setChosenSorting] = useState(""); + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); + const { chosenFilters, chosenSorting } = dataActionsBarProps; const { isMobile, pagePadding } = useScreenDimension(); const { isEnabled } = useCardano(); const navigate = useNavigate(); @@ -35,7 +33,7 @@ export const GovernanceActions = () => { const { proposals, isProposalsLoading } = useGetProposalsQuery({ filters: queryFilters, sorting: chosenSorting, - searchPhrase: searchText, + searchPhrase: debouncedSearchText, }); useEffect(() => { @@ -44,14 +42,6 @@ export const GovernanceActions = () => { } }, [isEnabled]); - const closeFilters = useCallback(() => { - setFiltersOpen(false); - }, [setFiltersOpen]); - - const closeSorts = useCallback(() => { - setSortOpen(false); - }, [setSortOpen]); - return ( @@ -79,22 +69,7 @@ export const GovernanceActions = () => { /> )} - + {!proposals || isProposalsLoading ? ( { diff --git a/govtool/frontend/src/pages/GovernanceActionsCategory.tsx b/govtool/frontend/src/pages/GovernanceActionsCategory.tsx index 82b67ce46..bafaa8ea5 100644 --- a/govtool/frontend/src/pages/GovernanceActionsCategory.tsx +++ b/govtool/frontend/src/pages/GovernanceActionsCategory.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Box, CircularProgress, Link } from "@mui/material"; @@ -14,6 +14,7 @@ import { useScreenDimension, useTranslation, useGetVoterInfo, + useDataActionsBar, } from "@hooks"; import { WALLET_LS_KEY, @@ -25,9 +26,8 @@ import { export const GovernanceActionsCategory = () => { const { category } = useParams(); - const [searchText, setSearchText] = useState(""); - const [sortOpen, setSortOpen] = useState(false); - const [chosenSorting, setChosenSorting] = useState(""); + const { debouncedSearchText, ...dataActionsBarProps } = useDataActionsBar(); + const { chosenSorting } = dataActionsBarProps; const { isMobile, pagePadding, screenWidth } = useScreenDimension(); const { isEnabled } = useCardano(); const navigate = useNavigate(); @@ -44,7 +44,7 @@ export const GovernanceActionsCategory = () => { } = useGetProposalsInfiniteQuery({ filters: [category?.replace(/ /g, "") ?? ""], sorting: chosenSorting, - searchPhrase: searchText, + searchPhrase: debouncedSearchText, }); const loadNextPageRef = useRef(null); @@ -59,19 +59,10 @@ export const GovernanceActionsCategory = () => { isProposalsFetching, ); - const mappedData = useMemo(() => { - const uniqueProposals = removeDuplicatedProposals(proposals); - - return uniqueProposals?.filter((i) => - getFullGovActionId(i.txHash, i.index) - .toLowerCase() - .includes(searchText.toLowerCase()), - ); - }, [ + const mappedData = useMemo(() => removeDuplicatedProposals(proposals), [ voter?.isRegisteredAsDRep, isProposalsFetchingNextPage, proposals, - searchText, ]); useEffect(() => { @@ -81,10 +72,6 @@ export const GovernanceActionsCategory = () => { } }, [isEnabled]); - const closeSorts = useCallback(() => { - setSortOpen(false); - }, [setSortOpen]); - return ( { { {category}   - {searchText && ( + {debouncedSearchText && ( <> {t("govActions.withCategoryNotExist.optional")}   - {searchText} + {debouncedSearchText} )} diff --git a/govtool/frontend/src/services/requests/getProposals.ts b/govtool/frontend/src/services/requests/getProposals.ts index e4d1c4bf6..96c52a99c 100644 --- a/govtool/frontend/src/services/requests/getProposals.ts +++ b/govtool/frontend/src/services/requests/getProposals.ts @@ -1,12 +1,12 @@ import { API } from "../API"; -export type getProposalsArguments = { +export type GetProposalsArguments = { dRepID?: string; filters?: string[]; page?: number; pageSize?: number; sorting?: string; - searchPhrase: string; + searchPhrase?: string; }; export const getProposals = async ({ @@ -15,23 +15,18 @@ export const getProposals = async ({ page = 0, // It allows fetch proposals and if we have 7 items, display 6 cards and "view all" button pageSize = 7, + searchPhrase = "", sorting = "", -}: Omit) => { - const urlBase = "/proposal/list"; - let urlParameters = `?page=${page}&pageSize=${pageSize}`; - - if (filters.length > 0) { - filters.forEach((item) => { - urlParameters += `&type=${item}`; - }); - } - if (sorting.length) { - urlParameters += `&sort=${sorting}`; - } - if (dRepID) { - urlParameters += `&drepId=${dRepID}`; - } - - const response = await API.get(`${urlBase}${urlParameters}`); +}: GetProposalsArguments) => { + const response = await API.get("/proposal/list", { + params: { + page, + pageSize, + ...(searchPhrase && { search: searchPhrase }), + ...(filters.length && { type: filters }), + ...(sorting && { sort: sorting }), + ...(dRepID && { drepId: dRepID }), + }, + }); return response.data; }; diff --git a/govtool/frontend/src/types/global.d.ts b/govtool/frontend/src/types/global.d.ts index 49890ef5b..816cb26ec 100644 --- a/govtool/frontend/src/types/global.d.ts +++ b/govtool/frontend/src/types/global.d.ts @@ -56,4 +56,7 @@ declare global { | null | { [property: string]: JSONValue } | JSONValue[]; + + type ArrayElement = + ArrayType extends readonly (infer ElementType)[] ? ElementType : never; } From 8380cb6af82ea955f9f8472a05bf81b5a3d14010 Mon Sep 17 00:00:00 2001 From: Joanna Dyczka Date: Tue, 26 Mar 2024 16:07:35 +0100 Subject: [PATCH 02/26] [#536] make filtering and sorting actions reusable --- .../components/molecules/DataActionsBar.tsx | 21 +++++++++-- ...ionsFilters.tsx => DataActionsFilters.tsx} | 37 +++++++++++-------- ...ionsSorting.tsx => DataActionsSorting.tsx} | 10 +++-- .../src/components/molecules/index.ts | 4 +- .../organisms/DashboardGovernanceActions.tsx | 9 ++++- .../DashboardGovernanceActionsCategory.tsx | 3 +- .../frontend/src/pages/GovernanceActions.tsx | 9 ++++- .../src/pages/GovernanceActionsCategory.tsx | 3 +- 8 files changed, 67 insertions(+), 29 deletions(-) rename govtool/frontend/src/components/molecules/{GovernanceActionsFilters.tsx => DataActionsFilters.tsx} (83%) rename govtool/frontend/src/components/molecules/{GovernanceActionsSorting.tsx => DataActionsSorting.tsx} (94%) diff --git a/govtool/frontend/src/components/molecules/DataActionsBar.tsx b/govtool/frontend/src/components/molecules/DataActionsBar.tsx index 08e27a1db..f8f1587c5 100644 --- a/govtool/frontend/src/components/molecules/DataActionsBar.tsx +++ b/govtool/frontend/src/components/molecules/DataActionsBar.tsx @@ -2,7 +2,7 @@ import { Dispatch, FC, SetStateAction } from "react"; import { Box, InputBase } from "@mui/material"; import Search from "@mui/icons-material/Search"; -import { GovernanceActionsFilters, GovernanceActionsSorting } from "@molecules"; +import { DataActionsFilters, DataActionsSorting } from "@molecules"; import { OrderActionsChip } from "./OrderActionsChip"; import { theme } from "@/theme"; @@ -12,7 +12,12 @@ type DataActionsBarProps = { chosenSorting: string; closeFilters?: () => void; closeSorts: () => void; + filterOptions?: { + key: string; + label: string; + }[]; filtersOpen?: boolean; + filtersTitle?: string; isFiltering?: boolean; searchText: string; setChosenFilters?: Dispatch>; @@ -22,6 +27,10 @@ type DataActionsBarProps = { setSortOpen: Dispatch>; sortingActive: boolean; sortOpen: boolean; + sortOptions?: { + key: string; + label: string; + }[]; }; export const DataActionsBar: FC = ({ ...props }) => { @@ -31,7 +40,9 @@ export const DataActionsBar: FC = ({ ...props }) => { chosenSorting, closeFilters = () => {}, closeSorts, + filterOptions = [], filtersOpen, + filtersTitle, isFiltering = true, searchText, setChosenFilters = () => {}, @@ -41,6 +52,7 @@ export const DataActionsBar: FC = ({ ...props }) => { setSortOpen, sortingActive, sortOpen, + sortOptions = [], } = props; const { palette: { boxShadow2 }, @@ -87,17 +99,20 @@ export const DataActionsBar: FC = ({ ...props }) => { sortOpen={sortOpen} > {filtersOpen && ( - )} {sortOpen && ( - )} diff --git a/govtool/frontend/src/components/molecules/GovernanceActionsFilters.tsx b/govtool/frontend/src/components/molecules/DataActionsFilters.tsx similarity index 83% rename from govtool/frontend/src/components/molecules/GovernanceActionsFilters.tsx rename to govtool/frontend/src/components/molecules/DataActionsFilters.tsx index 29bf5cc99..98ec187e3 100644 --- a/govtool/frontend/src/components/molecules/GovernanceActionsFilters.tsx +++ b/govtool/frontend/src/components/molecules/DataActionsFilters.tsx @@ -7,19 +7,25 @@ import { Typography, } from "@mui/material"; -import { GOVERNANCE_ACTIONS_FILTERS } from "@consts"; -import { useOnClickOutside, useScreenDimension, useTranslation } from "@hooks"; +import { useOnClickOutside, useScreenDimension } from "@hooks"; interface Props { chosenFilters: string[]; setChosenFilters: Dispatch>; closeFilters: () => void; + options: { + key: string; + label: string; + }[]; + title?: string; } -export const GovernanceActionsFilters = ({ +export const DataActionsFilters = ({ chosenFilters, setChosenFilters, closeFilters, + options, + title, }: Props) => { const handleFilterChange = useCallback( (e: React.ChangeEvent) => { @@ -37,7 +43,6 @@ export const GovernanceActionsFilters = ({ [chosenFilters, setChosenFilters], ); - const { t } = useTranslation(); const { isMobile, screenWidth } = useScreenDimension(); const wrapperRef = useRef(null); @@ -60,17 +65,19 @@ export const GovernanceActionsFilters = ({ }} ref={wrapperRef} > - - {t("govActions.filterTitle")} - - {GOVERNANCE_ACTIONS_FILTERS.map((item) => ( + {title && ( + + {title} + + )} + {options.map((item) => ( >; closeSorts: () => void; + options: { + key: string; + label: string; + }[]; } -export const GovernanceActionsSorting = ({ +export const DataActionsSorting = ({ chosenSorting, setChosenSorting, closeSorts, + options, }: Props) => { const { t } = useTranslation(); @@ -63,7 +67,7 @@ export const GovernanceActionsSorting = ({ setChosenSorting(e.target.value); }} > - {GOVERNANCE_ACTIONS_SORTING.map((item) => ( + {options.map((item) => ( { flexDirection="column" > <> - + {!proposals || !voter || isEnableLoading || isProposalsLoading ? ( { { /> )} - + {!proposals || isProposalsLoading ? ( { Date: Tue, 26 Mar 2024 17:04:37 +0100 Subject: [PATCH 03/26] Vote Context --- .../src/components/atoms/TextArea.tsx | 29 ++- .../frontend/src/components/atoms/types.ts | 1 + .../components/molecules/Field/TextArea.tsx | 10 +- .../components/molecules/VoteActionForm.tsx | 205 +++++++++--------- .../VoteContext/VoteContextCheckResult.tsx | 106 +++++++++ .../VoteContext/VoteContextModal.tsx | 75 +++++++ .../VoteContextStoringInformation.tsx | 154 +++++++++++++ .../VoteContext/VoteContextTerms.tsx | 60 +++++ .../organisms/VoteContext/VoteContextText.tsx | 77 +++++++ .../VoteContext/VoteContextWrapper.tsx | 64 ++++++ .../components/organisms/VoteContext/index.ts | 6 + .../src/components/organisms/index.ts | 1 + .../src/consts/governanceAction/fields.ts | 30 +++ govtool/frontend/src/context/modal.tsx | 7 +- govtool/frontend/src/hooks/forms/index.ts | 1 + .../forms/useCreateGovernanceActionForm.ts | 2 +- .../src/hooks/forms/useVoteActionForm.tsx | 47 +--- .../src/hooks/forms/useVoteContextForm.tsx | 123 +++++++++++ govtool/frontend/src/i18n/locales/en.ts | 11 +- 19 files changed, 863 insertions(+), 146 deletions(-) create mode 100644 govtool/frontend/src/components/organisms/VoteContext/VoteContextCheckResult.tsx create mode 100644 govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx create mode 100644 govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx create mode 100644 govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx create mode 100644 govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx create mode 100644 govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx create mode 100644 govtool/frontend/src/components/organisms/VoteContext/index.ts create mode 100644 govtool/frontend/src/hooks/forms/useVoteContextForm.tsx diff --git a/govtool/frontend/src/components/atoms/TextArea.tsx b/govtool/frontend/src/components/atoms/TextArea.tsx index 59ee06bfa..823a8acdb 100644 --- a/govtool/frontend/src/components/atoms/TextArea.tsx +++ b/govtool/frontend/src/components/atoms/TextArea.tsx @@ -8,11 +8,9 @@ import { TextAreaProps } from "./types"; const TextAreaBase = styled(TextareaAutosize)( () => ` font-family: "Poppins"; - font-size: 16px; font-weight: 400; ::placeholder { font-family: "Poppins"; - font-size: 16px; font-weight: 400; color: #a6a6a6; } @@ -20,7 +18,17 @@ const TextAreaBase = styled(TextareaAutosize)( ); export const TextArea = forwardRef( - ({ errorMessage, maxLength = 500, onBlur, onFocus, ...props }, ref) => { + ( + { + errorMessage, + maxLength = 500, + onBlur, + onFocus, + isModifiedLayout, + ...props + }, + ref, + ) => { const { isMobile } = useScreenDimension(); const textAraeRef = useRef(null); @@ -51,19 +59,32 @@ export const TextArea = forwardRef( [handleBlur, handleFocus], ); + const getTexAreaHeight = () => { + if (isModifiedLayout && isMobile) return "312px"; + if (isModifiedLayout) return "208px"; + if (isMobile) return "104px"; + return "128px"; + }; + return ( ); diff --git a/govtool/frontend/src/components/atoms/types.ts b/govtool/frontend/src/components/atoms/types.ts index ff2e603b8..ec5b668c4 100644 --- a/govtool/frontend/src/components/atoms/types.ts +++ b/govtool/frontend/src/components/atoms/types.ts @@ -66,6 +66,7 @@ export type FormHelpfulTextProps = { export type TextAreaProps = TextareaAutosizeProps & { errorMessage?: string; + isModifiedLayout?: boolean; }; export type InfoTextProps = { diff --git a/govtool/frontend/src/components/molecules/Field/TextArea.tsx b/govtool/frontend/src/components/molecules/Field/TextArea.tsx index 20688cbe8..9f544aba5 100644 --- a/govtool/frontend/src/components/molecules/Field/TextArea.tsx +++ b/govtool/frontend/src/components/molecules/Field/TextArea.tsx @@ -55,6 +55,14 @@ export const TextArea = forwardRef( } as unknown as HTMLTextAreaElement), [handleBlur, handleFocus], ); + + const getCounterBottomSxValue = () => { + if (props.isModifiedLayout && errorMessage) return 30; + if (props.isModifiedLayout) return 10; + if (errorMessage) return 52.5; + return 35; + }; + return ( ( { - const [isContext, setIsContext] = useState(false); + const [voteContextText, setVoteContextText] = useState(); + const [voteContextHash, setVoteContextHash] = useState(); + const [voteContextUrl, setVoteContextUrl] = useState(); + const [showWholeVoteContext, setShowWholeVoteContext] = + useState(false); + const { state } = useLocation(); const { isMobile } = useScreenDimension(); const { openModal } = useModal(); @@ -54,16 +49,19 @@ export const VoteActionForm = ({ const { areFormErrors, - clearErrors, confirmVote, - control, - errors, isDirty, isVoteLoading, registerInput, setValue, vote, - } = useVoteActionForm(); + } = useVoteActionForm(voteContextHash, voteContextUrl); + + const setVoteContextData = (url: string, hash: string, text: string) => { + setVoteContextUrl(url); + setVoteContextHash(hash); + setVoteContextText(text); + }; useEffect(() => { if (state && state.vote) { @@ -75,14 +73,6 @@ export const VoteActionForm = ({ } }, [state, voteFromEP, setValue]); - useEffect(() => { - clearErrors(); - }, [isContext]); - - const handleContext = useCallback(() => { - setIsContext((prev) => !prev); - }, []); - const renderCancelButton = useMemo( () => ( + ) : ( + )} void; + closeModal: () => void; + setStep: Dispatch>; + errorMessage?: string; +}; + +export const VoteContextCheckResult = ({ + submitVoteContext, + closeModal, + setStep, + errorMessage, +}: VoteContextCheckResultProps) => { + const { t } = useTranslation(); + const { isMobile } = useScreenDimension(); + + const { watch } = useVoteContextForm(); + const isContinueDisabled = !watch("voteContextText"); + + return ( + + Status icon + + {errorMessage ? "Data validation failed" : "Success"} + + + {errorMessage ?? "Data check has been successful"} + + {!errorMessage ? ( + + ) : ( + + + + + )} + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx new file mode 100644 index 000000000..9694d7d3d --- /dev/null +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextModal.tsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import { useForm, FormProvider } from "react-hook-form"; + +import { ModalWrapper } from "@atoms"; +import { useModal } from "@context"; +import { + VoteContextStoringInformation, + VoteContextCheckResult, + VoteContextTerms, + VoteContextText, +} from "@organisms"; +import { VoteContextFormValues } from "@hooks"; + +type VoteContextModalState = { + onSubmit: (url: string, hash: string | null, voteContextText: string) => void; +}; + +export const VoteContextModal = () => { + const [step, setStep] = useState(1); + const [savedHash, setSavedHash] = useState(""); + const [errorMessage, setErrorMessage] = useState( + undefined, + ); + + const { state, closeModal } = useModal(); + + const methods = useForm({ mode: "onChange" }); + const { getValues } = methods; + + const submitVoteContext = () => { + if (state && savedHash) { + state.onSubmit( + getValues("storingURL"), + savedHash, + getValues("voteContextText"), + ); + } + closeModal(); + }; + + return ( + + + {step === 1 && ( + + )} + {step === 2 && ( + + )} + {step === 3 && ( + + )} + {step === 4 && ( + + )} + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx new file mode 100644 index 000000000..96d91b3a8 --- /dev/null +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextStoringInformation.tsx @@ -0,0 +1,154 @@ +import { Dispatch, SetStateAction, useEffect } from "react"; +import { Box } from "@mui/material"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; + +import { Button, Spacer, Typography } from "@atoms"; +import { ICONS } from "@consts"; +import { useTranslation, useScreenDimension, useVoteContextForm } from "@hooks"; +import { Step } from "@molecules"; +import { ControlledField, VoteContextWrapper } from "@organisms"; +import { URL_REGEX, openInNewTab } from "@utils"; + +type VoteContextStoringInformationProps = { + setStep: Dispatch>; + setSavedHash: Dispatch>; + setErrorMessage: Dispatch>; + onCancel: () => void; +}; + +export const VoteContextStoringInformation = ({ + setStep, + setSavedHash, + setErrorMessage, + onCancel, +}: VoteContextStoringInformationProps) => { + const { t } = useTranslation(); + const { screenWidth } = useScreenDimension(); + + const { + control, + errors, + validateURL, + watch, + generateMetadata, + onClickDownloadJson, + } = useVoteContextForm(setSavedHash, setStep, setErrorMessage); + + // TODO: Change link to correct + const openGuideAboutStoringInformation = () => + openInNewTab("https://sancho.network/"); + + const isContinueDisabled = !watch("storingURL"); + + useEffect(() => { + generateMetadata(); + }, []); + + return ( + + + {t("createGovernanceAction.storingInformationTitle")} + + + + {t("createGovernanceAction.storingInformationDescription")} + + + } + sx={{ + width: "fit-content", + ml: screenWidth < 1024 ? 0 : 1.75, + mt: screenWidth < 1024 ? 1.5 : 0, + }} + variant="outlined" + > + {t("govActions.voteContextJsonldFileName")} + + } + componentsLayoutStyles={{ + alignItems: screenWidth < 1024 ? undefined : "center", + flexDirection: screenWidth < 1024 ? "column" : "row", + }} + label={t("createGovernanceAction.storingInformationStep1Label")} + stepNumber={1} + /> + + + } + onClick={openGuideAboutStoringInformation} + size="extraLarge" + sx={{ width: "fit-content" }} + variant="text" + > + {t("createGovernanceAction.storingInformationStep2Link")} + + } + label={t("createGovernanceAction.storingInformationStep2Label")} + stepNumber={2} + /> + + + } + label={t("createGovernanceAction.storingInformationStep3Label")} + stepNumber={3} + /> + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx new file mode 100644 index 000000000..22ac471c5 --- /dev/null +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextTerms.tsx @@ -0,0 +1,60 @@ +import { Dispatch, SetStateAction } from "react"; +import { Box, Link } from "@mui/material"; + +import { Spacer, Typography } from "@atoms"; +import { useScreenDimension, useTranslation, useVoteContextForm } from "@hooks"; +import { ControlledField, VoteContextWrapper } from "@organisms"; +import { openInNewTab } from "@utils"; + +type StoreDataInfoProps = { + setStep: Dispatch>; + onCancel: () => void; +}; + +export const VoteContextTerms = ({ setStep, onCancel }: StoreDataInfoProps) => { + const { t } = useTranslation(); + const { control, errors, watch } = useVoteContextForm(); + const { isMobile } = useScreenDimension(); + + // TODO: change link when available + const openLink = () => openInNewTab("https://docs.sanchogov.tools"); + + const isContinueDisabled = !watch("terms"); + + return ( + setStep(3)} + isContinueDisabled={isContinueDisabled} + onCancel={onCancel} + > + + {t("createGovernanceAction.storeDataTitle")} + + + {t("createGovernanceAction.storeDataLink")} + + + + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx new file mode 100644 index 000000000..f241de814 --- /dev/null +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextText.tsx @@ -0,0 +1,77 @@ +import { Dispatch, SetStateAction } from "react"; + +import { orange } from "@consts"; +import { Typography } from "@atoms"; +import { VoteContextWrapper } from "@organisms"; +import { useTranslation, useVoteContextForm } from "@/hooks"; +import { ControlledField } from ".."; + +type VoteContextTextProps = { + setStep: Dispatch>; + onCancel: () => void; +}; + +export const VoteContextText = ({ + setStep, + onCancel, +}: VoteContextTextProps) => { + const { t } = useTranslation(); + + const { control, errors, watch } = useVoteContextForm(); + const isContinueDisabled = !watch("voteContextText"); + + const fieldProps = { + key: "voteContextText", + layoutStyles: { mb: 3 }, + name: "voteContextText", + placeholder: t("govActions.provideContext"), + rules: { + required: { + value: true, + message: t("createGovernanceAction.fields.validations.required"), + }, + maxLength: { + value: 500, + message: t("createGovernanceAction.fields.validations.maxLength", { + maxLength: 500, + }), + }, + }, + }; + + return ( + setStep(2)} + isContinueDisabled={isContinueDisabled} + onCancel={onCancel} + > + + {t("optional")} + + + {t("govActions.provideContextAboutYourVote")} + + + {/* TODO: Update text when design is finalised */} + Additional information about your vote + + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx new file mode 100644 index 000000000..91ceb8ed5 --- /dev/null +++ b/govtool/frontend/src/components/organisms/VoteContext/VoteContextWrapper.tsx @@ -0,0 +1,64 @@ +import { FC, PropsWithChildren } from "react"; +import { Box } from "@mui/material"; + +import { useScreenDimension, useTranslation } from "@hooks"; +import { Button } from "@atoms"; + +type VoteContextWrapperProps = { + onContinue: () => void; + isContinueDisabled?: boolean; + onCancel: () => void; +}; + +export const VoteContextWrapper: FC< + PropsWithChildren +> = ({ onContinue, isContinueDisabled, onCancel, children }) => { + const { isMobile } = useScreenDimension(); + const { t } = useTranslation(); + + return ( + <> + + {children} + + + + + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/VoteContext/index.ts b/govtool/frontend/src/components/organisms/VoteContext/index.ts new file mode 100644 index 000000000..b288c8de3 --- /dev/null +++ b/govtool/frontend/src/components/organisms/VoteContext/index.ts @@ -0,0 +1,6 @@ +export * from "./VoteContextModal"; +export * from "./VoteContextStoringInformation"; +export * from "./VoteContextCheckResult"; +export * from "./VoteContextTerms"; +export * from "./VoteContextText"; +export * from "./VoteContextWrapper"; diff --git a/govtool/frontend/src/components/organisms/index.ts b/govtool/frontend/src/components/organisms/index.ts index cccbc8400..f8da1ecb0 100644 --- a/govtool/frontend/src/components/organisms/index.ts +++ b/govtool/frontend/src/components/organisms/index.ts @@ -29,4 +29,5 @@ export * from "./RetireAsSoleVoterBoxContent"; export * from "./Slider"; export * from "./StatusModal"; export * from "./TopNav"; +export * from "./VoteContext"; export * from "./VotingPowerModal"; diff --git a/govtool/frontend/src/consts/governanceAction/fields.ts b/govtool/frontend/src/consts/governanceAction/fields.ts index b0395cab4..b172bf784 100644 --- a/govtool/frontend/src/consts/governanceAction/fields.ts +++ b/govtool/frontend/src/consts/governanceAction/fields.ts @@ -163,3 +163,33 @@ export const GOVERNANCE_ACTION_CONTEXT = { }, }, }; + +export const VOTE_TEST_CONTEXT = { + "@language": "en-us", + CIP100: + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#", + CIP108: + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#", + hashAlgorithm: "CIP100:hashAlgorithm", + body: { + "@id": "CIP108:body", + "@context": { + text: "CIP108:text", + }, + }, + authors: { + "@id": "CIP100:authors", + "@container": "@set" as const, + "@context": { + name: "http://xmlns.com/foaf/0.1/name", + witness: { + "@id": "CIP100:witness", + "@context": { + witnessAlgorithm: "CIP100:witnessAlgorithm", + publicKey: "CIP100:publicKey", + signature: "CIP100:signature", + }, + }, + }, + }, +}; diff --git a/govtool/frontend/src/context/modal.tsx b/govtool/frontend/src/context/modal.tsx index 7dbe36e15..cebfab96b 100644 --- a/govtool/frontend/src/context/modal.tsx +++ b/govtool/frontend/src/context/modal.tsx @@ -5,6 +5,7 @@ import { ChooseWalletModal, ExternalLinkModal, StatusModal, + VoteContextModal, VotingPowerModal, } from "@organisms"; import { basicReducer, callAll, BasicReducer } from "@utils"; @@ -25,7 +26,8 @@ export type ModalType = | "chooseWallet" | "statusModal" | "externalLink" - | "votingPower"; + | "votingPower" + | "voteContext"; const modals: Record = { none: { @@ -43,6 +45,9 @@ const modals: Record = { votingPower: { component: , }, + voteContext: { + component: , + }, }; type Optional = Pick, K> & Omit; diff --git a/govtool/frontend/src/hooks/forms/index.ts b/govtool/frontend/src/hooks/forms/index.ts index a1b5ca4e4..f86aeba6c 100644 --- a/govtool/frontend/src/hooks/forms/index.ts +++ b/govtool/frontend/src/hooks/forms/index.ts @@ -4,3 +4,4 @@ export * from "./useRegisterAsdRepForm"; export * from "./useUpdatedRepMetadataForm"; export * from "./useUrlAndHashFormController"; export * from "./useVoteActionForm"; +export * from "./useVoteContextForm"; diff --git a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts index ff0d505b6..e8ff4f660 100644 --- a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts +++ b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts @@ -135,7 +135,7 @@ export const useCreateGovernanceActionForm = ( type: "statusModal", state: { ...storageInformationErrorModals[ - error.message as MetadataHashValidationErrors + error.message as MetadataHashValidationErrors ], onSubmit: backToForm, onCancel: backToDashboard, diff --git a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx index 107dc7cfd..50d85c5e6 100644 --- a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx @@ -6,54 +6,31 @@ import { useLocation, useNavigate } from "react-router-dom"; import { PATHS } from "@consts"; import { useCardano, useSnackbar } from "@context"; -import { HASH_REGEX, URL_REGEX } from "@utils"; -import { useTranslation } from "@hooks"; -import { UrlAndHashFormValues } from "./useUrlAndHashFormController"; -export interface VoteActionFormValues extends UrlAndHashFormValues { +export interface VoteActionFormValues { vote: string; } export const useVoteActionFormController = () => { - const { t } = useTranslation(); - const validationSchema = useMemo( () => Yup.object().shape({ vote: Yup.string().oneOf(["yes", "no", "abstain"]).required(), - url: Yup.string() - .trim() - .max(64, t("forms.errors.urlTooLong")) - .test( - "url-validation", - t("forms.errors.urlInvalidFormat"), - (value) => !value || URL_REGEX.test(value), - ), - hash: Yup.string() - .trim() - .test( - "hash-length-validation", - t("forms.errors.hashInvalidLength"), - (value) => !value || value.length === 64, - ) - .test( - "hash-format-validation", - t("forms.errors.hashInvalidFormat"), - (value) => !value || HASH_REGEX.test(value), - ), - storeData: Yup.boolean(), }), [], ); return useForm({ - defaultValues: { url: "", hash: "", vote: "" }, + defaultValues: { vote: "" }, mode: "onChange", resolver: yupResolver(validationSchema), }); }; -export const useVoteActionForm = () => { +export const useVoteActionForm = ( + voteContextHash?: string, + voteContextUrl?: string, +) => { const [isLoading, setIsLoading] = useState(false); const { buildSignSubmitConwayCertTx, buildVote, isPendingTransaction } = useCardano(); @@ -67,23 +44,22 @@ export const useVoteActionForm = () => { formState: { errors, isDirty }, setValue, register: registerInput, - clearErrors, } = useVoteActionFormController(); const watch = useWatch({ control, }); - const areFormErrors = !!errors.vote || !!errors.url || !!errors.hash; + const areFormErrors = !!errors.vote; const confirmVote = useCallback( async (values: VoteActionFormValues) => { setIsLoading(true); - const { url, hash, vote } = values; + const { vote } = values; - const urlSubmitValue = url ?? ""; - const hashSubmitValue = hash ?? ""; + const urlSubmitValue = voteContextUrl ?? ""; + const hashSubmitValue = voteContextHash ?? ""; try { const isPendingTx = isPendingTransaction(); @@ -118,14 +94,11 @@ export const useVoteActionForm = () => { ); return { - control, - errors, confirmVote: handleSubmit(confirmVote), setValue, vote: watch.vote, registerInput, isDirty, - clearErrors, areFormErrors, isVoteLoading: isLoading, }; diff --git a/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx new file mode 100644 index 000000000..27cfccede --- /dev/null +++ b/govtool/frontend/src/hooks/forms/useVoteContextForm.tsx @@ -0,0 +1,123 @@ +import { Dispatch, SetStateAction, useCallback, useState } from "react"; +import { NodeObject } from "jsonld"; +import { useFormContext } from "react-hook-form"; +import { blake2bHex } from "blakejs"; + +import { + CIP_108, + MetadataHashValidationErrors, + VOTE_TEST_CONTEXT, +} from "@consts"; +import { + canonizeJSON, + downloadJson, + generateJsonld, + validateMetadataHash, +} from "@utils"; +import { captureException } from "@sentry/react"; + +export type VoteContextFormValues = { + voteContextText: string; + terms?: boolean; + storingURL: string; +}; + +export const useVoteContextForm = ( + setSavedHash?: Dispatch>, + setStep?: Dispatch>, + setErrorMessage?: Dispatch>, +) => { + const [hash, setHash] = useState(null); + const [json, setJson] = useState(null); + + const { + control, + formState: { errors, isValid }, + getValues, + handleSubmit, + setValue, + watch, + register, + reset, + } = useFormContext(); + + const generateMetadata = useCallback(async () => { + const data = getValues(); + + const acceptedKeys = ["voteContextText"]; + + const filteredData = Object.entries(data) + .filter(([key]) => acceptedKeys.includes(key)) + .map(([key, value]) => [CIP_108 + key, value]); + + const body = { + ...Object.fromEntries(filteredData), + }; + + const jsonld = await generateJsonld(body, VOTE_TEST_CONTEXT); + const canonizedJson = await canonizeJSON(jsonld); + const canonizedJsonHash = blake2bHex(canonizedJson, undefined, 32); + + // That allows to validate metadata hash + setHash(canonizedJsonHash); + setJson(jsonld); + + return jsonld; + }, [getValues]); + + const onClickDownloadJson = useCallback(() => { + if (!json) return; + downloadJson(json, "Vote_Context"); + }, [json]); + + const validateHash = useCallback( + async (storingUrl: string, localHash: string | null) => { + try { + if (!localHash) { + throw new Error(MetadataHashValidationErrors.INVALID_HASH); + } + await validateMetadataHash(storingUrl, localHash); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if ( + Object.values(MetadataHashValidationErrors).includes(error.message) + ) { + if (setErrorMessage) setErrorMessage(error.message); + if (setStep) setStep(4); + } + throw error; + } + }, + [], + ); + + const onSubmit = useCallback( + async (data: VoteContextFormValues) => { + try { + await validateHash(data.storingURL, hash); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + captureException(error); + } finally { + if (setSavedHash) setSavedHash(hash); + if (setStep) setStep(4); + } + }, + [hash], + ); + + return { + control, + validateURL: handleSubmit(onSubmit), + errors, + generateMetadata, + getValues, + isValid, + onClickDownloadJson, + register, + reset, + setValue, + watch, + hash, + }; +}; diff --git a/govtool/frontend/src/i18n/locales/en.ts b/govtool/frontend/src/i18n/locales/en.ts index 19e49e5f0..04cbf8af8 100644 --- a/govtool/frontend/src/i18n/locales/en.ts +++ b/govtool/frontend/src/i18n/locales/en.ts @@ -323,6 +323,7 @@ export const en = { changeVote: "Change vote", changeYourVote: "Change your vote", chooseHowToVote: "Choose how you want to vote:", + contextAboutYourVote: "Context about your vote", dataMissing: "Data Missing", dataMissingTooltipExplanation: "Please click “View Details” for more information.", @@ -333,12 +334,14 @@ export const en = { forGovAction: "for this Governance Action", governanceActionId: "Governance Action ID:", governanceActionType: "Governance Action Type:", + goToVote: "Go to Vote", motivation: "Motivation", myVote: "My Vote:", noResultsForTheSearch: "No results for the search.", onChainTransactionDetails: "On-chain Transaction Details", optional: "(optional)", - provideContext: "Provide context about your vote", + provideContext: "Provide context", + provideContextAboutYourVote: "Provide context about your vote", rationale: "Rationale", seeExternalData: "See external data", selectDifferentOption: "Select a different option to change your vote", @@ -353,6 +356,7 @@ export const en = { viewOtherDetails: "View other details", viewProposalDetails: "View proposal details", vote: "Vote", + voteContextJsonldFileName: "Vote_Context.jsonld", votedOnByMe: "Voted on by me", voteOnGovActions: "Vote on Governance Action", voteSubmitted: "Vote submitted", @@ -361,6 +365,8 @@ export const en = { votesSubmitted: "Votes submitted", votesSubmittedOnChain: "Votes submitted on-chain by DReps, SPOs and Constitutional Committee members.", + youCanProvideContext: + "You can provide context about your vote. This information will be viewable by other users.", youHaventVotedYet: "You haven't voted on any Governance Actions yet. Check the 'To vote on' section to vote on Governance Actions.", withCategoryNotExist: { @@ -605,10 +611,12 @@ export const en = { cancel: "Cancel", clear: "Clear", clickToCopyLink: "Click to copy link", + close: "Close", confirm: "Confirm", continue: "Continue", delegate: "Delegate", filter: "Filter", + goBack: "Go back", here: "here", inProgress: "In progress", learnMore: "Learn more", @@ -623,6 +631,7 @@ export const en = { seeTransaction: "See transaction", select: "Select", share: "Share", + showLess: "Show less", showMore: "Show more", skip: "Skip", sort: "Sort", From b4fe47a0d5cfe64adab34c278b8b54ce2afbd0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Placzy=C5=84ski?= Date: Wed, 27 Mar 2024 08:54:17 +0100 Subject: [PATCH 04/26] [#569] Fix Dockerfile to correctly build backend module The deployment of the project was failing due to a bug in the Dockerfile of the backend module, caused by a change in the GHC version. The issue was identified during the build process, as the executable path had changed, preventing successful building of the backend module. Changes made in this commit: - Updated the Dockerfile for the backend module to reflect the correct GHC version (9.2.7) and executable path. --- govtool/backend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/govtool/backend/Dockerfile b/govtool/backend/Dockerfile index 75600043c..b8882627d 100644 --- a/govtool/backend/Dockerfile +++ b/govtool/backend/Dockerfile @@ -3,4 +3,4 @@ FROM 733019650473.dkr.ecr.eu-west-1.amazonaws.com/backend-base:$BASE_IMAGE_TAG WORKDIR /src COPY . . RUN cabal build -RUN cp dist-newstyle/build/x86_64-linux/ghc-9.2.8/vva-be-0.1.0.0/x/vva-be/build/vva-be/vva-be /usr/local/bin +RUN cp dist-newstyle/build/x86_64-linux/ghc-9.2.7/vva-be-0.1.0.0/x/vva-be/build/vva-be/vva-be /usr/local/bin From 5c820c0ceaa9e915ae3233ed334444a66ab94315 Mon Sep 17 00:00:00 2001 From: Joanna Dyczka Date: Wed, 20 Mar 2024 09:53:43 +0100 Subject: [PATCH 05/26] [#541] move individual dashboard cards into separate components --- .../components/organisms/DashboardCards.tsx | 501 ++---------------- .../DashboardCards/DRepDashboardCard.tsx | 144 +++++ .../DashboardCards/DelegateDashboardCard.tsx | 207 ++++++++ .../ListGovActionsDashboardCard.tsx | 30 ++ .../ProposeGovActionDashboardCard.tsx | 49 ++ .../DashboardCards/SoleVoterDashboardCard.tsx | 117 ++++ 6 files changed, 590 insertions(+), 458 deletions(-) create mode 100644 govtool/frontend/src/components/organisms/DashboardCards/DRepDashboardCard.tsx create mode 100644 govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx create mode 100644 govtool/frontend/src/components/organisms/DashboardCards/ListGovActionsDashboardCard.tsx create mode 100644 govtool/frontend/src/components/organisms/DashboardCards/ProposeGovActionDashboardCard.tsx create mode 100644 govtool/frontend/src/components/organisms/DashboardCards/SoleVoterDashboardCard.tsx diff --git a/govtool/frontend/src/components/organisms/DashboardCards.tsx b/govtool/frontend/src/components/organisms/DashboardCards.tsx index 98a99630d..051c70b91 100644 --- a/govtool/frontend/src/components/organisms/DashboardCards.tsx +++ b/govtool/frontend/src/components/organisms/DashboardCards.tsx @@ -1,19 +1,16 @@ -import { useCallback, useMemo } from "react"; -import { useNavigate } from "react-router-dom"; import { Box, CircularProgress } from "@mui/material"; -import { Trans } from "react-i18next"; - -import { IMAGES, PATHS } from "@consts"; import { useCardano } from "@context"; import { useGetAdaHolderVotingPowerQuery, useScreenDimension, useGetAdaHolderCurrentDelegationQuery, - useTranslation, useGetVoterInfo, } from "@hooks"; -import { DashboardActionCard } from "@molecules"; -import { correctAdaFormat, formHexToBech32, openInNewTab } from "@utils"; +import { DelegateDashboardCard } from "./DashboardCards/DelegateDashboardCard"; +import { DRepDashboardCard } from "./DashboardCards/DRepDashboardCard"; +import { SoleVoterDashboardCard } from "./DashboardCards/SoleVoterDashboardCard"; +import { ListGovActionsDashboardCards } from "./DashboardCards/ListGovActionsDashboardCard"; +import { ProposeGovActionDashboardCard } from "./DashboardCards/ProposeGovActionDashboardCard"; export const DashboardCards = () => { const { @@ -23,246 +20,29 @@ export const DashboardCards = () => { pendingTransaction, stakeKey, } = useCardano(); - const navigate = useNavigate(); - const { currentDelegation } = useGetAdaHolderCurrentDelegationQuery(stakeKey); const { screenWidth } = useScreenDimension(); + + const { currentDelegation } = useGetAdaHolderCurrentDelegationQuery(stakeKey); const { votingPower } = useGetAdaHolderVotingPowerQuery(stakeKey); - const { t } = useTranslation(); const { voter } = useGetVoterInfo(); - const delegationDescription = useMemo(() => { - const correctAdaRepresentation = correctAdaFormat(votingPower); - if (currentDelegation === dRepID) { - return ( - - ); - } - if (currentDelegation === "drep_always_no_confidence") { - return ( - - ); - } - if (currentDelegation === "drep_always_abstain") { - return ( - - ); - } - if (currentDelegation) { - return ( - - ); - } + if (!currentDelegation || !voter || !votingPower) { return ( - + + + ); - }, [currentDelegation, dRepID, votingPower]); - - const delegationStatusTestForId = useMemo(() => { - if (currentDelegation === dRepID) { - return "myself"; - } - if (currentDelegation === "drep_always_no_confidence") { - return "no-confidence"; - } - if (currentDelegation === "drep_always_abstain") { - return "abstain"; - } - if (currentDelegation) { - return "dRep"; - } - return "not_delegated"; - }, [currentDelegation, dRepID, votingPower]); - - const progressDescription = useMemo(() => { - const correctAdaRepresentation = correctAdaFormat(votingPower); - if (!pendingTransaction.delegate) return; - const { resourceId } = pendingTransaction.delegate; - - if (resourceId === dRepID) { - return ( - - ); - } - if (resourceId === "no confidence") { - return ( - - ); - } - if (resourceId === "abstain") { - return ( - - ); - } - if (resourceId) { - return ( - - ); - } - }, [pendingTransaction, dRepID, votingPower]); - - const navigateTo = useCallback( - (path: string) => { - const isPendingTx = isPendingTransaction(); - if (isPendingTx) return; - navigate(path); - }, - [isPendingTransaction, navigate], - ); - - const onClickGovernanceActionCardActionButton = useCallback(() => { - if (pendingTransaction.createGovAction) { - navigate(PATHS.dashboardGovernanceActions); - return; - } - navigate(PATHS.createGovernanceAction); - }, [pendingTransaction.createGovAction, navigate]); - - const displayedDelegationId = useMemo(() => { - const restrictedNames = [ - dRepID, - "drep_always_abstain", - "drep_always_no_confidence", - "abstain", - "no confidence", - ]; - if (pendingTransaction.delegate) { - const delegateTo = pendingTransaction.delegate.resourceId; - if (!restrictedNames.includes(delegateTo)) { - return delegateTo.includes("drep") - ? delegateTo - : formHexToBech32(delegateTo); - } - return undefined; - } - if (!restrictedNames.includes(currentDelegation)) { - return formHexToBech32(currentDelegation); - } - return undefined; - }, [currentDelegation, dRepID, pendingTransaction, formHexToBech32]); - - const registrationCardDescription = useMemo(() => { - if (pendingTransaction.registerAsDrep) - return t("dashboard.registration.registrationInProgress"); - - if (pendingTransaction.retireAsDrep) - return t("dashboard.registration.retirementInProgress"); - - if (pendingTransaction.updateMetaData) - return t("dashboard.registration.metadataUpdateInProgress"); - - if (voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep) - return t("dashboard.registration.holdersCanDelegate"); - - return t("dashboard.registration.ifYouWant"); - }, [ - pendingTransaction, - voter?.isRegisteredAsDRep, - voter?.wasRegisteredAsDRep, - ]); - - const soleVoterCardDescription = useMemo(() => { - if (pendingTransaction.registerAsSoleVoter) - return "dashboard.soleVoter.registrationInProgress"; - - if (pendingTransaction.retireAsSoleVoter) - return "dashboard.soleVoter.retirementInProgress"; - - if (voter?.isRegisteredAsSoleVoter) - return "dashboard.soleVoter.isRegisteredDescription"; - - if (voter?.wasRegisteredAsSoleVoter) - return "dashboard.soleVoter.wasRegisteredDescription"; - - return "dashboard.soleVoter.registerDescription"; - }, [ - pendingTransaction, - voter?.isRegisteredAsSoleVoter, - voter?.wasRegisteredAsSoleVoter, - ]); + } - const registrationCardTitle = useMemo(() => { - if (pendingTransaction.retireAsDrep) - return t("dashboard.registration.dRepRetirement"); - - if (pendingTransaction.registerAsDrep) - return t("dashboard.registration.dRepRegistration"); - - if (pendingTransaction.updateMetaData) - return t("dashboard.registration.dRepUpdate"); - - if (voter?.isRegisteredAsDRep) - return t("dashboard.registration.youAreRegistered"); - - if (voter?.wasRegisteredAsDRep) - return t("dashboard.registration.registerAgain"); - - return t("dashboard.registration.registerAsDRep"); - }, [ - pendingTransaction, - voter?.isRegisteredAsDRep, - voter?.wasRegisteredAsDRep, - ]); - - const soleVoterCardTitle = useMemo(() => { - if (pendingTransaction.retireAsSoleVoter) - return t("dashboard.soleVoter.retirement"); - - if (pendingTransaction.registerAsSoleVoter) - return t("dashboard.soleVoter.registration"); - - if (voter?.isRegisteredAsSoleVoter) - return t("dashboard.soleVoter.youAreSoleVoterTitle"); - - if (voter?.wasRegisteredAsSoleVoter) - return t("dashboard.soleVoter.wasSoleVoterTitle"); - - return t("dashboard.soleVoter.registerTitle"); - }, [ - pendingTransaction, - voter?.isRegisteredAsSoleVoter, - voter?.isRegisteredAsSoleVoter, - ]); - - return !voter || !votingPower ? ( - - - - ) : ( + return ( { rowGap: 3, }} > - {/* DELEGATION CARD */} - navigateTo(PATHS.delegateTodRep)} - firstButtonLabel={ - pendingTransaction.delegate - ? "" - : currentDelegation - ? t("dashboard.delegation.changeDelegation") - : t("delegate") - } - firstButtonVariant={currentDelegation ? "outlined" : "contained"} - imageURL={IMAGES.govActionDelegateImage} - cardId={displayedDelegationId} - inProgress={!!pendingTransaction.delegate} - cardTitle={t("dashboard.delegation.dRepDelegatedTo")} - secondButtonAction={ - pendingTransaction.delegate - ? () => openInNewTab("https://adanordic.com/latest_transactions") - : () => - openInNewTab( - "https://docs.sanchogov.tools/faqs/ways-to-use-your-voting-power", - ) - } - secondButtonLabel={ - pendingTransaction.delegate - ? t("seeTransaction") - : currentDelegation - ? "" - : t("learnMore") - } - title={ - pendingTransaction.delegate ? ( - t("dashboard.delegation.votingPowerDelegation") - ) : currentDelegation ? ( - - ) : ( - t("dashboard.delegation.useYourVotingPower") - ) - } + - {/* DELEGATION CARD END */} - {/* REGISTARTION AS DREP CARD */} - - navigateTo( - voter?.isRegisteredAsDRep - ? PATHS.retireAsDrep - : PATHS.registerAsdRep, - ) - } - firstButtonLabel={ - pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep - ? "" - : t( - `dashboard.registration.${ - voter?.isRegisteredAsDRep ? "retire" : "register" - }`, - ) - } - inProgress={ - !!( - pendingTransaction.registerAsDrep || - pendingTransaction.retireAsDrep || - pendingTransaction.updateMetaData - ) - } - imageURL={IMAGES.govActionRegisterImage} - secondButtonAction={ - pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep - ? () => openInNewTab("https://adanordic.com/latest_transactions") - : voter?.isRegisteredAsDRep - ? () => { - navigateTo(PATHS.updateMetadata); - } - : () => - openInNewTab( - "https://docs.sanchogov.tools/faqs/what-does-it-mean-to-register-as-a-drep", - ) - } - secondButtonLabel={ - pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep - ? t("seeTransaction") - : voter?.isRegisteredAsDRep - ? t("dashboard.registration.changeMetadata") - : t("learnMore") - } - cardId={ - voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep - ? dRepIDBech32 - : "" - } - cardTitle={ - voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep - ? t("myDRepId") - : "" - } - title={registrationCardTitle} - /> - {/* DREP CARD END */} - {/* SOLE VOTER CARD */} - - } - firstButtonLabel={ - pendingTransaction.registerAsSoleVoter - ? "" - : t( - voter?.isRegisteredAsSoleVoter - ? "dashboard.soleVoter.retire" - : voter?.wasRegisteredAsSoleVoter - ? "dashboard.soleVoter.reRegister" - : "dashboard.soleVoter.register", - ) - } - firstButtonAction={() => - navigateTo( - voter?.isRegisteredAsSoleVoter - ? PATHS.retireAsSoleVoter - : PATHS.registerAsSoleVoter, - ) - } - firstButtonVariant={ - voter?.isRegisteredAsSoleVoter ? "outlined" : "contained" - } - secondButtonLabel={t("learnMore")} - secondButtonAction={() => - openInNewTab( - "https://docs.sanchogov.tools/faqs/what-does-it-mean-to-register-as-a-drep", - ) - } - secondButtonVariant="outlined" - imageURL={IMAGES.soleVoterImage} - /> - {/* REGISTARTION AS SOLE VOTER CARD END */} - {/* GOV ACTIONS LIST CARD */} - navigate(PATHS.dashboardGovernanceActions)} - firstButtonLabel={t( - `dashboard.govActions.${ - voter?.isRegisteredAsDRep ? "reviewAndVote" : "view" - }`, - )} - imageURL={IMAGES.govActionListImage} - title={t("dashboard.govActions.title")} + + - {/* GOV ACTIONS LIST CARD END */} - {/* GOV ACTIONS LIST CARD */} - - openInNewTab( - "https://docs.sanchogov.tools/faqs/what-is-a-governance-action", - ) - } - secondButtonVariant="outlined" - imageURL={IMAGES.proposeGovActionImage} - title={t("dashboard.proposeGovernanceAction.title")} + + - {/* GOV ACTIONS LIST CARD END */} + + + + ); }; diff --git a/govtool/frontend/src/components/organisms/DashboardCards/DRepDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/DRepDashboardCard.tsx new file mode 100644 index 000000000..9f3bd4301 --- /dev/null +++ b/govtool/frontend/src/components/organisms/DashboardCards/DRepDashboardCard.tsx @@ -0,0 +1,144 @@ +import { useCallback, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; + +import { IMAGES, PATHS } from "@consts"; +import { useTranslation } from "@hooks"; +import { DashboardActionCard } from "@molecules"; +import { openInNewTab } from "@utils"; +import { PendingTransaction } from "@/context/pendingTransaction"; +import { VoterInfo } from "@/models"; + +type DRepDashboardCardProps = { + dRepIDBech32: string; + isPendingTransaction: () => boolean; + pendingTransaction: PendingTransaction; + voter: VoterInfo; +}; + +export const DRepDashboardCard = ({ + dRepIDBech32, + isPendingTransaction, + pendingTransaction, + voter, +}: DRepDashboardCardProps) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + + const navigateTo = useCallback( + (path: string) => { + const isPendingTx = isPendingTransaction(); + if (isPendingTx) return; + navigate(path); + }, + [isPendingTransaction, navigate] + ); + + const registrationCardDescription = useMemo(() => { + if (pendingTransaction.registerAsDrep) return t("dashboard.registration.registrationInProgress"); + + if (pendingTransaction.retireAsDrep) return t("dashboard.registration.retirementInProgress"); + + if (pendingTransaction.updateMetaData) return t("dashboard.registration.metadataUpdateInProgress"); + + if (voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep) return t("dashboard.registration.holdersCanDelegate"); + + return t("dashboard.registration.ifYouWant"); + }, [ + pendingTransaction, + voter?.isRegisteredAsDRep, + voter?.wasRegisteredAsDRep, + ]); + + const registrationCardTitle = useMemo(() => { + if (pendingTransaction.retireAsDrep) return t("dashboard.registration.dRepRetirement"); + + if (pendingTransaction.registerAsDrep) return t("dashboard.registration.dRepRegistration"); + + if (pendingTransaction.updateMetaData) return t("dashboard.registration.dRepUpdate"); + + if (voter?.isRegisteredAsDRep) return t("dashboard.registration.youAreRegistered"); + + if (voter?.wasRegisteredAsDRep) return t("dashboard.registration.registerAgain"); + + return t("dashboard.registration.registerAsDRep"); + }, [ + pendingTransaction, + voter?.isRegisteredAsDRep, + voter?.wasRegisteredAsDRep, + ]); + + return ( + navigateTo(PATHS.retireAsDrep) + : () => navigateTo(PATHS.registerAsdRep) + } + firstButtonLabel={ + pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep + ? "" + : t( + `dashboard.registration.${ + voter?.isRegisteredAsDRep ? "retire" : "register" + }` + ) + } + inProgress={ + !!( + pendingTransaction.registerAsDrep || + pendingTransaction.retireAsDrep || + pendingTransaction.updateMetaData + ) + } + imageURL={IMAGES.govActionRegisterImage} + secondButtonAction={ + pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep + ? () => openInNewTab("https://adanordic.com/latest_transactions") + : voter?.isRegisteredAsDRep + ? () => { + navigateTo(PATHS.updateMetadata); + } + : () => + openInNewTab( + "https://docs.sanchogov.tools/faqs/what-does-it-mean-to-register-as-a-drep" + ) + } + secondButtonLabel={ + pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep + ? t("seeTransaction") + : voter?.isRegisteredAsDRep + ? t("dashboard.registration.changeMetadata") + : t("learnMore") + } + cardId={ + voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep + ? dRepIDBech32 + : "" + } + cardTitle={ + voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep + ? t("myDRepId") + : "" + } + title={registrationCardTitle} + /> + ); +}; diff --git a/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx new file mode 100644 index 000000000..81ad86d6b --- /dev/null +++ b/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx @@ -0,0 +1,207 @@ +import { useCallback, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import { Trans } from "react-i18next"; + +import { IMAGES, PATHS } from "@consts"; +import { useTranslation } from "@hooks"; +import { DashboardActionCard } from "@molecules"; +import { correctAdaFormat, formHexToBech32, openInNewTab } from "@utils"; +import { PendingTransaction } from "@/context/pendingTransaction"; + +type DelegateDashboardCardProps = { + currentDelegation: string; + dRepID: string; + isPendingTransaction: () => boolean; + pendingTransaction: PendingTransaction; + votingPower: number; +}; + +export const DelegateDashboardCard = ({ + currentDelegation, + dRepID, + isPendingTransaction, + pendingTransaction, + votingPower, +}: DelegateDashboardCardProps) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + + const delegationDescription = useMemo(() => { + const correctAdaRepresentation = correctAdaFormat(votingPower); + if (currentDelegation === dRepID) { + return ( + + ); + } if (currentDelegation === "drep_always_no_confidence") { + return ( + + ); + } if (currentDelegation === "drep_always_abstain") { + return ( + + ); + } if (currentDelegation) { + return ( + + ); + } + return ( + + ); + }, [currentDelegation, dRepID, votingPower]); + + const delegationStatusTestForId = useMemo(() => { + if (currentDelegation === dRepID) { + return "myself"; + } if (currentDelegation === "drep_always_no_confidence") { + return "no-confidence"; + } if (currentDelegation === "drep_always_abstain") { + return "abstain"; + } if (currentDelegation) { + return "dRep"; + } + return "not_delegated"; + }, [currentDelegation, dRepID, votingPower]); + + const progressDescription = useMemo(() => { + const correctAdaRepresentation = correctAdaFormat(votingPower); + if (!pendingTransaction.delegate) return; + const { resourceId } = pendingTransaction.delegate; + if (resourceId === dRepID) { + return ( + + ); + } + if (resourceId === "no confidence") { + return ( + + ); + } + if (resourceId === "abstain") { + return ( + + ); + } + if (resourceId) { + return ( + + ); + } + }, [pendingTransaction, dRepID, votingPower]); + + const navigateTo = useCallback( + (path: string) => { + const isPendingTx = isPendingTransaction(); + if (isPendingTx) return; + navigate(path); + }, + [isPendingTransaction, navigate] + ); + + const displayedDelegationId = useMemo(() => { + const restrictedNames = [ + dRepID, + "drep_always_abstain", + "drep_always_no_confidence", + "abstain", + "no confidence", + ]; + if (pendingTransaction.delegate) { + const delegateTo = pendingTransaction.delegate.resourceId; + if (!restrictedNames.includes(delegateTo)) { + return delegateTo.includes("drep") + ? delegateTo + : formHexToBech32(delegateTo); + } + return undefined; + } + if (!restrictedNames.includes(currentDelegation)) { + return formHexToBech32(currentDelegation); + } + return undefined; + }, [currentDelegation, dRepID, pendingTransaction, formHexToBech32]); + + return ( + navigateTo(PATHS.delegateTodRep)} + firstButtonLabel={ + pendingTransaction.delegate + ? "" + : currentDelegation + ? t("dashboard.delegation.changeDelegation") + : t("delegate") + } + firstButtonVariant={currentDelegation ? "outlined" : "contained"} + imageURL={IMAGES.govActionDelegateImage} + cardId={displayedDelegationId} + inProgress={!!pendingTransaction.delegate} + cardTitle={t("dashboard.delegation.dRepDelegatedTo")} + secondButtonAction={ + pendingTransaction.delegate + ? () => openInNewTab("https://adanordic.com/latest_transactions") + : () => + openInNewTab( + "https://docs.sanchogov.tools/faqs/ways-to-use-your-voting-power" + ) + } + secondButtonLabel={ + pendingTransaction.delegate + ? t("seeTransaction") + : currentDelegation + ? "" + : t("learnMore") + } + title={ + pendingTransaction.delegate ? ( + t("dashboard.delegation.votingPowerDelegation") + ) : currentDelegation ? ( + + ) : ( + t("dashboard.delegation.useYourVotingPower") + ) + } + /> + ); +}; diff --git a/govtool/frontend/src/components/organisms/DashboardCards/ListGovActionsDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/ListGovActionsDashboardCard.tsx new file mode 100644 index 000000000..2d384468d --- /dev/null +++ b/govtool/frontend/src/components/organisms/DashboardCards/ListGovActionsDashboardCard.tsx @@ -0,0 +1,30 @@ +import { useNavigate } from "react-router-dom"; + +import { IMAGES, PATHS } from "@consts"; +import { useTranslation } from "@hooks"; +import { DashboardActionCard } from "@molecules"; +import { VoterInfo } from "@/models"; + +type ListGovActionsDashboardCardsProps = { + voter: VoterInfo; +}; + +export const ListGovActionsDashboardCards = ({ voter }: ListGovActionsDashboardCardsProps) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + + return ( + navigate(PATHS.dashboardGovernanceActions)} + firstButtonLabel={t( + `dashboard.govActions.${ + voter?.isRegisteredAsDRep ? "reviewAndVote" : "view" + }` + )} + imageURL={IMAGES.govActionListImage} + title={t("dashboard.govActions.title")} + /> + ); +}; diff --git a/govtool/frontend/src/components/organisms/DashboardCards/ProposeGovActionDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/ProposeGovActionDashboardCard.tsx new file mode 100644 index 000000000..b61f8869c --- /dev/null +++ b/govtool/frontend/src/components/organisms/DashboardCards/ProposeGovActionDashboardCard.tsx @@ -0,0 +1,49 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router-dom"; + +import { IMAGES, PATHS } from "@consts"; +import { useTranslation } from "@hooks"; +import { DashboardActionCard } from "@molecules"; +import { openInNewTab } from "@utils"; +import { PendingTransaction } from "@/context/pendingTransaction"; + +type ProposeGovActionDashboardCardProps = { + pendingTransaction: PendingTransaction; +}; + +export const ProposeGovActionDashboardCard = ({ + pendingTransaction, +}: ProposeGovActionDashboardCardProps) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + + const onClickGovernanceActionCardActionButton = useCallback(() => { + if (!pendingTransaction.createGovAction) { + navigate(PATHS.dashboardGovernanceActions); + return; + } + navigate(PATHS.createGovernanceAction); + }, [pendingTransaction, navigate]); + + return ( + + openInNewTab( + "https://docs.sanchogov.tools/faqs/what-is-a-governance-action" + )} + secondButtonVariant="outlined" + imageURL={IMAGES.proposeGovActionImage} + title={t("dashboard.proposeGovernanceAction.title")} + /> + ); +}; diff --git a/govtool/frontend/src/components/organisms/DashboardCards/SoleVoterDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/SoleVoterDashboardCard.tsx new file mode 100644 index 000000000..ace447c0c --- /dev/null +++ b/govtool/frontend/src/components/organisms/DashboardCards/SoleVoterDashboardCard.tsx @@ -0,0 +1,117 @@ +import { useCallback, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import { Trans } from "react-i18next"; + +import { IMAGES, PATHS } from "@consts"; +import { useTranslation } from "@hooks"; +import { DashboardActionCard } from "@molecules"; +import { correctAdaFormat, openInNewTab } from "@utils"; +import { PendingTransaction } from "@/context/pendingTransaction"; +import { VoterInfo } from "@/models"; + +type SoleVoterDashboardCardProps = { + isPendingTransaction: () => boolean; + pendingTransaction: PendingTransaction; + voter: VoterInfo; + votingPower: number; +}; + +export const SoleVoterDashboardCard = ({ + isPendingTransaction, + pendingTransaction, + voter, + votingPower, +}: SoleVoterDashboardCardProps) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + + const navigateTo = useCallback( + (path: string) => { + const isPendingTx = isPendingTransaction(); + if (isPendingTx) return; + navigate(path); + }, + [isPendingTransaction, navigate] + ); + + const soleVoterCardDescription = useMemo(() => { + if (pendingTransaction.registerAsSoleVoter) return "dashboard.soleVoter.registrationInProgress"; + + if (pendingTransaction.retireAsSoleVoter) return "dashboard.soleVoter.retirementInProgress"; + + if (voter?.isRegisteredAsSoleVoter) return "dashboard.soleVoter.isRegisteredDescription"; + + if (voter?.wasRegisteredAsSoleVoter) return "dashboard.soleVoter.wasRegisteredDescription"; + + return "dashboard.soleVoter.registerDescription"; + }, [ + pendingTransaction, + voter?.isRegisteredAsSoleVoter, + voter?.wasRegisteredAsSoleVoter, + ]); + + const soleVoterCardTitle = useMemo(() => { + if (pendingTransaction.retireAsSoleVoter) return t("dashboard.soleVoter.retirement"); + + if (pendingTransaction.registerAsSoleVoter) return t("dashboard.soleVoter.registration"); + + if (voter?.isRegisteredAsSoleVoter) return t("dashboard.soleVoter.youAreSoleVoterTitle"); + + if (voter?.wasRegisteredAsSoleVoter) return t("dashboard.soleVoter.wasSoleVoterTitle"); + + return t("dashboard.soleVoter.registerTitle"); + }, [ + pendingTransaction, + voter?.isRegisteredAsSoleVoter, + voter?.isRegisteredAsSoleVoter, + ]); + + return ( + + )} + firstButtonLabel={ + pendingTransaction.registerAsSoleVoter + ? "" + : t( + voter?.isRegisteredAsSoleVoter + ? "dashboard.soleVoter.retire" + : voter?.wasRegisteredAsSoleVoter + ? "dashboard.soleVoter.reRegister" + : "dashboard.soleVoter.register" + ) + } + firstButtonAction={() => + navigateTo( + voter?.isRegisteredAsSoleVoter + ? PATHS.retireAsSoleVoter + : PATHS.registerAsSoleVoter + )} + firstButtonVariant={ + voter?.isRegisteredAsSoleVoter ? "outlined" : "contained" + } + secondButtonLabel={t("learnMore")} + secondButtonAction={() => + openInNewTab( + "https://docs.sanchogov.tools/faqs/what-does-it-mean-to-register-as-a-drep" + )} + secondButtonVariant="outlined" + imageURL={IMAGES.soleVoterImage} + /> + ); +}; From 3ba91d2be0c6426d32c30799012180e99a8db0ad Mon Sep 17 00:00:00 2001 From: Joanna Dyczka Date: Wed, 20 Mar 2024 13:25:04 +0100 Subject: [PATCH 06/26] [#541] refactor dashboard cards --- .../frontend/src/components/atoms/types.ts | 1 + .../src/components/molecules/Card.tsx | 2 +- .../src/components/molecules/CopyableInfo.tsx | 47 +++ .../src/components/molecules/DRepInfoCard.tsx | 8 +- .../molecules/DashboardActionCard.tsx | 222 ++++-------- .../components/molecules/WalletInfoCard.tsx | 18 +- .../src/components/molecules/index.ts | 1 + .../components/organisms/DashboardCards.tsx | 11 +- .../DashboardCards/DRepDashboardCard.tsx | 217 ++++++------ .../DashboardCards/DelegateDashboardCard.tsx | 320 ++++++++---------- .../ListGovActionsDashboardCard.tsx | 18 +- .../ProposeGovActionDashboardCard.tsx | 58 ++-- .../DashboardCards/SoleVoterDashboardCard.tsx | 170 +++++----- .../src/stories/DashboardCard.stories.ts | 27 +- 14 files changed, 525 insertions(+), 595 deletions(-) create mode 100644 govtool/frontend/src/components/molecules/CopyableInfo.tsx diff --git a/govtool/frontend/src/components/atoms/types.ts b/govtool/frontend/src/components/atoms/types.ts index ff2e603b8..9d9f959a0 100644 --- a/govtool/frontend/src/components/atoms/types.ts +++ b/govtool/frontend/src/components/atoms/types.ts @@ -11,6 +11,7 @@ import * as TooltipMUI from "@mui/material/Tooltip"; export type ButtonProps = Omit & { size?: "small" | "medium" | "large" | "extraLarge"; + dataTestId?: string; }; export type LoadingButtonProps = ButtonProps & { diff --git a/govtool/frontend/src/components/molecules/Card.tsx b/govtool/frontend/src/components/molecules/Card.tsx index c1c5dceff..a6ef31177 100644 --- a/govtool/frontend/src/components/molecules/Card.tsx +++ b/govtool/frontend/src/components/molecules/Card.tsx @@ -39,7 +39,7 @@ export const Card = ({ variant = "default", border = variant !== "default", children, - elevation = 4, + elevation = 3, label, sx, }: CardProps) => { diff --git a/govtool/frontend/src/components/molecules/CopyableInfo.tsx b/govtool/frontend/src/components/molecules/CopyableInfo.tsx new file mode 100644 index 000000000..60597f572 --- /dev/null +++ b/govtool/frontend/src/components/molecules/CopyableInfo.tsx @@ -0,0 +1,47 @@ +import { Box, Typography } from "@mui/material"; + +import { CopyButton } from "@atoms"; +import { Card } from "./Card"; +import { gray } from "@/consts"; + +type CopyableInfoProps = { + dataTestId?: string; + label: string; + value: string; +}; + +export const CopyableInfo = ({ + dataTestId, + label, + value, +}: CopyableInfoProps) => ( + theme.palette.neutralWhite, + }} + > + + + {label} + + + + + + {value} + + + +); diff --git a/govtool/frontend/src/components/molecules/DRepInfoCard.tsx b/govtool/frontend/src/components/molecules/DRepInfoCard.tsx index f01d31897..00663ea63 100644 --- a/govtool/frontend/src/components/molecules/DRepInfoCard.tsx +++ b/govtool/frontend/src/components/molecules/DRepInfoCard.tsx @@ -3,15 +3,17 @@ import { Box, Typography } from "@mui/material"; import { useCardano } from "@context"; import { CopyButton } from "@atoms"; import { useTranslation } from "@hooks"; +import { Card } from "./Card"; +import { gray } from "@/consts"; export const DRepInfoCard = () => { const { dRepIDBech32 } = useCardano(); const { t } = useTranslation(); return ( - + - + {t("myDRepId")} @@ -28,6 +30,6 @@ export const DRepInfoCard = () => { {dRepIDBech32} - + ); }; diff --git a/govtool/frontend/src/components/molecules/DashboardActionCard.tsx b/govtool/frontend/src/components/molecules/DashboardActionCard.tsx index 29d9cd940..55dde49ad 100644 --- a/govtool/frontend/src/components/molecules/DashboardActionCard.tsx +++ b/govtool/frontend/src/components/molecules/DashboardActionCard.tsx @@ -1,30 +1,18 @@ -import { Box, ButtonProps, Skeleton } from "@mui/material"; +import { Box, Skeleton } from "@mui/material"; import { FC, ReactNode } from "react"; -import { CopyButton, LoadingButton, Typography } from "@atoms"; +import { LoadingButton, LoadingButtonProps, Typography } from "@atoms"; import { useScreenDimension, useTranslation } from "@hooks"; -import { theme } from "@/theme"; +import { Card } from "./Card"; -type DashboardActionCardProps = { - cardId?: string; - cardTitle?: string; +export type DashboardActionCardProps = { + buttons?: LoadingButtonProps[]; + children?: ReactNode; dataTestidDelegationStatus?: string; - dataTestidDrepIdBox?: string; - dataTestidFirstButton?: string; - dataTestidSecondButton?: string; description?: ReactNode; - firstButtonAction?: () => void; - firstButtonDisabled?: boolean; - firstButtonIsLoading?: boolean; - firstButtonLabel?: string; - firstButtonVariant?: ButtonProps["variant"]; imageURL?: string; - inProgress?: boolean; isLoading?: boolean; - secondButtonAction?: () => void; - secondButtonIsLoading?: boolean; - secondButtonLabel?: string; - secondButtonVariant?: ButtonProps["variant"]; + state?: "active" | "inProgress" | "default"; title?: ReactNode; }; @@ -33,62 +21,32 @@ export const DashboardActionCard: FC = ({ }) => { const { t } = useTranslation(); const { - cardId, - cardTitle, - dataTestidDrepIdBox, - dataTestidFirstButton, - dataTestidSecondButton, + buttons, + children, description, - firstButtonAction, - firstButtonDisabled = false, - firstButtonIsLoading = false, - firstButtonLabel, - firstButtonVariant = "contained", imageURL, - inProgress, isLoading = false, - secondButtonAction, - secondButtonIsLoading = false, - secondButtonLabel, - secondButtonVariant = "outlined", + state = "default", title, } = props; - const { - palette: { boxShadow2 }, - } = theme; - const { isMobile, screenWidth } = useScreenDimension(); + const { screenWidth } = useScreenDimension(); return ( - - {inProgress && !isLoading && ( - - - {t("inProgress")} - - - )} {imageURL ? ( isLoading ? ( @@ -113,7 +71,7 @@ export const DashboardActionCard: FC = ({ {isLoading ? : title} ) : null} - {inProgress && !isLoading ? ( + {state === "inProgress" && !isLoading ? ( {t("inProgress")} @@ -122,7 +80,7 @@ export const DashboardActionCard: FC = ({ @@ -133,102 +91,46 @@ export const DashboardActionCard: FC = ({ )} ) : null} - {cardId && ( - - - - {cardTitle} - - - {cardId} - - - - - )} - {isLoading ? ( - - - - - ) : ( - - {firstButtonLabel ? ( - - {firstButtonLabel} - - ) : null} - {secondButtonLabel ? ( + {children} + + {isLoading ? ( + <> + + + + ) : ( + buttons?.map(({ dataTestId, ...buttonProps }) => ( - {secondButtonLabel} - - ) : null} - - )} - + {...buttonProps} + /> + )) + )} + + ); }; diff --git a/govtool/frontend/src/components/molecules/WalletInfoCard.tsx b/govtool/frontend/src/components/molecules/WalletInfoCard.tsx index d45dd3e31..b9d3a482c 100644 --- a/govtool/frontend/src/components/molecules/WalletInfoCard.tsx +++ b/govtool/frontend/src/components/molecules/WalletInfoCard.tsx @@ -1,9 +1,10 @@ import { useNavigate } from "react-router-dom"; import { Box, Button, Typography } from "@mui/material"; -import { PATHS } from "@consts"; +import { PATHS, gray } from "@consts"; import { useCardano } from "@context"; import { useTranslation } from "@hooks"; +import { Card } from "./Card"; export const WalletInfoCard = () => { const { address, disconnectWallet } = useCardano(); @@ -18,17 +19,8 @@ export const WalletInfoCard = () => { return ( address && ( - - + + {t("wallet.connectedWallet")} @@ -52,7 +44,7 @@ export const WalletInfoCard = () => { {t("wallet.disconnect")} - + ) ); }; diff --git a/govtool/frontend/src/components/molecules/index.ts b/govtool/frontend/src/components/molecules/index.ts index 78d4f0563..49002a007 100644 --- a/govtool/frontend/src/components/molecules/index.ts +++ b/govtool/frontend/src/components/molecules/index.ts @@ -3,6 +3,7 @@ export * from "./Breadcrumbs"; export * from "./Card"; export * from "./CenteredBoxBottomButtons"; export * from "./CenteredBoxPageWrapper"; +export * from "./CopyableInfo"; export * from "./DashboardActionCard"; export * from "./DataActionsBar"; export * from "./DataMissingInfoBox"; diff --git a/govtool/frontend/src/components/organisms/DashboardCards.tsx b/govtool/frontend/src/components/organisms/DashboardCards.tsx index 051c70b91..ec7747cf3 100644 --- a/govtool/frontend/src/components/organisms/DashboardCards.tsx +++ b/govtool/frontend/src/components/organisms/DashboardCards.tsx @@ -1,4 +1,5 @@ import { Box, CircularProgress } from "@mui/material"; + import { useCardano } from "@context"; import { useGetAdaHolderVotingPowerQuery, @@ -16,7 +17,6 @@ export const DashboardCards = () => { const { dRepID, dRepIDBech32, - isPendingTransaction, pendingTransaction, stakeKey, } = useCardano(); @@ -61,21 +61,18 @@ export const DashboardCards = () => { > { - + ); }; diff --git a/govtool/frontend/src/components/organisms/DashboardCards/DRepDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/DRepDashboardCard.tsx index 9f3bd4301..4c9298d3e 100644 --- a/govtool/frontend/src/components/organisms/DashboardCards/DRepDashboardCard.tsx +++ b/govtool/frontend/src/components/organisms/DashboardCards/DRepDashboardCard.tsx @@ -1,144 +1,133 @@ -import { useCallback, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { IMAGES, PATHS } from "@consts"; import { useTranslation } from "@hooks"; -import { DashboardActionCard } from "@molecules"; +import { + CopyableInfo, + DashboardActionCard, + DashboardActionCardProps, +} from "@molecules"; import { openInNewTab } from "@utils"; import { PendingTransaction } from "@/context/pendingTransaction"; import { VoterInfo } from "@/models"; type DRepDashboardCardProps = { dRepIDBech32: string; - isPendingTransaction: () => boolean; pendingTransaction: PendingTransaction; voter: VoterInfo; }; export const DRepDashboardCard = ({ dRepIDBech32, - isPendingTransaction, pendingTransaction, voter, }: DRepDashboardCardProps) => { const navigate = useNavigate(); const { t } = useTranslation(); - const navigateTo = useCallback( - (path: string) => { - const isPendingTx = isPendingTransaction(); - if (isPendingTx) return; - navigate(path); - }, - [isPendingTransaction, navigate] + const inProgress = !!( + pendingTransaction.registerAsDrep || + pendingTransaction.retireAsDrep || + pendingTransaction.updateMetaData ); - const registrationCardDescription = useMemo(() => { - if (pendingTransaction.registerAsDrep) return t("dashboard.registration.registrationInProgress"); - - if (pendingTransaction.retireAsDrep) return t("dashboard.registration.retirementInProgress"); - - if (pendingTransaction.updateMetaData) return t("dashboard.registration.metadataUpdateInProgress"); - - if (voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep) return t("dashboard.registration.holdersCanDelegate"); - - return t("dashboard.registration.ifYouWant"); - }, [ - pendingTransaction, - voter?.isRegisteredAsDRep, - voter?.wasRegisteredAsDRep, - ]); - - const registrationCardTitle = useMemo(() => { - if (pendingTransaction.retireAsDrep) return t("dashboard.registration.dRepRetirement"); - - if (pendingTransaction.registerAsDrep) return t("dashboard.registration.dRepRegistration"); - - if (pendingTransaction.updateMetaData) return t("dashboard.registration.dRepUpdate"); - - if (voter?.isRegisteredAsDRep) return t("dashboard.registration.youAreRegistered"); - - if (voter?.wasRegisteredAsDRep) return t("dashboard.registration.registerAgain"); - - return t("dashboard.registration.registerAsDRep"); - }, [ - pendingTransaction, - voter?.isRegisteredAsDRep, - voter?.wasRegisteredAsDRep, - ]); + const cardProps: Partial = (() => { + // transaction in progress + if (inProgress) { + return { + buttons: [ + { + children: t("seeTransaction"), + onClick: () => + openInNewTab("https://adanordic.com/latest_transactions"), + }, + ], + state: "inProgress", + ...(pendingTransaction.registerAsDrep && { + description: t("dashboard.registration.registrationInProgress"), + title: t("dashboard.registration.dRepRegistration"), + }), + ...(pendingTransaction.retireAsDrep && { + description: t("dashboard.registration.retirementInProgress"), + title: t("dashboard.registration.dRepRetirement"), + }), + ...(pendingTransaction.updateMetaData && { + description: t("dashboard.registration.metadataUpdateInProgress"), + title: t("dashboard.registration.dRepUpdate"), + }), + }; + } + + // currently registered + if (voter?.isRegisteredAsDRep) { + return { + buttons: [ + { + children: t("dashboard.registration.retire"), + dataTestId: "retire-button", + onClick: () => navigate(PATHS.retireAsDrep), + }, + { + children: t("dashboard.registration.changeMetadata"), + dataTestId: "change-metadata-button", + onClick: () => navigate(PATHS.updateMetadata), + variant: "text", + }, + ], + description: t("dashboard.registration.holdersCanDelegate"), + state: "active", + title: t("dashboard.registration.youAreRegistered"), + }; + } + + // common buttons for was registered or not registered + const wasRegisteredOrNotRegisteredButtons: DashboardActionCardProps["buttons"] = + [ + { + children: t("dashboard.registration.register"), + dataTestId: "register-button", + onClick: () => navigate(PATHS.registerAsdRep), + variant: "contained", + }, + { + children: t("learnMore"), + dataTestId: "register-learn-more-button", + onClick: () => + openInNewTab( + "https://docs.sanchogov.tools/faqs/what-does-it-mean-to-register-as-a-drep" + ), + }, + ]; + + // was registered + if (voter?.wasRegisteredAsDRep) { + return { + buttons: wasRegisteredOrNotRegisteredButtons, + description: t("dashboard.registration.holdersCanDelegate"), + title: t("dashboard.registration.registerAgain"), + }; + } + + // not registered + return { + buttons: wasRegisteredOrNotRegisteredButtons, + description: t("dashboard.registration.ifYouWant"), + title: t("dashboard.registration.registerAsDRep"), + }; + })(); return ( navigateTo(PATHS.retireAsDrep) - : () => navigateTo(PATHS.registerAsdRep) - } - firstButtonLabel={ - pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep - ? "" - : t( - `dashboard.registration.${ - voter?.isRegisteredAsDRep ? "retire" : "register" - }` - ) - } - inProgress={ - !!( - pendingTransaction.registerAsDrep || - pendingTransaction.retireAsDrep || - pendingTransaction.updateMetaData - ) - } imageURL={IMAGES.govActionRegisterImage} - secondButtonAction={ - pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep - ? () => openInNewTab("https://adanordic.com/latest_transactions") - : voter?.isRegisteredAsDRep - ? () => { - navigateTo(PATHS.updateMetadata); - } - : () => - openInNewTab( - "https://docs.sanchogov.tools/faqs/what-does-it-mean-to-register-as-a-drep" - ) - } - secondButtonLabel={ - pendingTransaction.registerAsDrep || pendingTransaction.retireAsDrep - ? t("seeTransaction") - : voter?.isRegisteredAsDRep - ? t("dashboard.registration.changeMetadata") - : t("learnMore") - } - cardId={ - voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep - ? dRepIDBech32 - : "" - } - cardTitle={ - voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep - ? t("myDRepId") - : "" - } - title={registrationCardTitle} - /> + {...cardProps} + > + {(voter?.isRegisteredAsDRep || voter?.wasRegisteredAsDRep) && ( + + )} + ); }; diff --git a/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx index 81ad86d6b..6f68f06a9 100644 --- a/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx +++ b/govtool/frontend/src/components/organisms/DashboardCards/DelegateDashboardCard.tsx @@ -1,207 +1,185 @@ -import { useCallback, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { Trans } from "react-i18next"; import { IMAGES, PATHS } from "@consts"; import { useTranslation } from "@hooks"; -import { DashboardActionCard } from "@molecules"; +import { + CopyableInfo, + DashboardActionCard, + DashboardActionCardProps, +} from "@molecules"; import { correctAdaFormat, formHexToBech32, openInNewTab } from "@utils"; import { PendingTransaction } from "@/context/pendingTransaction"; type DelegateDashboardCardProps = { currentDelegation: string; + delegateTx: PendingTransaction["delegate"]; dRepID: string; - isPendingTransaction: () => boolean; - pendingTransaction: PendingTransaction; votingPower: number; }; export const DelegateDashboardCard = ({ currentDelegation, + delegateTx, dRepID, - isPendingTransaction, - pendingTransaction, votingPower, }: DelegateDashboardCardProps) => { const navigate = useNavigate(); const { t } = useTranslation(); - const delegationDescription = useMemo(() => { - const correctAdaRepresentation = correctAdaFormat(votingPower); - if (currentDelegation === dRepID) { - return ( - - ); - } if (currentDelegation === "drep_always_no_confidence") { - return ( - - ); - } if (currentDelegation === "drep_always_abstain") { - return ( - - ); - } if (currentDelegation) { - return ( - - ); - } - return ( - - ); - }, [currentDelegation, dRepID, votingPower]); + const ada = correctAdaFormat(votingPower); - const delegationStatusTestForId = useMemo(() => { - if (currentDelegation === dRepID) { - return "myself"; - } if (currentDelegation === "drep_always_no_confidence") { - return "no-confidence"; - } if (currentDelegation === "drep_always_abstain") { - return "abstain"; - } if (currentDelegation) { - return "dRep"; + const cardProps: Partial = (() => { + // transaction in progress + if (delegateTx) { + return { + buttons: [ + { + children: t("seeTransaction"), + dataTestId: "see-transaction-button", + onClick: () => + openInNewTab("https://adanordic.com/latest_transactions"), + }, + ], + description: getProgressDescription( + delegateTx?.resourceId, + dRepID, + ada + ), + state: "inProgress", + title: t("dashboard.delegation.votingPowerDelegation"), + }; } - return "not_delegated"; - }, [currentDelegation, dRepID, votingPower]); - const progressDescription = useMemo(() => { - const correctAdaRepresentation = correctAdaFormat(votingPower); - if (!pendingTransaction.delegate) return; - const { resourceId } = pendingTransaction.delegate; - if (resourceId === dRepID) { - return ( - - ); - } - if (resourceId === "no confidence") { - return ( - - ); - } - if (resourceId === "abstain") { - return ( - - ); + // current delegation + if (currentDelegation) { + return { + buttons: [ + { + children: t("dashboard.delegation.changeDelegation"), + dataTestId: "change-dRep-button", + onClick: () => navigate(PATHS.delegateTodRep), + }, + ], + description: getDelegationDescription(currentDelegation, dRepID, ada), + state: "active", + title: ( + + ), + }; } - if (resourceId) { - return ( + + // no current delegation + return { + buttons: [ + { + children: t("delegate"), + dataTestId: "delegate-button", + onClick: () => navigate(PATHS.delegateTodRep), + variant: "contained", + }, + { + children: t("learnMore"), + dataTestId: "delegate-learn-more-button", + onClick: () => + openInNewTab( + "https://docs.sanchogov.tools/faqs/ways-to-use-your-voting-power" + ), + }, + ], + description: ( - ); - } - }, [pendingTransaction, dRepID, votingPower]); + ), + title: t("dashboard.delegation.useYourVotingPower"), + }; + })(); - const navigateTo = useCallback( - (path: string) => { - const isPendingTx = isPendingTransaction(); - if (isPendingTx) return; - navigate(path); - }, - [isPendingTransaction, navigate] + const displayedDelegationId = getDisplayedDelegationId( + currentDelegation, + delegateTx?.resourceId, + dRepID ); - const displayedDelegationId = useMemo(() => { - const restrictedNames = [ - dRepID, - "drep_always_abstain", - "drep_always_no_confidence", - "abstain", - "no confidence", - ]; - if (pendingTransaction.delegate) { - const delegateTo = pendingTransaction.delegate.resourceId; - if (!restrictedNames.includes(delegateTo)) { - return delegateTo.includes("drep") - ? delegateTo - : formHexToBech32(delegateTo); - } - return undefined; - } - if (!restrictedNames.includes(currentDelegation)) { - return formHexToBech32(currentDelegation); - } - return undefined; - }, [currentDelegation, dRepID, pendingTransaction, formHexToBech32]); - return ( navigateTo(PATHS.delegateTodRep)} - firstButtonLabel={ - pendingTransaction.delegate - ? "" - : currentDelegation - ? t("dashboard.delegation.changeDelegation") - : t("delegate") - } - firstButtonVariant={currentDelegation ? "outlined" : "contained"} imageURL={IMAGES.govActionDelegateImage} - cardId={displayedDelegationId} - inProgress={!!pendingTransaction.delegate} - cardTitle={t("dashboard.delegation.dRepDelegatedTo")} - secondButtonAction={ - pendingTransaction.delegate - ? () => openInNewTab("https://adanordic.com/latest_transactions") - : () => - openInNewTab( - "https://docs.sanchogov.tools/faqs/ways-to-use-your-voting-power" - ) - } - secondButtonLabel={ - pendingTransaction.delegate - ? t("seeTransaction") - : currentDelegation - ? "" - : t("learnMore") - } - title={ - pendingTransaction.delegate ? ( - t("dashboard.delegation.votingPowerDelegation") - ) : currentDelegation ? ( - - ) : ( - t("dashboard.delegation.useYourVotingPower") - ) - } - /> + {...cardProps} + > + {displayedDelegationId && ( + + )} + ); }; + +const getDelegationDescription = ( + currentDelegation: string, + dRepID: string, + ada: number +) => { + const key = + currentDelegation === dRepID + ? "dashboard.delegation.toYourself" + : currentDelegation === "drep_always_no_confidence" + ? "dashboard.delegation.voteNo" + : currentDelegation === "drep_always_abstain" + ? "dashboard.delegation.voteAbstain" + : currentDelegation + ? "dashboard.delegation.toDRep" + : undefined; + return ; +}; + +const getProgressDescription = ( + delegateTo: string, + dRepID: string, + ada: number +) => { + const key = (() => { + if (!delegateTo) return undefined; + switch (delegateTo) { + case dRepID: + return "dashboard.delegation.inProgress.toYourself"; + case "no confidence": + return "dashboard.delegation.inProgress.voteNo"; + case "abstain": + return "dashboard.delegation.inProgress.voteAbstain"; + default: + return "dashboard.delegation.inProgress.toDRep"; + } + })(); + return ; +}; + +const getDisplayedDelegationId = ( + currentDelegation: string, + delegateTo: string | undefined, + dRepID: string +) => { + const restrictedNames = [ + dRepID, + "drep_always_abstain", + "drep_always_no_confidence", + "abstain", + "no confidence", + ]; + if (delegateTo) { + if (!restrictedNames.includes(delegateTo)) { + return delegateTo.includes("drep") + ? delegateTo + : formHexToBech32(delegateTo); + } + return undefined; + } + if (!restrictedNames.includes(currentDelegation)) { + return formHexToBech32(currentDelegation); + } + return undefined; +}; diff --git a/govtool/frontend/src/components/organisms/DashboardCards/ListGovActionsDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/ListGovActionsDashboardCard.tsx index 2d384468d..dfc42cc36 100644 --- a/govtool/frontend/src/components/organisms/DashboardCards/ListGovActionsDashboardCard.tsx +++ b/govtool/frontend/src/components/organisms/DashboardCards/ListGovActionsDashboardCard.tsx @@ -15,14 +15,18 @@ export const ListGovActionsDashboardCards = ({ voter }: ListGovActionsDashboardC return ( navigate(PATHS.dashboardGovernanceActions), + }, + ]} description={t("dashboard.govActions.description")} - firstButtonAction={() => navigate(PATHS.dashboardGovernanceActions)} - firstButtonLabel={t( - `dashboard.govActions.${ - voter?.isRegisteredAsDRep ? "reviewAndVote" : "view" - }` - )} imageURL={IMAGES.govActionListImage} title={t("dashboard.govActions.title")} /> diff --git a/govtool/frontend/src/components/organisms/DashboardCards/ProposeGovActionDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/ProposeGovActionDashboardCard.tsx index b61f8869c..6bace3a8d 100644 --- a/govtool/frontend/src/components/organisms/DashboardCards/ProposeGovActionDashboardCard.tsx +++ b/govtool/frontend/src/components/organisms/DashboardCards/ProposeGovActionDashboardCard.tsx @@ -1,4 +1,3 @@ -import { useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { IMAGES, PATHS } from "@consts"; @@ -8,41 +7,50 @@ import { openInNewTab } from "@utils"; import { PendingTransaction } from "@/context/pendingTransaction"; type ProposeGovActionDashboardCardProps = { - pendingTransaction: PendingTransaction; + createGovActionTx: PendingTransaction["createGovAction"]; }; export const ProposeGovActionDashboardCard = ({ - pendingTransaction, + createGovActionTx, }: ProposeGovActionDashboardCardProps) => { const navigate = useNavigate(); const { t } = useTranslation(); - const onClickGovernanceActionCardActionButton = useCallback(() => { - if (!pendingTransaction.createGovAction) { - navigate(PATHS.dashboardGovernanceActions); - return; - } - navigate(PATHS.createGovernanceAction); - }, [pendingTransaction, navigate]); - return ( navigate(PATHS.dashboardGovernanceActions), + variant: "contained", + } as const, + ] + // default + : [ + { + children: t("dashboard.proposeGovernanceAction.propose"), + dataTestId: "propose-governance-actions-button", + onClick: () => navigate(PATHS.createGovernanceAction), + variant: "contained", + } as const, + ]), + // common + { + children: t("learnMore"), + dataTestId: "learn-more-button", + onClick: () => + openInNewTab( + "https://docs.sanchogov.tools/faqs/what-is-a-governance-action" + ), + }, + ]} description={t("dashboard.proposeGovernanceAction.description")} - firstButtonAction={onClickGovernanceActionCardActionButton} - firstButtonLabel={t( - `dashboard.proposeGovernanceAction.${ - pendingTransaction.createGovAction ? "view" : "propose" - }` - )} - inProgress={!!pendingTransaction.createGovAction} - secondButtonLabel={t("learnMore")} - secondButtonAction={() => - openInNewTab( - "https://docs.sanchogov.tools/faqs/what-is-a-governance-action" - )} - secondButtonVariant="outlined" imageURL={IMAGES.proposeGovActionImage} + state={createGovActionTx ? "inProgress" : "default"} title={t("dashboard.proposeGovernanceAction.title")} /> ); diff --git a/govtool/frontend/src/components/organisms/DashboardCards/SoleVoterDashboardCard.tsx b/govtool/frontend/src/components/organisms/DashboardCards/SoleVoterDashboardCard.tsx index ace447c0c..95eb1449b 100644 --- a/govtool/frontend/src/components/organisms/DashboardCards/SoleVoterDashboardCard.tsx +++ b/govtool/frontend/src/components/organisms/DashboardCards/SoleVoterDashboardCard.tsx @@ -1,23 +1,21 @@ -import { useCallback, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { Trans } from "react-i18next"; import { IMAGES, PATHS } from "@consts"; import { useTranslation } from "@hooks"; -import { DashboardActionCard } from "@molecules"; +import { DashboardActionCard, DashboardActionCardProps } from "@molecules"; import { correctAdaFormat, openInNewTab } from "@utils"; +import { LoadingButtonProps } from "@atoms"; import { PendingTransaction } from "@/context/pendingTransaction"; import { VoterInfo } from "@/models"; type SoleVoterDashboardCardProps = { - isPendingTransaction: () => boolean; pendingTransaction: PendingTransaction; voter: VoterInfo; votingPower: number; }; export const SoleVoterDashboardCard = ({ - isPendingTransaction, pendingTransaction, voter, votingPower, @@ -25,93 +23,95 @@ export const SoleVoterDashboardCard = ({ const navigate = useNavigate(); const { t } = useTranslation(); - const navigateTo = useCallback( - (path: string) => { - const isPendingTx = isPendingTransaction(); - if (isPendingTx) return; - navigate(path); - }, - [isPendingTransaction, navigate] - ); - - const soleVoterCardDescription = useMemo(() => { - if (pendingTransaction.registerAsSoleVoter) return "dashboard.soleVoter.registrationInProgress"; - - if (pendingTransaction.retireAsSoleVoter) return "dashboard.soleVoter.retirementInProgress"; - - if (voter?.isRegisteredAsSoleVoter) return "dashboard.soleVoter.isRegisteredDescription"; - - if (voter?.wasRegisteredAsSoleVoter) return "dashboard.soleVoter.wasRegisteredDescription"; + const ada = correctAdaFormat(votingPower); - return "dashboard.soleVoter.registerDescription"; - }, [ - pendingTransaction, - voter?.isRegisteredAsSoleVoter, - voter?.wasRegisteredAsSoleVoter, - ]); + const cardProps: Partial = (() => { + // transaction in progress + if ( + !!pendingTransaction.registerAsSoleVoter || + !!pendingTransaction.retireAsSoleVoter + ) { + return { + buttons: [ + { + children: t("seeTransaction"), + dataTestId: "see-transaction-button", + onClick: () => + openInNewTab("https://adanordic.com/latest_transactions"), + }, + ], + state: "inProgress", + ...(pendingTransaction.registerAsSoleVoter && { + description: t("dashboard.soleVoter.registrationInProgress"), + title: t("dashboard.soleVoter.registration"), + }), + ...(pendingTransaction.retireAsSoleVoter && { + description: t("dashboard.soleVoter.retirementInProgress"), + title: t("dashboard.soleVoter.retirement"), + }), + }; + } - const soleVoterCardTitle = useMemo(() => { - if (pendingTransaction.retireAsSoleVoter) return t("dashboard.soleVoter.retirement"); - - if (pendingTransaction.registerAsSoleVoter) return t("dashboard.soleVoter.registration"); + // learn more button + const learnMoreButton: LoadingButtonProps = { + children: t("learnMore"), + dataTestId: "learn-more-button", + onClick: () => + openInNewTab( + "https://docs.sanchogov.tools/faqs/what-does-it-mean-to-register-as-a-drep" + ), + }; - if (voter?.isRegisteredAsSoleVoter) return t("dashboard.soleVoter.youAreSoleVoterTitle"); + // currently registered + if (voter?.isRegisteredAsSoleVoter) { + return { + buttons: [ + { + children: t("dashboard.soleVoter.retire"), + dataTestId: "retire-as-sole-voter-button", + onClick: () => navigate(PATHS.retireAsSoleVoter), + }, + learnMoreButton, + ], + description: , + state: "active", + title: t("dashboard.soleVoter.youAreSoleVoterTitle"), + }; + } - if (voter?.wasRegisteredAsSoleVoter) return t("dashboard.soleVoter.wasSoleVoterTitle"); + // was registered + if (voter?.wasRegisteredAsSoleVoter) { + return { + buttons: [ + { + children: t("dashboard.soleVoter.reRegister"), + dataTestId: "register-as-sole-voter-button", + onClick: () => navigate(PATHS.registerAsSoleVoter), + }, + learnMoreButton, + ], + description: , + title: t("dashboard.soleVoter.wasSoleVoterTitle"), + }; + } - return t("dashboard.soleVoter.registerTitle"); - }, [ - pendingTransaction, - voter?.isRegisteredAsSoleVoter, - voter?.isRegisteredAsSoleVoter, - ]); + // not registered + return { + buttons: [ + { + children: t("dashboard.soleVoter.register"), + dataTestId: "register-as-sole-voter-button", + onClick: () => navigate(PATHS.registerAsSoleVoter), + variant: "contained", + }, + learnMoreButton, + ], + description: , + title: t("dashboard.soleVoter.registerTitle"), + }; + })(); return ( - - )} - firstButtonLabel={ - pendingTransaction.registerAsSoleVoter - ? "" - : t( - voter?.isRegisteredAsSoleVoter - ? "dashboard.soleVoter.retire" - : voter?.wasRegisteredAsSoleVoter - ? "dashboard.soleVoter.reRegister" - : "dashboard.soleVoter.register" - ) - } - firstButtonAction={() => - navigateTo( - voter?.isRegisteredAsSoleVoter - ? PATHS.retireAsSoleVoter - : PATHS.registerAsSoleVoter - )} - firstButtonVariant={ - voter?.isRegisteredAsSoleVoter ? "outlined" : "contained" - } - secondButtonLabel={t("learnMore")} - secondButtonAction={() => - openInNewTab( - "https://docs.sanchogov.tools/faqs/what-does-it-mean-to-register-as-a-drep" - )} - secondButtonVariant="outlined" - imageURL={IMAGES.soleVoterImage} - /> + ); }; diff --git a/govtool/frontend/src/stories/DashboardCard.stories.ts b/govtool/frontend/src/stories/DashboardCard.stories.ts index 7574a9f5a..ac4fb83a2 100644 --- a/govtool/frontend/src/stories/DashboardCard.stories.ts +++ b/govtool/frontend/src/stories/DashboardCard.stories.ts @@ -19,10 +19,12 @@ type Story = StoryObj; export const DashboardCardComponent: Story = { args: { + buttons: [ + { children: "first button" }, + { children: "second button" }, + ], description: "Lorem ipsum dolor sit amet, consectetur adipisicing elit.", - firstButtonLabel: "first button", imageURL: IMAGES.govActionDelegateImage, - secondButtonLabel: "second button", title: "Action card", }, play: async ({ canvasElement }) => { @@ -38,21 +40,24 @@ export const DashboardCardComponent: Story = { export const WithDRepIdDashboardCardComponent: Story = { args: { + buttons: [ + { children: "first button" }, + { children: "second button" }, + ], description: "Lorem ipsum dolor sit amet, consectetur adipisicing elit.", - firstButtonLabel: "first button", imageURL: IMAGES.govActionDelegateImage, - secondButtonLabel: "second button", title: "Action card", - cardId: "drep1gwsw9ckkhuwscj9savt5f7u9xsrudw209hne7pggcktzuw5sv32", }, }; export const LoadingDashboardCard: Story = { args: { + buttons: [ + { children: "first button" }, + { children: "second button" }, + ], description: "Lorem ipsum dolor sit amet, consectetur adipisicing elit.", - firstButtonLabel: "first button", imageURL: IMAGES.govActionDelegateImage, - secondButtonLabel: "second button", title: "Action card", isLoading: true, }, @@ -67,12 +72,14 @@ export const LoadingDashboardCard: Story = { export const InProgressDashboardCard: Story = { args: { + buttons: [ + { children: "first button" }, + { children: "second button" }, + ], description: "Lorem ipsum dolor sit amet, consectetur adipisicing elit.", - firstButtonLabel: "first button", imageURL: IMAGES.govActionDelegateImage, - secondButtonLabel: "second button", title: "Action card", - inProgress: true, + state: "inProgress", }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); From 66d36fb1eb5382fbb85fc7330c562f9c95b63c8a Mon Sep 17 00:00:00 2001 From: Jan Jaroszczak Date: Wed, 27 Mar 2024 10:41:39 +0100 Subject: [PATCH 07/26] [#257 #374] Display GA using data from BE --- .../molecules/GovernanceActionCard.tsx | 21 ++++--- ...GovernanceActionDetailsCardOnChainData.tsx | 9 +-- .../molecules/GovernanceActionsDatesBox.tsx | 10 +-- .../molecules/GovernanceVotedOnCard.tsx | 45 +++++++------ .../DashboardGovernanceActionDetails.tsx | 18 +++++- .../organisms/GovernanceActionDetailsCard.tsx | 26 +++++++- .../GovernanceActionDetailsCardData.tsx | 63 +++++++++---------- govtool/frontend/src/models/api.ts | 8 ++- .../src/pages/GovernanceActionDetails.tsx | 20 +++++- govtool/frontend/src/types/global.d.ts | 12 +++- 10 files changed, 151 insertions(+), 81 deletions(-) diff --git a/govtool/frontend/src/components/molecules/GovernanceActionCard.tsx b/govtool/frontend/src/components/molecules/GovernanceActionCard.tsx index cbb17ac4b..0be42cea0 100644 --- a/govtool/frontend/src/components/molecules/GovernanceActionCard.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceActionCard.tsx @@ -27,13 +27,11 @@ type ActionTypeProps = Omit< | "abstainVotes" | "metadataHash" | "url" - | "details" | "id" - | "txHash" - | "index" + | "details" + | "rationale" + | "motivation" > & { - txHash: string; - index: number; isDataMissing: boolean; onClick?: () => void; inProgress?: boolean; @@ -44,11 +42,15 @@ export const GovernanceActionCard: FC = ({ ...props }) => { type, inProgress = false, expiryDate, + expiryEpochNo, onClick, createdDate, + createdEpochNo, txHash, index, isDataMissing, + title, + about, } = props; const { isMobile, screenWidth } = useScreenDimension(); const { t } = useTranslation(); @@ -85,13 +87,14 @@ export const GovernanceActionCard: FC = ({ ...props }) => { }} > = ({ ...props }) => { - {data.map(({ label, content }) => ( + {Object.entries(data).map(([label, content]) => ( {label}: diff --git a/govtool/frontend/src/components/molecules/GovernanceActionsDatesBox.tsx b/govtool/frontend/src/components/molecules/GovernanceActionsDatesBox.tsx index 99065e13e..0e302b21d 100644 --- a/govtool/frontend/src/components/molecules/GovernanceActionsDatesBox.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceActionsDatesBox.tsx @@ -8,12 +8,16 @@ import { useScreenDimension, useTranslation } from "@hooks"; type GovernanceActionsDatesBoxProps = { createdDate: string; expiryDate: string; + expiryEpochNo: number; + createdEpochNo: number; isSliderCard?: boolean; }; export const GovernanceActionsDatesBox = ({ createdDate, expiryDate, + expiryEpochNo, + createdEpochNo, isSliderCard, }: GovernanceActionsDatesBoxProps) => { const { t } = useTranslation(); @@ -54,8 +58,7 @@ export const GovernanceActionsDatesBox = ({ > , , @@ -97,8 +100,7 @@ export const GovernanceActionsDatesBox = ({ > , , diff --git a/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx b/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx index 7e5a6aa2b..a93f191f6 100644 --- a/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx @@ -35,6 +35,17 @@ export const GovernanceVotedOnCard = ({ }: Props) => { const navigate = useNavigate(); const { proposal, vote } = votedProposal; + const { + createdDate, + createdEpochNo, + expiryDate, + expiryEpochNo, + type, + txHash, + index, + title, + about, + } = proposal; const { isMobile, screenWidth } = useScreenDimension(); const { t } = useTranslation(); @@ -60,9 +71,7 @@ export const GovernanceVotedOnCard = ({ ? "1px solid #F6D5D5" : "1px solid #C0E4BA", }} - data-testid={`govaction-${getProposalTypeNoEmptySpaces( - proposal.type, - )}-card`} + data-testid={`govaction-${getProposalTypeNoEmptySpaces(type)}-card`} > @@ -121,14 +130,14 @@ export const GovernanceVotedOnCard = ({