diff --git a/govtool/frontend/public/icons/CopyBlueThin.svg b/govtool/frontend/public/icons/CopyBlueThin.svg new file mode 100644 index 000000000..2050c9abe --- /dev/null +++ b/govtool/frontend/public/icons/CopyBlueThin.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/govtool/frontend/public/icons/Share.svg b/govtool/frontend/public/icons/Share.svg new file mode 100644 index 000000000..a8b9eac2b --- /dev/null +++ b/govtool/frontend/public/icons/Share.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/govtool/frontend/src/components/atoms/ClickOutside.tsx b/govtool/frontend/src/components/atoms/ClickOutside.tsx deleted file mode 100644 index 86266bf2a..000000000 --- a/govtool/frontend/src/components/atoms/ClickOutside.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { useRef, useEffect, RefObject } from "react"; - -const useOutsideClick = (ref: RefObject, onClick: () => void) => { - useEffect(() => { - document.addEventListener("mousedown", (e) => { - if (ref.current && !ref.current.contains(e.target as Node)) { - onClick(); - } - }); - - return () => { - document.removeEventListener("mousedown", (e) => { - if (ref.current && !ref.current.contains(e.target as Node)) { - onClick(); - } - }); - }; - }, [ref]); -}; - -interface Props { - children: React.ReactElement; - onClick: () => void; -} - -export const ClickOutside = ({ children, onClick }: Props) => { - const wrapperRef = useRef(null); - useOutsideClick(wrapperRef, onClick); - return
{children}
; -}; diff --git a/govtool/frontend/src/components/atoms/CopyButton.tsx b/govtool/frontend/src/components/atoms/CopyButton.tsx index 65601c718..021d5fcc0 100644 --- a/govtool/frontend/src/components/atoms/CopyButton.tsx +++ b/govtool/frontend/src/components/atoms/CopyButton.tsx @@ -4,11 +4,11 @@ import { ICONS } from "@consts"; import { useSnackbar } from "@context"; import { useTranslation } from "@hooks"; -interface Props { +type Props = { isChecked?: boolean; text: string; - variant?: string; -} + variant?: "blueThin" | "blue"; +}; export const CopyButton = ({ isChecked, text, variant }: Props) => { const { addSuccessAlert } = useSnackbar(); @@ -19,6 +19,10 @@ export const CopyButton = ({ isChecked, text, variant }: Props) => { return ICONS.copyBlueIcon; } + if (variant === "blueThin") { + return ICONS.copyBlueThinIcon; + } + if (isChecked) { return ICONS.copyWhiteIcon; } diff --git a/govtool/frontend/src/components/atoms/ExternalModalButton.tsx b/govtool/frontend/src/components/atoms/ExternalModalButton.tsx new file mode 100644 index 000000000..8584c15b0 --- /dev/null +++ b/govtool/frontend/src/components/atoms/ExternalModalButton.tsx @@ -0,0 +1,49 @@ +import { Typography } from "@mui/material"; + +import { Button } from "@atoms"; +import { ICONS } from "@consts"; +import { useModal } from "@context"; + +export const ExternalModalButton = ({ + label, + url, +}: { + label: string; + url: string; +}) => { + const { openModal } = useModal(); + + return ( + + ); +}; diff --git a/govtool/frontend/src/components/atoms/Radio.tsx b/govtool/frontend/src/components/atoms/Radio.tsx index 538fa7deb..d997c1b58 100644 --- a/govtool/frontend/src/components/atoms/Radio.tsx +++ b/govtool/frontend/src/components/atoms/Radio.tsx @@ -1,6 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Box, Typography } from "@mui/material"; +import { Box } from "@mui/material"; + +import { Typography } from "@atoms"; import { UseFormRegister, UseFormSetValue } from "react-hook-form"; +import { theme } from "@/theme"; type RadioProps = { isChecked: boolean; @@ -10,11 +13,20 @@ type RadioProps = { setValue: UseFormSetValue; register: UseFormRegister; dataTestId?: string; + disabled?: boolean; }; export const Radio = ({ ...props }: RadioProps) => { - const { isChecked, name, setValue, title, value, dataTestId, register } = - props; + const { + isChecked, + name, + setValue, + title, + value, + dataTestId, + register, + disabled, + } = props; const handleClick = () => { setValue(name, value); @@ -23,13 +35,23 @@ export const Radio = ({ ...props }: RadioProps) => { return ( { + if (!disabled) handleClick(); + }} + borderRadius={isChecked ? "15px" : "12px"} + p={isChecked ? "2px" : 0} + border={isChecked ? 2 : 0} + borderColor={isChecked ? "specialCyanBorder" : undefined} + sx={[ + { + boxShadow: theme.shadows[1], + + "&:hover": { + color: "blue", + cursor: disabled ? "default" : "pointer", + }, + }, + ]} > { checked={isChecked} /> {title} diff --git a/govtool/frontend/src/components/atoms/Tooltip.tsx b/govtool/frontend/src/components/atoms/Tooltip.tsx index 4b8b7a90a..dd8d8dc23 100644 --- a/govtool/frontend/src/components/atoms/Tooltip.tsx +++ b/govtool/frontend/src/components/atoms/Tooltip.tsx @@ -1,12 +1,7 @@ import { styled } from "@mui/material"; import * as TooltipMUI from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; - -type TooltipProps = Omit & { - heading?: string; - paragraphOne?: string; - paragraphTwo?: string; -}; +import { TooltipProps } from "@atoms"; const StyledTooltip = styled( ({ className, ...props }: TooltipMUI.TooltipProps) => ( diff --git a/govtool/frontend/src/components/atoms/VotePill.tsx b/govtool/frontend/src/components/atoms/VotePill.tsx index d28ed9da4..de29ac900 100644 --- a/govtool/frontend/src/components/atoms/VotePill.tsx +++ b/govtool/frontend/src/components/atoms/VotePill.tsx @@ -1,6 +1,7 @@ -import { Vote } from "@models"; import { Box, Typography } from "@mui/material"; +import { Vote } from "@models"; + export const VotePill = ({ vote, width, diff --git a/govtool/frontend/src/components/atoms/index.ts b/govtool/frontend/src/components/atoms/index.ts index 7072b7635..186752f80 100644 --- a/govtool/frontend/src/components/atoms/index.ts +++ b/govtool/frontend/src/components/atoms/index.ts @@ -2,9 +2,9 @@ export * from "./ActionRadio"; export * from "./Background"; export * from "./Button"; export * from "./Checkbox"; -export * from "./ClickOutside"; export * from "./CopyButton"; export * from "./DrawerLink"; +export * from "./ExternalModalButton"; export * from "./FormErrorMessage"; export * from "./FormHelpfulText"; export * from "./HighlightedText"; diff --git a/govtool/frontend/src/components/atoms/types.ts b/govtool/frontend/src/components/atoms/types.ts index 34f0fdf14..ff2e603b8 100644 --- a/govtool/frontend/src/components/atoms/types.ts +++ b/govtool/frontend/src/components/atoms/types.ts @@ -7,6 +7,7 @@ import { TextareaAutosizeProps, SxProps, } from "@mui/material"; +import * as TooltipMUI from "@mui/material/Tooltip"; export type ButtonProps = Omit & { size?: "small" | "medium" | "large" | "extraLarge"; @@ -71,3 +72,9 @@ export type InfoTextProps = { label: string; sx?: SxProps; }; + +export type TooltipProps = Omit & { + heading?: string; + paragraphOne?: string; + paragraphTwo?: string; +}; diff --git a/govtool/frontend/src/components/molecules/Breadcrumbs.tsx b/govtool/frontend/src/components/molecules/Breadcrumbs.tsx new file mode 100644 index 000000000..8a9e564a2 --- /dev/null +++ b/govtool/frontend/src/components/molecules/Breadcrumbs.tsx @@ -0,0 +1,62 @@ +import { NavLink, To } from "react-router-dom"; +import { Box } from "@mui/material"; +import Divider from "@mui/material/Divider"; + +import { useScreenDimension, useTranslation } from "@hooks"; +import { Typography } from "@atoms"; + +type BreadcrumbsProps = { + elementOne: string; + elementOnePath: To; + elementTwo: string; + isDataMissing: boolean; +}; + +export const Breadcrumbs = ({ + elementOne, + elementOnePath, + elementTwo, + isDataMissing, +}: BreadcrumbsProps) => { + const { t } = useTranslation(); + const { isMobile } = useScreenDimension(); + + return ( + + + + {elementOne} + + + + + {isDataMissing ? t("govActions.dataMissing") : elementTwo} + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/DataActionsBar.tsx b/govtool/frontend/src/components/molecules/DataActionsBar.tsx index 86613a8bc..08e27a1db 100644 --- a/govtool/frontend/src/components/molecules/DataActionsBar.tsx +++ b/govtool/frontend/src/components/molecules/DataActionsBar.tsx @@ -2,10 +2,8 @@ import { Dispatch, FC, SetStateAction } from "react"; import { Box, InputBase } from "@mui/material"; import Search from "@mui/icons-material/Search"; -import { GovernanceActionsFilters, GovernanceActionsSorting } from "."; +import { GovernanceActionsFilters, GovernanceActionsSorting } from "@molecules"; import { OrderActionsChip } from "./OrderActionsChip"; -import { ClickOutside } from "../atoms"; - import { theme } from "@/theme"; type DataActionsBarProps = { @@ -50,7 +48,7 @@ export const DataActionsBar: FC = ({ ...props }) => { return ( <> - + setSearchText(e.target.value)} @@ -76,7 +74,7 @@ export const DataActionsBar: FC = ({ ...props }) => { fontWeight: 500, height: 48, padding: "16px 24px", - width: 231, + width: 500, }} /> = ({ ...props }) => { setSortOpen={setSortOpen} sortingActive={sortingActive} sortOpen={sortOpen} - /> + > + {filtersOpen && ( + + )} + {sortOpen && ( + + )} + - {filtersOpen && ( - - - - )} - {sortOpen && ( - - - - )} ); }; diff --git a/govtool/frontend/src/components/molecules/DataMissingInfoBox.tsx b/govtool/frontend/src/components/molecules/DataMissingInfoBox.tsx new file mode 100644 index 000000000..06830bdf5 --- /dev/null +++ b/govtool/frontend/src/components/molecules/DataMissingInfoBox.tsx @@ -0,0 +1,60 @@ +import { Box, Link } from "@mui/material"; + +import { Typography } from "@atoms"; +import { useTranslation } from "@hooks"; +import { openInNewTab } from "@utils"; + +export const DataMissingInfoBox = () => { + const { t } = useTranslation(); + + return ( + + + {/* TODO: Text to confirm/change */} + The data that was originally used when this Governance Action was + created has been formatted incorrectly. + + + {/* TODO: Text to confirm/change */} + GovTool uses external sources for Governance Action data, and these + sources are maintained by the proposers of the Actions. This error means + that GovTool cannot locate the data on the URL specified when the + Governance Action was originally posted. + + + // TODO: Add the correct link + openInNewTab( + "https://docs.sanchogov.tools/how-to-use-the-govtool/getting-started/get-a-compatible-wallet" + ) + } + sx={{ + fontFamily: "Poppins", + fontSize: "16px", + lineHeight: "24px", + cursor: "pointer", + }} + > + {t("learnMore")} + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/GovernanceActionCard.tsx b/govtool/frontend/src/components/molecules/GovernanceActionCard.tsx index 567a3fb50..cbb17ac4b 100644 --- a/govtool/frontend/src/components/molecules/GovernanceActionCard.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceActionCard.tsx @@ -1,35 +1,43 @@ import { FC } from "react"; import { Box } from "@mui/material"; -import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; -import { Button, Typography, Tooltip } from "@atoms"; +import { Button } from "@atoms"; +import { + GovernanceActionCardElement, + GovernanceActionCardHeader, + GovernanceActionCardStatePill, + GovernanceActionsDatesBox, +} from "@molecules"; + import { useScreenDimension, useTranslation } from "@hooks"; import { formatDisplayDate, getFullGovActionId, getProposalTypeLabel, - getShortenedGovActionId, + getProposalTypeNoEmptySpaces, } from "@utils"; -import { theme } from "@/theme"; -interface ActionTypeProps - extends Omit< - ActionType, - | "yesVotes" - | "noVotes" - | "abstainVotes" - | "metadataHash" - | "url" - | "details" - | "id" - | "txHash" - | "index" - > { - onClick?: () => void; - inProgress?: boolean; +const mockedLongText = + "Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sit, distinctio culpa minus eaque illo quidem voluptates quisquam mollitia consequuntur ex, sequi saepe? Ad ex adipisci molestiae sed."; + +type ActionTypeProps = Omit< + ActionType, + | "yesVotes" + | "noVotes" + | "abstainVotes" + | "metadataHash" + | "url" + | "details" + | "id" + | "txHash" + | "index" +> & { txHash: string; index: number; -} + isDataMissing: boolean; + onClick?: () => void; + inProgress?: boolean; +}; export const GovernanceActionCard: FC = ({ ...props }) => { const { @@ -40,193 +48,82 @@ export const GovernanceActionCard: FC = ({ ...props }) => { createdDate, txHash, index, + isDataMissing, } = props; const { isMobile, screenWidth } = useScreenDimension(); const { t } = useTranslation(); - const { - palette: { lightBlue }, - } = theme; - const govActionId = getFullGovActionId(txHash, index); - const proposalTypeNoEmptySpaces = getProposalTypeLabel(type).replace( - / /g, - "", - ); return ( - {inProgress && ( - - - {t("inProgress")} - - - )} + {inProgress && } - - - {t("govActions.governanceActionType")} - - - - - {getProposalTypeLabel(type)} - - - - - - - {t("govActions.governanceActionId")} - - - - - {getShortenedGovActionId(txHash, index)} - - - - + + + + + - {createdDate ? ( - - - {t("govActions.submissionDate")} - - - {formatDisplayDate(createdDate)} - - - - - - ) : null} - {expiryDate ? ( - - - {t("govActions.expiryDate")} - - - {formatDisplayDate(expiryDate)} - - - - - - ) : null} diff --git a/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx b/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx new file mode 100644 index 000000000..94517a9ca --- /dev/null +++ b/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx @@ -0,0 +1,136 @@ +import { Box } from "@mui/material"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; + +import { Typography, Tooltip, CopyButton, TooltipProps } from "@atoms"; + +type BaseProps = { + label: string; + text: string; + dataTestId?: string; + isSliderCard?: boolean; + tooltipProps?: Omit; + marginBottom?: number; +}; + +type PillVariantProps = BaseProps & { + textVariant: "pill"; + isCopyButton?: false; +}; + +type OtherVariantsProps = BaseProps & { + textVariant?: "oneLine" | "twoLines" | "longText"; + isCopyButton?: boolean; +}; + +type GovernanceActionCardElementProps = PillVariantProps | OtherVariantsProps; + +export const GovernanceActionCardElement = ({ + label, + text, + dataTestId, + isSliderCard, + textVariant = "oneLine", + isCopyButton, + tooltipProps, + marginBottom, +}: GovernanceActionCardElementProps) => ( + + + + {label} + + {tooltipProps && ( + + + + )} + + + {textVariant === "pill" ? ( + + + {text} + + + ) : ( + + + {text} + + {isCopyButton && ( + + + + )} + + )} + + +); diff --git a/govtool/frontend/src/components/molecules/GovernanceActionCardHeader.tsx b/govtool/frontend/src/components/molecules/GovernanceActionCardHeader.tsx new file mode 100644 index 000000000..06314cea2 --- /dev/null +++ b/govtool/frontend/src/components/molecules/GovernanceActionCardHeader.tsx @@ -0,0 +1,60 @@ +import { Box } from "@mui/material"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; + +import { Tooltip, Typography } from "@atoms"; +import { useTranslation } from "@hooks"; + +type GovernanceActionCardHeaderProps = { + title: string; + isDataMissing: boolean; +}; + +export const GovernanceActionCardHeader = ({ + title, + isDataMissing, +}: GovernanceActionCardHeaderProps) => { + const { t } = useTranslation(); + + return ( + + + {isDataMissing ? t("govActions.dataMissing") : title} + + {isDataMissing && ( + + + + )} + + ); +}; diff --git a/govtool/frontend/src/components/molecules/GovernanceActionCardMyVote.tsx b/govtool/frontend/src/components/molecules/GovernanceActionCardMyVote.tsx new file mode 100644 index 000000000..c1dde83ec --- /dev/null +++ b/govtool/frontend/src/components/molecules/GovernanceActionCardMyVote.tsx @@ -0,0 +1,60 @@ +import { Box } from "@mui/material"; + +import { Button, Typography, VotePill } from "@atoms"; +import { openInNewTab } from "@utils"; +import { useTranslation } from "@hooks"; +import { Vote } from "@models"; + +type Props = { + vote: Vote; +}; + +export const GovernanceActionCardMyVote = ({ vote }: Props) => { + const { t } = useTranslation(); + + return ( + + + {t("govActions.myVote")} + + + + + + + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/GovernanceActionCardStatePill.tsx b/govtool/frontend/src/components/molecules/GovernanceActionCardStatePill.tsx new file mode 100644 index 000000000..bba17071c --- /dev/null +++ b/govtool/frontend/src/components/molecules/GovernanceActionCardStatePill.tsx @@ -0,0 +1,51 @@ +import { Box } from "@mui/material"; +import CheckIcon from "@mui/icons-material/Check"; + +import { Typography } from "@atoms"; +import { useTranslation } from "@hooks"; + +export const GovernanceActionCardStatePill = ({ + variant = "voteSubmitted", +}: { + variant?: "inProgress" | "voteSubmitted"; +}) => { + const { t } = useTranslation(); + + return ( + + + {variant === "voteSubmitted" && ( + + )} + {variant === "inProgress" + ? t("inProgress") + : t("govActions.voteSubmitted")} + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardHeader.tsx b/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardHeader.tsx new file mode 100644 index 000000000..5ba07c5c6 --- /dev/null +++ b/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardHeader.tsx @@ -0,0 +1,57 @@ +import { useLocation } from "react-router-dom"; +import { Box } from "@mui/material"; + +import { Typography } from "@atoms"; +import { Share } from "@molecules"; +import { useTranslation } from "@hooks"; + +type GovernanceActionDetailsCardHeaderProps = { + title: string; + isDataMissing: boolean; +}; + +export const GovernanceActionDetailsCardHeader = ({ + title, + isDataMissing, +}: GovernanceActionDetailsCardHeaderProps) => { + const { t } = useTranslation(); + const { pathname, hash } = useLocation(); + + const govActionLinkToShare = `${window.location.protocol}//${ + window.location.hostname + }${window.location.port ? `:${window.location.port}` : ""}${pathname}${hash}`; + + return ( + + + + {isDataMissing ? t("govActions.dataMissing") : title} + + + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardLinks.tsx b/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardLinks.tsx new file mode 100644 index 000000000..8028668be --- /dev/null +++ b/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardLinks.tsx @@ -0,0 +1,58 @@ +import { Box } from "@mui/material"; + +import { Typography } from "@atoms"; +import { useScreenDimension, useTranslation } from "@hooks"; +import { LinkWithIcon } from "@molecules"; +import { openInNewTab } from "@/utils"; +import { ICONS } from "@/consts"; + +// TODO: When BE is ready, pass links as props +const LINKS = [ + "https://docs.sanchogov.tools/support/get-help-in-discord", + "https://docs.sanchogov.tools/how-to-use-the-govtool/prerequsites", + "https://docs.sanchogov.tools/faqs", + "https://docs.sanchogov.tools/", +]; + +export const GovernanceActionDetailsCardLinks = () => { + const { isMobile } = useScreenDimension(); + const { t } = useTranslation(); + + return ( + <> + + {t("govActions.supportingLinks")} + + + {LINKS.map((link) => ( + openInNewTab(link)} + icon={link} + cutWithEllipsis + /> + ))} + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardOnChainData.tsx b/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardOnChainData.tsx new file mode 100644 index 000000000..e58aba1ed --- /dev/null +++ b/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardOnChainData.tsx @@ -0,0 +1,79 @@ +import { Box } from "@mui/material"; + +import { Typography } from "@atoms"; +import { useTranslation } from "@hooks"; + +type GovernanceActionDetailsCardOnChainDataProps = { + data: { + label: string; + content: string; + }[]; +}; + +export const GovernanceActionDetailsCardOnChainData = ({ + data, +}: GovernanceActionDetailsCardOnChainDataProps) => { + const { t } = useTranslation(); + + return ( + + + + {t("govActions.onChainTransactionDetails")} + + + {data.map(({ label, content }) => ( + + + {label}: + + + {content} + + + ))} + + ); +}; diff --git a/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardVotes.tsx b/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardVotes.tsx new file mode 100644 index 000000000..2abf51ca9 --- /dev/null +++ b/govtool/frontend/src/components/molecules/GovernanceActionDetailsCardVotes.tsx @@ -0,0 +1,61 @@ +import { Dispatch, SetStateAction } from "react"; +import { Box } from "@mui/material"; + +import { useScreenDimension } from "@hooks"; +import { VoteActionForm, VotesSubmitted } from "@molecules"; + +type GovernanceActionCardVotesProps = { + setIsVoteSubmitted: Dispatch>; + abstainVotes: number; + noVotes: number; + yesVotes: number; + isOneColumn: boolean; + isVoter?: boolean; + voteFromEP?: string; + isDashboard?: boolean; + isInProgress?: boolean; +}; + +export const GovernanceActionDetailsCardVotes = ({ + setIsVoteSubmitted, + abstainVotes, + noVotes, + yesVotes, + isOneColumn, + isVoter, + voteFromEP, + isDashboard, + isInProgress, +}: GovernanceActionCardVotesProps) => { + const { screenWidth } = useScreenDimension(); + + const isModifiedPadding = + (isDashboard && screenWidth < 1368) ?? screenWidth < 1100; + + return ( + + {isVoter ? ( + + ) : ( + + )} + + ); +}; diff --git a/govtool/frontend/src/components/molecules/GovernanceActionsDatesBox.tsx b/govtool/frontend/src/components/molecules/GovernanceActionsDatesBox.tsx new file mode 100644 index 000000000..99065e13e --- /dev/null +++ b/govtool/frontend/src/components/molecules/GovernanceActionsDatesBox.tsx @@ -0,0 +1,126 @@ +import { Box } from "@mui/material"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import { Trans } from "react-i18next"; + +import { Tooltip, Typography } from "@atoms"; +import { useScreenDimension, useTranslation } from "@hooks"; + +type GovernanceActionsDatesBoxProps = { + createdDate: string; + expiryDate: string; + isSliderCard?: boolean; +}; + +export const GovernanceActionsDatesBox = ({ + createdDate, + expiryDate, + isSliderCard, +}: GovernanceActionsDatesBoxProps) => { + const { t } = useTranslation(); + const { screenWidth } = useScreenDimension(); + + const isFontSizeSmaller = screenWidth < 420; + + return ( + + + + , + , + ]} + /> + + + + + + + + , + , + ]} + /> + + + + + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/GovernanceActionsFilters.tsx b/govtool/frontend/src/components/molecules/GovernanceActionsFilters.tsx index 05746f111..29bf5cc99 100644 --- a/govtool/frontend/src/components/molecules/GovernanceActionsFilters.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceActionsFilters.tsx @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction, useCallback } from "react"; +import { Dispatch, SetStateAction, useCallback, useRef } from "react"; import { Box, Checkbox, @@ -8,16 +8,18 @@ import { } from "@mui/material"; import { GOVERNANCE_ACTIONS_FILTERS } from "@consts"; -import { useTranslation } from "@hooks"; +import { useOnClickOutside, useScreenDimension, useTranslation } from "@hooks"; interface Props { chosenFilters: string[]; setChosenFilters: Dispatch>; + closeFilters: () => void; } export const GovernanceActionsFilters = ({ chosenFilters, setChosenFilters, + closeFilters, }: Props) => { const handleFilterChange = useCallback( (e: React.ChangeEvent) => { @@ -36,6 +38,10 @@ export const GovernanceActionsFilters = ({ ); const { t } = useTranslation(); + const { isMobile, screenWidth } = useScreenDimension(); + + const wrapperRef = useRef(null); + useOnClickOutside(wrapperRef, closeFilters); return ( >; + closeSorts: () => void; } export const GovernanceActionsSorting = ({ chosenSorting, setChosenSorting, + closeSorts, }: Props) => { const { t } = useTranslation(); + const wrapperRef = useRef(null); + useOnClickOutside(wrapperRef, closeSorts); + return ( diff --git a/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx b/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx index 817c8495d..7e5a6aa2b 100644 --- a/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceVotedOnCard.tsx @@ -1,9 +1,7 @@ import { useNavigate } from "react-router-dom"; import { Box } from "@mui/material"; -import CheckIcon from "@mui/icons-material/Check"; -import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; -import { Button, VotePill, Typography, Tooltip } from "@atoms"; +import { Button } from "@atoms"; import { PATHS } from "@consts"; import { useScreenDimension, useTranslation } from "@hooks"; import { VotedProposal } from "@models"; @@ -11,239 +9,114 @@ import { formatDisplayDate, getFullGovActionId, getProposalTypeLabel, - getShortenedGovActionId, - openInNewTab, + getProposalTypeNoEmptySpaces, } from "@utils"; -import { theme } from "@/theme"; +import { + GovernanceActionCardElement, + GovernanceActionCardHeader, + GovernanceActionCardMyVote, + GovernanceActionCardStatePill, + GovernanceActionsDatesBox, +} from "@molecules"; + +const mockedLongText = + "Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sit, distinctio culpa minus eaque illo quidem voluptates quisquam mollitia consequuntur ex, sequi saepe? Ad ex adipisci molestiae sed."; -interface Props { +type Props = { votedProposal: VotedProposal; + isDataMissing: boolean; inProgress?: boolean; -} +}; -export const GovernanceVotedOnCard = ({ votedProposal, inProgress }: Props) => { +export const GovernanceVotedOnCard = ({ + votedProposal, + isDataMissing, + inProgress, +}: Props) => { const navigate = useNavigate(); const { proposal, vote } = votedProposal; - const { - palette: { lightBlue }, - } = theme; - const { isMobile } = useScreenDimension(); - const { t } = useTranslation(); - const proposalTypeNoEmptySpaces = getProposalTypeLabel(proposal.type).replace( - / /g, - "", - ); + const { isMobile, screenWidth } = useScreenDimension(); + const { t } = useTranslation(); return ( + - - {inProgress ? ( - t("inProgress") - ) : ( - <> - - {t("govActions.voteSubmitted")} - - )} - - - - - - {t("govActions.governanceActionType")} - - - - - {getProposalTypeLabel(proposal.type)} - - - - - - - {t("govActions.governanceActionId")} - - - - - {getShortenedGovActionId(proposal.txHash, proposal.index)} - - - - - - - {t("govActions.myVote")} - - - - - - - - + + + + + + - {proposal.createdDate ? ( - - - {t("govActions.submissionDate")} - - - {formatDisplayDate(proposal.createdDate)} - - - - - - ) : null} - {proposal.expiryDate ? ( - - - {t("govActions.expiryDate")} - - - {formatDisplayDate(proposal.expiryDate)} - - - - - - ) : null} diff --git a/govtool/frontend/src/components/molecules/LinkWithIcon.tsx b/govtool/frontend/src/components/molecules/LinkWithIcon.tsx index ab12e106c..ecfa97b16 100644 --- a/govtool/frontend/src/components/molecules/LinkWithIcon.tsx +++ b/govtool/frontend/src/components/molecules/LinkWithIcon.tsx @@ -10,6 +10,7 @@ export const LinkWithIcon = ({ onClick, icon, sx, + cutWithEllipsis, }: LinkWithIconProps) => ( {label} diff --git a/govtool/frontend/src/components/molecules/OrderActionsChip.tsx b/govtool/frontend/src/components/molecules/OrderActionsChip.tsx index ea1009154..006133653 100644 --- a/govtool/frontend/src/components/molecules/OrderActionsChip.tsx +++ b/govtool/frontend/src/components/molecules/OrderActionsChip.tsx @@ -1,23 +1,30 @@ import { Dispatch, SetStateAction } from "react"; -import { Box, Typography } from "@mui/material"; +import { Box } from "@mui/material"; +import { useScreenDimension, useTranslation } from "@hooks"; +import { Typography } from "@atoms"; import { ICONS } from "@consts"; import { theme } from "@/theme"; -interface Props { +type Props = { filtersOpen?: boolean; setFiltersOpen?: Dispatch>; chosenFiltersLength?: number; sortOpen: boolean; setSortOpen: Dispatch>; sortingActive: boolean; + children?: React.ReactNode; isFiltering?: boolean; -} +}; export const OrderActionsChip = (props: Props) => { + const { t } = useTranslation(); + const { isMobile } = useScreenDimension(); + const { palette: { secondary }, } = theme; + const { filtersOpen, setFiltersOpen = () => {}, @@ -26,33 +33,69 @@ export const OrderActionsChip = (props: Props) => { setSortOpen, sortingActive, isFiltering = true, + children, } = props; return ( - + {isFiltering && ( - + { + setSortOpen(false); + if (isFiltering) { + setFiltersOpen(!filtersOpen); + } + }} + data-testid="filters-button" + > filter { - setSortOpen(false); - if (isFiltering) { - setFiltersOpen(!filtersOpen); - } - }} src={filtersOpen ? ICONS.filterWhiteIcon : ICONS.filterIcon} style={{ - background: filtersOpen ? secondary.main : "transparent", borderRadius: "100%", - cursor: "pointer", - padding: "14px", + marginRight: "8px", overflow: "visible", height: 20, width: 20, objectFit: "contain", + ...(isMobile && { + background: filtersOpen ? secondary.main : "transparent", + padding: "14px", + marginRight: "0", + }), }} /> + {!isMobile && ( + + {t("filter")} + + )} {!filtersOpen && chosenFiltersLength > 0 && ( { height: "16px", justifyContent: "center", position: "absolute", - right: "0", + right: "-3px", top: "0", width: "16px", }} @@ -77,27 +120,54 @@ export const OrderActionsChip = (props: Props) => { )} )} - + { + if (isFiltering) { + setFiltersOpen(false); + } + setSortOpen(!sortOpen); + }} + data-testid="sort-button" + > sort { - if (isFiltering) { - setFiltersOpen(false); - } - setSortOpen(!sortOpen); - }} src={sortOpen ? ICONS.sortWhiteIcon : ICONS.sortIcon} style={{ - background: sortOpen ? secondary.main : "transparent", borderRadius: "100%", - cursor: "pointer", - padding: "14px", - height: 24, - width: 24, + marginRight: "8px", + height: 20, + width: 20, objectFit: "contain", + ...(isMobile && { + background: sortOpen ? secondary.main : "transparent", + padding: "14px", + marginRight: "0", + }), }} /> + {!isMobile && ( + + {t("sort")} + + )} {!sortOpen && sortingActive && ( { height: "16px", justifyContent: "center", position: "absolute", - right: "0", + right: "-3px", top: "0", width: "16px", }} @@ -119,6 +189,7 @@ export const OrderActionsChip = (props: Props) => { )} + {children} ); }; diff --git a/govtool/frontend/src/components/molecules/Share.tsx b/govtool/frontend/src/components/molecules/Share.tsx new file mode 100644 index 000000000..a57e7763d --- /dev/null +++ b/govtool/frontend/src/components/molecules/Share.tsx @@ -0,0 +1,97 @@ +import { useState } from "react"; +import { Box, Popover } from "@mui/material"; + +import { Typography } from "@atoms"; +import { ICONS } from "@consts"; +import { useSnackbar } from "@context"; +import { useTranslation } from "@hooks"; + +export const Share = ({ link }: { link: string }) => { + const { addSuccessAlert } = useSnackbar(); + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = (e: React.MouseEvent) => { + setAnchorEl(e.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const onCopy = (e: React.MouseEvent) => { + navigator.clipboard.writeText(link); + addSuccessAlert(t("alerts.copiedToClipboard")); + e.stopPropagation(); + }; + + const open = Boolean(anchorEl); + const id = open ? "simple-popover" : undefined; + + return ( + <> + + share icon + + + + {t("share")} + + link + + {t("clickToCopyLink")} + + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/SliderArrow.tsx b/govtool/frontend/src/components/molecules/SliderArrow.tsx new file mode 100644 index 000000000..b4b079d48 --- /dev/null +++ b/govtool/frontend/src/components/molecules/SliderArrow.tsx @@ -0,0 +1,45 @@ +import { Box } from "@mui/material"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; + +import { theme } from "@/theme"; + +type SliderArrowProps = { + disabled: boolean; + onClick: (e: React.MouseEvent) => void; + left?: boolean; +}; + +export const SliderArrow = ({ disabled, onClick, left }: SliderArrowProps) => { + const { + palette: { primaryBlue, arcticWhite, lightBlue }, + } = theme; + + return ( + + + + ); +}; diff --git a/govtool/frontend/src/components/molecules/SliderArrows.tsx b/govtool/frontend/src/components/molecules/SliderArrows.tsx new file mode 100644 index 000000000..6d5f9e7a6 --- /dev/null +++ b/govtool/frontend/src/components/molecules/SliderArrows.tsx @@ -0,0 +1,46 @@ +import { KeenSliderHooks, KeenSliderInstance } from "keen-slider/react"; +import { SliderArrow } from "@molecules"; +import { Box } from "@mui/material"; + +type SliderArrowsProps = { + currentSlide: number; + instanceRef: React.MutableRefObject | null>; + itemsPerView: number; +}; + +export const SliderArrows = ({ + currentSlide, + instanceRef, + itemsPerView, +}: SliderArrowsProps) => + instanceRef.current && ( + + ) => { + e.stopPropagation(); + instanceRef.current?.prev(); + }} + disabled={currentSlide === 0} + /> + ) => { + e.stopPropagation(); + instanceRef.current?.next(); + }} + disabled={ + currentSlide + itemsPerView >= + instanceRef.current.track.details.slides.length + } + /> + + ); diff --git a/govtool/frontend/src/components/molecules/VoteActionForm.tsx b/govtool/frontend/src/components/molecules/VoteActionForm.tsx index 4c7413400..5222bea75 100644 --- a/govtool/frontend/src/components/molecules/VoteActionForm.tsx +++ b/govtool/frontend/src/components/molecules/VoteActionForm.tsx @@ -1,4 +1,11 @@ -import { useState, useEffect, useMemo, useCallback } from "react"; +import { + useState, + useEffect, + useMemo, + useCallback, + Dispatch, + SetStateAction, +} from "react"; import { useLocation } from "react-router-dom"; import { Box, Link } from "@mui/material"; @@ -13,22 +20,34 @@ import { } from "@hooks"; import { openInNewTab } from "@utils"; +import { Trans } from "react-i18next"; import { ControlledField } from "../organisms"; +// TODO: Decide with BE on how cast votes will be implemented +// and adjust accordingly the component below (UI is ready). +const castVoteDate = undefined; +const castVoteChangeDeadline = "20.06.2024 (Epoch 445)"; + +type VoteActionFormProps = { + setIsVoteSubmitted: Dispatch>; + voteFromEP?: string; + yesVotes: number; + noVotes: number; + abstainVotes: number; + isInProgress?: boolean; +}; + export const VoteActionForm = ({ + setIsVoteSubmitted, voteFromEP, yesVotes, noVotes, abstainVotes, -}: { - voteFromEP?: string; - yesVotes: number; - noVotes: number; - abstainVotes: number; -}) => { - const { state } = useLocation(); + isInProgress, +}: VoteActionFormProps) => { const [isContext, setIsContext] = useState(false); - const { isMobile, screenWidth } = useScreenDimension(); + const { state } = useLocation(); + const { isMobile } = useScreenDimension(); const { openModal } = useModal(); const { t } = useTranslation(); const { voter } = useGetVoterInfo(); @@ -49,8 +68,10 @@ export const VoteActionForm = ({ useEffect(() => { if (state && state.vote) { setValue("vote", state.vote); + setIsVoteSubmitted(true); } else if (voteFromEP) { setValue("vote", voteFromEP); + setIsVoteSubmitted(true); } }, [state, voteFromEP, setValue]); @@ -79,81 +100,124 @@ export const VoteActionForm = ({ [state], ); - const renderChangeVoteButton = useMemo(() => ( - - {t('govActions.changeVote')} - - ), + const renderChangeVoteButton = useMemo( + () => ( + + {t("govActions.changeVote")} + + ), [confirmVote, areFormErrors, vote, isVoteLoading], ); return ( - - - - {t("govActions.chooseHowToVote")} - - - - - - - - - - + + + {castVoteDate ? ( + <> + + ]} + /> + + + {t("govActions.castVoteDeadline", { + date: castVoteChangeDeadline, + })} + + + ) : ( + + {t("govActions.chooseHowToVote")} + + )} + + + + {(voter?.isRegisteredAsDRep || voter?.isRegisteredAsSoleVoter) && ( )} + {/* TODO: Change below into new voting context */}

{isContext && ( - + - @@ -228,7 +300,8 @@ export const VoteActionForm = ({ "https://docs.sanchogov.tools/faqs/how-to-create-a-metadata-anchor", ) } - mb={isMobile ? 2 : 8} + mt="12px" + mb={isMobile ? 2 : 6} sx={{ cursor: "pointer" }} textAlign="center" visibility={!isContext ? "hidden" : "visible"} @@ -251,16 +324,16 @@ export const VoteActionForm = ({ {t("govActions.selectDifferentOption")} {(state?.vote && state?.vote !== vote) || - (voteFromEP && voteFromEP !== vote) ? ( - - {isMobile ? renderChangeVoteButton : renderCancelButton} - - {isMobile ? renderCancelButton : renderChangeVoteButton} - + (voteFromEP && voteFromEP !== vote) ? ( + + {isMobile ? renderChangeVoteButton : renderCancelButton} + + {isMobile ? renderCancelButton : renderChangeVoteButton} + ) : ( { - const { - palette: { lightBlue }, - } = theme; - const { isMobile } = useScreenDimension(); const { t } = useTranslation(); return ( @@ -31,68 +26,66 @@ export const VotesSubmitted = ({ yesVotes, noVotes, abstainVotes }: Props) => { src={IMAGES.govActionListImage} width="64px" height="64px" - style={{ marginBottom: "10px" }} + style={{ marginBottom: "24px" }} /> - + {t("govActions.voteSubmitted")} - - {t("govActions.forGovAction")} - - - {t("govActions.votesSubmittedOnChain")} - - {t("govActions.votes")} + {t("govActions.forGovAction")} + + + {t("govActions.votesSubmittedOnChain")} - + ₳ {correctAdaFormat(yesVotes)} - + ₳ {correctAdaFormat(abstainVotes)} - + ₳ {correctAdaFormat(noVotes)} diff --git a/govtool/frontend/src/components/molecules/index.ts b/govtool/frontend/src/components/molecules/index.ts index b87a802df..78d4f0563 100644 --- a/govtool/frontend/src/components/molecules/index.ts +++ b/govtool/frontend/src/components/molecules/index.ts @@ -1,18 +1,32 @@ export * from "./ActionCard"; +export * from "./Breadcrumbs"; export * from "./Card"; export * from "./CenteredBoxBottomButtons"; export * from "./CenteredBoxPageWrapper"; export * from "./DashboardActionCard"; export * from "./DataActionsBar"; +export * from "./DataMissingInfoBox"; export * from "./DRepInfoCard"; export * from "./Field"; export * from "./GovActionDetails"; export * from "./GovernanceActionCard"; +export * from "./GovernanceActionCardElement"; +export * from "./GovernanceActionCardHeader"; +export * from "./GovernanceActionDetailsCardLinks"; +export * from "./GovernanceActionCardMyVote"; +export * from "./GovernanceActionCardStatePill"; +export * from "./GovernanceActionDetailsCardVotes"; +export * from "./GovernanceActionDetailsCardHeader"; +export * from "./GovernanceActionDetailsCardOnChainData"; +export * from "./GovernanceActionsDatesBox"; export * from "./GovernanceActionsFilters"; export * from "./GovernanceActionsSorting"; export * from "./GovernanceVotedOnCard"; export * from "./LinkWithIcon"; export * from "./OrderActionsChip"; +export * from "./Share"; +export * from "./SliderArrow"; +export * from "./SliderArrows"; export * from "./Step"; export * from "./VoteActionForm"; export * from "./VotesSubmitted"; diff --git a/govtool/frontend/src/components/molecules/types.ts b/govtool/frontend/src/components/molecules/types.ts index 0ed7df185..dbe0db33c 100644 --- a/govtool/frontend/src/components/molecules/types.ts +++ b/govtool/frontend/src/components/molecules/types.ts @@ -5,6 +5,7 @@ export type LinkWithIconProps = { onClick: () => void; icon?: JSX.Element; sx?: SxProps; + cutWithEllipsis?: boolean; }; export type StepProps = { diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx index 586005b07..480de270c 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx @@ -1,17 +1,10 @@ import { useNavigate, useLocation, - NavLink, useParams, generatePath, } from "react-router-dom"; -import { - Box, - Breadcrumbs, - CircularProgress, - Link, - Typography, -} from "@mui/material"; +import { Box, CircularProgress, Link, Typography } from "@mui/material"; import { ICONS, PATHS } from "@consts"; import { @@ -20,18 +13,24 @@ import { useScreenDimension, useTranslation, } from "@hooks"; -import { GovernanceActionDetailsCard } from "@organisms"; import { formatDisplayDate, getShortenedGovActionId, getProposalTypeLabel, } from "@utils"; +import { GovernanceActionDetailsCard } from "@organisms"; +import { Breadcrumbs } from "@molecules"; +import { useCardano } from "@/context"; + +// TODO: Remove when data validation is ready +const isDataMissing = false; export const DashboardGovernanceActionDetails = () => { const { voter } = useGetVoterInfo(); + const { pendingTransaction } = useCardano(); const { state, hash } = useLocation(); const navigate = useNavigate(); - const { isMobile, screenWidth } = useScreenDimension(); + const { isMobile } = useScreenDimension(); const { t } = useTranslation(); const { proposalId } = useParams(); const fullProposalId = proposalId + hash; @@ -43,21 +42,6 @@ export const DashboardGovernanceActionDetails = () => { state ? state.index : data?.proposal.index ?? "", ); - const breadcrumbs = [ - - - {t("govActions.title")} - - , - - {t("govActions.voteOnGovActions")} - , - ]; - return ( { flex={1} > - {breadcrumbs} - + elementOne={t("govActions.title")} + elementOnePath={PATHS.dashboardGovernanceActions} + elementTwo="Fund our project" + isDataMissing={false} + /> { style={{ marginRight: "12px", transform: "rotate(180deg)" }} /> - {t("backToList")} + {t("back")} @@ -128,23 +108,32 @@ export const DashboardGovernanceActionDetails = () => { ? formatDisplayDate(state.createdDate) : formatDisplayDate(data.proposal.createdDate) } - details={state ? state.details : data.proposal.details} + // TODO: Add data validation + isDataMissing={isDataMissing} expiryDate={ state ? formatDisplayDate(state.expiryDate) : formatDisplayDate(data.proposal.expiryDate) } - isDRep={voter?.isRegisteredAsDRep || voter?.isRegisteredAsSoleVoter} + isVoter={ + voter?.isRegisteredAsDRep || voter?.isRegisteredAsSoleVoter + } noVotes={state ? state.noVotes : data.proposal.noVotes} type={ state ? getProposalTypeLabel(state.type) : getProposalTypeLabel(data.proposal.type) } - url={state ? state.url : data.proposal.url} + // TODO: To decide if we want to keep it when metadate BE is ready + // url={state ? state.url : data.proposal.url} yesVotes={state ? state.yesVotes : data.proposal.yesVotes} voteFromEP={data?.vote?.vote} - shortenedGovActionId={shortenedGovActionId} + govActionId={fullProposalId} + isInProgress={ + pendingTransaction.vote?.resourceId === + fullProposalId.replace("#", "") + } + isDashboard /> ) : ( diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx index d865e5e8d..f9b22bc1e 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActions.tsx @@ -2,25 +2,25 @@ import { useState, useCallback, 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 { GOVERNANCE_ACTIONS_FILTERS } from "@consts"; +import { useCardano } from "@context"; import { useGetProposalsQuery, useGetVoterInfo, useScreenDimension, useTranslation, -} from '@hooks'; -import { DataActionsBar } from '@molecules'; +} from "@hooks"; +import { DataActionsBar } from "@molecules"; import { GovernanceActionsToVote, DashboardGovernanceActionsVotedOn, -} from '@organisms'; +} from "@organisms"; -interface TabPanelProps { +type TabPanelProps = { children?: React.ReactNode; index: number; value: number; -} +}; const defaultCategories = GOVERNANCE_ACTIONS_FILTERS.map( (category) => category.key, @@ -36,8 +36,8 @@ const CustomTabPanel = (props: TabPanelProps) => { id={`simple-tabpanel-${index}`} aria-labelledby={`simple-tab-${index}`} style={{ - display: 'flex', - flexDirection: 'column', + display: "flex", + flexDirection: "column", flex: value !== index ? 0 : 1, }} > @@ -53,22 +53,22 @@ type StyledTabProps = { const StyledTab = styled((props: StyledTabProps) => ( ))(() => ({ - textTransform: 'none', + textTransform: "none", fontWeight: 400, fontSize: 16, - color: '#242232', - '&.Mui-selected': { - color: '#FF640A', + color: "#242232", + "&.Mui-selected": { + color: "#FF640A", fontWeight: 500, }, })); export const DashboardGovernanceActions = () => { - const [searchText, setSearchText] = useState(''); + const [searchText, setSearchText] = useState(""); const [filtersOpen, setFiltersOpen] = useState(false); const [chosenFilters, setChosenFilters] = useState([]); const [sortOpen, setSortOpen] = useState(false); - const [chosenSorting, setChosenSorting] = useState(''); + const [chosenSorting, setChosenSorting] = useState(""); const { voter } = useGetVoterInfo(); const { isMobile } = useScreenDimension(); const { t } = useTranslation(); @@ -145,7 +145,7 @@ export const DashboardGovernanceActions = () => { { > diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx index e2b024610..5f8758cd2 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActionsVotedOn.tsx @@ -1,22 +1,22 @@ -import { useMemo } from 'react'; -import { Box, Typography, CircularProgress } from '@mui/material'; +import { useMemo } from "react"; +import { Box, Typography, CircularProgress } from "@mui/material"; -import { GovernanceVotedOnCard } from '@molecules'; +import { GovernanceVotedOnCard } from "@molecules"; import { useGetDRepVotesQuery, useScreenDimension, useTranslation, -} from '@hooks'; -import { Slider } from '.'; -import { getProposalTypeLabel } from '@/utils/getProposalTypeLabel'; -import { getFullGovActionId } from '@/utils'; -import { useCardano } from '@/context'; +} from "@hooks"; +import { Slider } from "@organisms"; +import { getProposalTypeLabel } from "@/utils/getProposalTypeLabel"; +import { getFullGovActionId } from "@/utils"; +import { useCardano } from "@/context"; -interface DashboardGovernanceActionsVotedOnProps { +type DashboardGovernanceActionsVotedOnProps = { filters: string[]; searchPhrase?: string; sorting: string; -} +}; export const DashboardGovernanceActionsVotedOn = ({ filters, @@ -52,11 +52,11 @@ export const DashboardGovernanceActionsVotedOn = ({ <> {!data.length ? ( - {t('govActions.youHaventVotedYet')} + {t("govActions.youHaventVotedYet")} ) : !filteredData?.length ? ( - {t('govActions.noResultsForTheSearch')} + {t("govActions.noResultsForTheSearch")} ) : ( <> @@ -64,10 +64,11 @@ export const DashboardGovernanceActionsVotedOn = ({

(
{ - const { screenWidth } = useScreenDimension(); - const { openModal } = useModal(); - const { t } = useTranslation(); + const [isVoteSubmitted, setIsVoteSubmitted] = useState(false); + const { screenWidth, isMobile } = useScreenDimension(); + + const isOneColumn = (isDashboard && screenWidth < 1036) ?? isMobile; return ( - - - - - {t("govActions.submissionDate")} - - - {createdDate} - - - - - - - - {t("govActions.expiryDate")} - - - {expiryDate} - - - - - - - - - - {t("govActions.governanceActionType")} - - - - {type} - - - - - - {t("govActions.governanceActionId")} - - - - - {shortenedGovActionId} - - - - - - - {t("govActions.details")} - - - - {JSON.stringify(details, null, 1)} - - - - - - - - {isDRep ? ( - - ) : ( - - )} - + {(isVoteSubmitted || isInProgress) && ( + + )} + + ); }; diff --git a/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx new file mode 100644 index 000000000..9a6641d41 --- /dev/null +++ b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx @@ -0,0 +1,131 @@ +import { Box } from "@mui/material"; + +import { ExternalModalButton } from "@atoms"; +import { + GovernanceActionCardElement, + GovernanceActionDetailsCardLinks, + DataMissingInfoBox, + GovernanceActionDetailsCardHeader, + GovernanceActionsDatesBox, + GovernanceActionDetailsCardOnChainData, +} from "@molecules"; +import { useScreenDimension, useTranslation } from "@hooks"; +import { getProposalTypeNoEmptySpaces } from "@utils"; + +const mockedLongDescription = + "I am the Cardano crusader carving his path in the blockchain battleground. With a mind sharper than a Ledger Nano X, this fearless crypto connoisseur fearlessly navigates the volatile seas of Cardano, turning code into currency. Armed with a keyboard and a heart pumping with blockchain beats, Mister Big Bad fearlessly champions decentralization, smart contracts, and the Cardano community. His Twitter feed is a mix of market analysis that rivals CNBC and memes that could break the internet."; + +const mockedOnChainData = [ + { + label: "Reward Address", + content: "Lorem ipsum dolor sit amet consectetur.", + }, + { label: "Amount", content: "₳ 12,350" }, +]; + +type GovernanceActionDetailsCardDataProps = { + type: string; + govActionId: string; + createdDate: string; + expiryDate: string; + isDataMissing: boolean; + isOneColumn: boolean; + isDashboard?: boolean; +}; + +export const GovernanceActionDetailsCardData = ({ + type, + govActionId, + createdDate, + expiryDate, + isDataMissing, + isOneColumn, + isDashboard, +}: GovernanceActionDetailsCardDataProps) => { + const { t } = useTranslation(); + const { screenWidth } = useScreenDimension(); + + const isModifiedPadding = + (isDashboard && screenWidth < 1168) ?? screenWidth < 900; + + return ( + + + {isDataMissing && } + + + {isDataMissing && ( + + )} + + + + + + + {/* TODO: To add option display of on-chain data when BE is ready */} + + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx b/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx index 41eaa2bb2..b9ce93be9 100644 --- a/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx +++ b/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx @@ -1,14 +1,13 @@ -/* eslint-disable no-unsafe-optional-chaining */ import { useNavigate, generatePath } from "react-router-dom"; import { Box } from "@mui/material"; -import { Typography } from '@atoms'; -import { PATHS } from '@consts'; -import { useCardano } from '@context'; -import { useScreenDimension, useTranslation } from '@hooks'; -import { GovernanceActionCard } from '@molecules'; -import { getProposalTypeLabel, getFullGovActionId, openInNewTab } from '@utils'; -import { Slider } from './Slider'; +import { Typography } from "@atoms"; +import { PATHS } from "@consts"; +import { useCardano } from "@context"; +import { useScreenDimension, useTranslation } from "@hooks"; +import { GovernanceActionCard } from "@molecules"; +import { getProposalTypeLabel, getFullGovActionId, openInNewTab } from "@utils"; +import { Slider } from "@organisms"; type GovernanceActionsToVoteProps = { filters: string[]; @@ -34,7 +33,7 @@ export const GovernanceActionsToVote = ({ <> {!proposals.length ? ( - {t('govActions.noResultsForTheSearch')} + {t("govActions.noResultsForTheSearch")} ) : ( <> @@ -46,8 +45,8 @@ export const GovernanceActionsToVote = ({ className="keen-slider__slide" key={action.id} style={{ - overflow: 'visible', - width: 'auto', + overflow: "visible", + width: "auto", }} > - onDashboard && + // TODO: Add data validation + isDataMissing={false} + onClick={() => { + if ( + onDashboard && pendingTransaction.vote?.resourceId === - action?.txHash + action?.index - ? openInNewTab( + `${action.txHash ?? ""}${action.index ?? ""}` + ) { + openInNewTab( "https://adanordic.com/latest_transactions", - ) - : navigate( + ); + } else { + navigate( onDashboard ? generatePath( - PATHS.dashboardGovernanceActionsAction, - { - proposalId: getFullGovActionId( + PATHS.dashboardGovernanceActionsAction, + { + proposalId: getFullGovActionId( + action.txHash, + action.index, + ), + }, + ) + : PATHS.governanceActionsAction.replace( + ":proposalId", + getFullGovActionId( action.txHash, action.index, ), - }, - ) - : PATHS.governanceActionsAction.replace( - ":proposalId", - getFullGovActionId( - action.txHash, - action.index, ), - ), { state: { ...action }, }, - ) - } + ); + } + }} />
))} diff --git a/govtool/frontend/src/components/organisms/Slider.tsx b/govtool/frontend/src/components/organisms/Slider.tsx index 577ca56a6..6be504364 100644 --- a/govtool/frontend/src/components/organisms/Slider.tsx +++ b/govtool/frontend/src/components/organisms/Slider.tsx @@ -1,15 +1,15 @@ -import { useCallback, useEffect, useMemo } from "react"; -import { Box, Link, Typography } from "@mui/material"; +import { useEffect, useMemo, useState } from "react"; +import { generatePath, useNavigate } from "react-router-dom"; +import { Box } from "@mui/material"; import { KeenSliderOptions } from "keen-slider"; import "keen-slider/keen-slider.min.css"; -import { ICONS, PATHS } from "@consts"; import { useCardano } from "@context"; -import { useScreenDimension, useSlider, useTranslation } from "@hooks"; -import { generatePath, useNavigate } from "react-router-dom"; -import styles from "./slider.module.css"; - -const SLIDER_MAX_LENGTH = 1000; +import { useScreenDimension, useTranslation, useSlider } from "@hooks"; +import { Button, Typography } from "@atoms"; +import { SliderArrows } from "@molecules"; +import { PATHS } from "@consts"; +import { theme } from "@/theme"; type SliderProps = { title: string; @@ -36,99 +36,121 @@ export const Slider = ({ searchPhrase, sorting, }: SliderProps) => { - const { isMobile, screenWidth, pagePadding } = useScreenDimension(); + const [isSliderInitialized, setIsSliderInitialized] = useState(false); + + const { isMobile, screenWidth } = useScreenDimension(); const navigate = useNavigate(); const { pendingTransaction } = useCardano(); const { t } = useTranslation(); + const { + palette: { primaryBlue, arcticWhite, lightBlue }, + } = theme; + const DEFAULT_SLIDER_CONFIG = { mode: "free", initial: 0, slides: { perView: "auto", - spacing: 24, + spacing: 20, }, } as KeenSliderOptions; - const { - currentRange, - sliderRef, - setPercentageValue, - instanceRef, - setCurrentRange, - } = useSlider({ + const isShowArrows = useMemo( + () => + // Arrows are to be show only on desktop view. + // 268 - side menu width; 40 - distance needed from the left on + // disconnected wallet (no side menu); 350 - gov action card width; + // other values are for paddings and margins + screenWidth < + (onDashboard ? 268 : 40) + 28 + dataLength * 350 + 20 * dataLength - 5, + [screenWidth, dataLength], + ); + + const { sliderRef, instanceRef, currentSlide, itemsPerView } = useSlider({ config: DEFAULT_SLIDER_CONFIG, - sliderMaxLength: SLIDER_MAX_LENGTH, }); - const paddingOffset = useMemo(() => { - const padding = onDashboard ? (isMobile ? 2 : 3.5) : pagePadding; - return padding * 8 * 2; - }, [isMobile, pagePadding]); - const refresh = () => { instanceRef.current?.update(instanceRef.current?.options); - setCurrentRange(0); instanceRef.current?.track.to(0); instanceRef.current?.moveToIdx(0); }; useEffect(() => { - refresh(); - }, [filters, sorting, searchPhrase, pendingTransaction.vote?.resourceId, data]); - - const rangeSliderCalculationElement = - dataLength < notSlicedDataLength - ? (screenWidth + - (onDashboard ? -290 - paddingOffset : -paddingOffset + 250)) / - 437 - : (screenWidth + (onDashboard ? -280 - paddingOffset : -paddingOffset)) / - 402; - - const handleLinkPress = useCallback(() => { - if (onDashboard) { - navigate( - generatePath(PATHS.dashboardGovernanceActionsCategory, { - category: navigateKey, - }), - ); - } else { - navigate( - generatePath(PATHS.governanceActionsCategory, { - category: navigateKey, - }), - ); + if (instanceRef.current) { + setIsSliderInitialized(true); } - }, [onDashboard]); + }, [instanceRef.current]); + + useEffect(() => { + refresh(); + }, [ + filters, + sorting, + searchPhrase, + pendingTransaction.vote?.resourceId, + data, + ]); return ( - - - {title} - - {isMobile && isShowAll && ( - - + + {title} + {(notSlicedDataLength > 6 || (isMobile && isShowAll)) && ( + + )} + + {isSliderInitialized && isShowArrows && dataLength > 1 && !isMobile && ( + )}
{data} - {!isMobile && isShowAll && dataLength < notSlicedDataLength && ( -
- - - {t("slider.viewAll")} - - arrow - -
- )}
- {!isMobile && Math.floor(rangeSliderCalculationElement) < dataLength && ( - - - - )}
); }; diff --git a/govtool/frontend/src/components/organisms/index.ts b/govtool/frontend/src/components/organisms/index.ts index bbded8e90..cccbc8400 100644 --- a/govtool/frontend/src/components/organisms/index.ts +++ b/govtool/frontend/src/components/organisms/index.ts @@ -17,6 +17,7 @@ export * from "./DrawerMobile"; export * from "./ExternalLinkModal"; export * from "./Footer"; export * from "./GovernanceActionDetailsCard"; +export * from "./GovernanceActionDetailsCardData"; export * from "./GovernanceActionsToVote"; export * from "./Hero"; export * from "./HomeCards"; diff --git a/govtool/frontend/src/consts/icons.ts b/govtool/frontend/src/consts/icons.ts index 2a5fc394d..dfabf260d 100644 --- a/govtool/frontend/src/consts/icons.ts +++ b/govtool/frontend/src/consts/icons.ts @@ -8,6 +8,7 @@ export const ICONS = { closeIcon: "/icons/Close.svg", closeWhiteIcon: "/icons/CloseWhite.svg", copyBlueIcon: "/icons/CopyBlue.svg", + copyBlueThinIcon: "/icons/CopyBlueThin.svg", copyIcon: "/icons/Copy.svg", copyWhiteIcon: "/icons/CopyWhite.svg", dashboardActiveIcon: "/icons/DashboardActive.svg", @@ -25,6 +26,7 @@ export const ICONS = { guidesIcon: "/icons/Guides.svg", helpIcon: "/icons/Help.svg", link: "/icons/Link.svg", + share: "/icons/Share.svg", sortActiveIcon: "/icons/SortActive.svg", sortIcon: "/icons/Sort.svg", sortWhiteIcon: "/icons/SortWhite.svg", diff --git a/govtool/frontend/src/context/wallet.tsx b/govtool/frontend/src/context/wallet.tsx index a6eb929e9..c60b6cbaf 100644 --- a/govtool/frontend/src/context/wallet.tsx +++ b/govtool/frontend/src/context/wallet.tsx @@ -4,7 +4,7 @@ import { useContext, useMemo, useState, -} from 'react'; +} from "react"; import { Address, BigNum, @@ -40,16 +40,16 @@ import { GovernanceAction, TreasuryWithdrawals, TreasuryWithdrawalsAction, -} from '@emurgo/cardano-serialization-lib-asmjs'; -import { Buffer } from 'buffer'; -import { useNavigate } from 'react-router-dom'; -import { Link } from '@mui/material'; -import * as Sentry from '@sentry/react'; -import { Trans } from 'react-i18next'; - -import { PATHS } from '@consts'; -import { CardanoApiWallet, Protocol } from '@models'; -import type { StatusModalState } from '@organisms'; +} from "@emurgo/cardano-serialization-lib-asmjs"; +import { Buffer } from "buffer"; +import { useNavigate } from "react-router-dom"; +import { Link } from "@mui/material"; +import * as Sentry from "@sentry/react"; +import { Trans } from "react-i18next"; + +import { PATHS } from "@consts"; +import { CardanoApiWallet, Protocol } from "@models"; +import type { StatusModalState } from "@organisms"; import { checkIsMaintenanceOn, generateAnchor, @@ -61,17 +61,17 @@ import { SANCHO_INFO_KEY, setItemToLocalStorage, WALLET_LS_KEY, -} from '@utils'; -import { getEpochParams } from '@services'; -import { useTranslation } from '@hooks'; -import { getUtxos } from './getUtxos'; -import { useModal, useSnackbar } from '.'; +} from "@utils"; +import { getEpochParams } from "@services"; +import { useTranslation } from "@hooks"; +import { getUtxos } from "./getUtxos"; +import { useModal, useSnackbar } from "."; import { PendingTransaction, TransactionStateWithResource, TransactionStateWithoutResource, usePendingTransaction, -} from './pendingTransaction'; +} from "./pendingTransaction"; interface Props { children: React.ReactNode; @@ -178,9 +178,9 @@ const CardanoProvider = (props: Props) => { undefined, ); const [address, setAddress] = useState(undefined); - const [pubDRepKey, setPubDRepKey] = useState(''); - const [dRepID, setDRepID] = useState(''); - const [dRepIDBech32, setDRepIDBech32] = useState(''); + const [pubDRepKey, setPubDRepKey] = useState(""); + const [dRepID, setDRepID] = useState(""); + const [dRepIDBech32, setDRepIDBech32] = useState(""); const [stakeKey, setStakeKey] = useState(undefined); const [stakeKeys, setStakeKeys] = useState([]); const [isMainnet, setIsMainnet] = useState(false); @@ -204,7 +204,7 @@ const CardanoProvider = (props: Props) => { try { const raw = await enabledApi.getChangeAddress(); const changeAddress = Address.from_bytes( - Buffer.from(raw, 'hex'), + Buffer.from(raw, "hex"), ).to_bech32(); setWalletState((prev) => ({ ...prev, changeAddress })); } catch (err) { @@ -218,7 +218,7 @@ const CardanoProvider = (props: Props) => { const raw = await enabledApi.getUsedAddresses(); const rawFirst = raw[0]; const usedAddress = Address.from_bytes( - Buffer.from(rawFirst, 'hex'), + Buffer.from(rawFirst, "hex"), ).to_bech32(); setWalletState((prev) => ({ ...prev, usedAddress })); } catch (err) { @@ -237,13 +237,13 @@ const CardanoProvider = (props: Props) => { try { // Check that this wallet supports CIP-95 connection if (!window.cardano[walletName].supportedExtensions) { - throw new Error(t('errors.walletNoCIP30Support')); + throw new Error(t("errors.walletNoCIP30Support")); } else if ( !window.cardano[walletName].supportedExtensions.some( (item) => item.cip === 95, ) ) { - throw new Error(t('errors.walletNoCIP30Nor90Support')); + throw new Error(t("errors.walletNoCIP30Nor90Support")); } // Enable wallet connection const enabledApi: CardanoApiWallet = await window.cardano[walletName] @@ -261,7 +261,7 @@ const CardanoProvider = (props: Props) => { // Check if wallet has enabled the CIP-95 extension const enabledExtensions = await enabledApi.getExtensions(); if (!enabledExtensions.some((item) => item.cip === 95)) { - throw new Error(t('errors.walletNoCIP90FunctionsEnabled')); + throw new Error(t("errors.walletNoCIP90FunctionsEnabled")); } const network = await enabledApi.getNetworkId(); if (network !== NETWORK) { @@ -277,7 +277,7 @@ const CardanoProvider = (props: Props) => { const usedAddresses = await enabledApi.getUsedAddresses(); const unusedAddresses = await enabledApi.getUnusedAddresses(); if (!usedAddresses.length && !unusedAddresses.length) { - throw new Error(t('errors.noAddressesFound')); + throw new Error(t("errors.noAddressesFound")); } if (!usedAddresses.length) { setAddress(unusedAddresses[0]); @@ -341,22 +341,22 @@ const CardanoProvider = (props: Props) => { stakeKeySet = true; } const dRepIDs = await getPubDRepID(enabledApi); - setPubDRepKey(dRepIDs?.dRepKey || ''); - setDRepID(dRepIDs?.dRepID || ''); - setDRepIDBech32(dRepIDs?.dRepIDBech32 || ''); + setPubDRepKey(dRepIDs?.dRepKey || ""); + setDRepID(dRepIDs?.dRepID || ""); + setDRepIDBech32(dRepIDs?.dRepIDBech32 || ""); setItemToLocalStorage(`${WALLET_LS_KEY}_name`, walletName); const protocol = await getEpochParams(); setItemToLocalStorage(PROTOCOL_PARAMS_KEY, protocol); - return { status: t('ok'), stakeKey: stakeKeySet }; + return { status: t("ok"), stakeKey: stakeKeySet }; } catch (e) { Sentry.captureException(e); console.error(e); setError(`${e}`); setAddress(undefined); setWalletApi(undefined); - setPubDRepKey(''); + setPubDRepKey(""); setStakeKey(undefined); setIsEnabled(false); // eslint-disable-next-line no-throw-literal @@ -439,7 +439,7 @@ const CardanoProvider = (props: Props) => { const txBuilder = await initTransactionBuilder(); if (!txBuilder) { - throw new Error(t('errors.appCannotCreateTransaction')); + throw new Error(t("errors.appCannotCreateTransaction")); } if (certBuilder) { @@ -468,10 +468,10 @@ const CardanoProvider = (props: Props) => { ); // Add output of 1 ADA to the address of our wallet - let outputValue = BigNum.from_str('1000000'); + let outputValue = BigNum.from_str("1000000"); if ( - (type === 'retireAsDrep' || type === 'retireAsSoleVoter') && + (type === "retireAsDrep" || type === "retireAsSoleVoter") && voterDeposit ) { outputValue = outputValue.checked_add(BigNum.from_str(voterDeposit)); @@ -484,7 +484,7 @@ const CardanoProvider = (props: Props) => { const utxos = await getUtxos(walletApi); if (!utxos) { - throw new Error(t('errors.appCannotGetUtxos')); + throw new Error(t("errors.appCannotGetUtxos")); } // Find the available UTXOs in the wallet and use them as Inputs for the transaction const txUnspentOutputs = await getTxUnspentOutputs(utxos); @@ -511,11 +511,11 @@ const CardanoProvider = (props: Props) => { // Create witness set object using the witnesses provided by the wallet txVkeyWitnesses = TransactionWitnessSet.from_bytes( - Buffer.from(txVkeyWitnesses, 'hex'), + Buffer.from(txVkeyWitnesses, "hex"), ); const vkeys = txVkeyWitnesses.vkeys(); - if (!vkeys) throw new Error(t('errors.appCannotGetVkeys')); + if (!vkeys) throw new Error(t("errors.appCannotGetVkeys")); transactionWitnessSet.set_vkeys(vkeys); // Build transaction with witnesses @@ -546,7 +546,7 @@ const CardanoProvider = (props: Props) => { } Sentry.captureException(error); - console.error(error, 'error'); + console.error(error, "error"); throw error?.info ?? error; } }, @@ -560,7 +560,7 @@ const CardanoProvider = (props: Props) => { const certBuilder = CertificatesBuilder.new(); let stakeCred; if (!stakeKey) { - throw new Error(t('errors.noStakeKeySelected')); + throw new Error(t("errors.noStakeKeySelected")); } // Remove network tag from stake key hash const stakeKeyHash = Ed25519KeyHash.from_hex(stakeKey.substring(2)); @@ -575,11 +575,11 @@ const CardanoProvider = (props: Props) => { // Create correct DRep let targetDRep; - if (target === 'abstain') { + if (target === "abstain") { targetDRep = DRep.new_always_abstain(); - } else if (target === 'no confidence') { + } else if (target === "no confidence") { targetDRep = DRep.new_always_no_confidence(); - } else if (target.includes('drep')) { + } else if (target.includes("drep")) { targetDRep = DRep.new_key_hash(Ed25519KeyHash.from_bech32(target)); } else { targetDRep = DRep.new_key_hash(Ed25519KeyHash.from_hex(target)); @@ -725,9 +725,9 @@ const CardanoProvider = (props: Props) => { ); let votingChoice; - if (voteChoice === 'yes') { + if (voteChoice === "yes") { votingChoice = 1; - } else if (voteChoice === 'no') { + } else if (voteChoice === "no") { votingChoice = 0; } else { votingChoice = 2; @@ -761,11 +761,11 @@ const CardanoProvider = (props: Props) => { const getRewardAddress = useCallback(async () => { const addresses = await walletApi?.getRewardAddresses(); if (!addresses) { - throw new Error('Can not get reward addresses from wallet.'); + throw new Error("Can not get reward addresses from wallet."); } const firstAddress = addresses[0]; const bech32Address = Address.from_bytes( - Buffer.from(firstAddress, 'hex'), + Buffer.from(firstAddress, "hex"), ).to_bech32(); return RewardAddress.from_address(Address.from_bech32(bech32Address)); @@ -783,7 +783,7 @@ const CardanoProvider = (props: Props) => { const anchor = generateAnchor(url, hash); const rewardAddr = await getRewardAddress(); - if (!rewardAddr) throw new Error('Can not get reward address'); + if (!rewardAddr) throw new Error("Can not get reward address"); // Create voting proposal const votingProposal = VotingProposal.new( @@ -811,7 +811,7 @@ const CardanoProvider = (props: Props) => { Address.from_bech32(receivingAddress), ); - if (!treasuryTarget) throw new Error('Can not get tresasury target'); + if (!treasuryTarget) throw new Error("Can not get tresasury target"); const myWithdrawal = BigNum.from_str(amount); const withdrawals = TreasuryWithdrawals.new(); @@ -825,7 +825,7 @@ const CardanoProvider = (props: Props) => { const rewardAddr = await getRewardAddress(); - if (!rewardAddr) throw new Error('Can not get reward address'); + if (!rewardAddr) throw new Error("Can not get reward address"); // Create voting proposal const votingProposal = VotingProposal.new( treasuryGovAct, @@ -909,7 +909,7 @@ function useCardano() { const { t } = useTranslation(); if (context === undefined) { - throw new Error(t('errors.useCardano')); + throw new Error(t("errors.useCardano")); } const enable = useCallback( @@ -922,22 +922,22 @@ function useCardano() { if (!result.error) { closeModal(); if (result.stakeKey) { - addSuccessAlert(t('alerts.walletConnected'), 3000); + addSuccessAlert(t("alerts.walletConnected"), 3000); } if (!isSanchoInfoShown) { openModal({ - type: 'statusModal', + type: "statusModal", state: { - status: 'info', - dataTestId: 'info-about-sancho-net-modal', + status: "info", + dataTestId: "info-about-sancho-net-modal", message: (

- {t('system.sanchoNetIsBeta')} + {t("system.sanchoNetIsBeta")} openInNewTab('https://sancho.network/')} - sx={{ cursor: 'pointer' }} + onClick={() => openInNewTab("https://sancho.network/")} + sx={{ cursor: "pointer" }} > - {t('system.sanchoNet')} + {t("system.sanchoNet")} .
@@ -950,8 +950,8 @@ function useCardano() { />

), - title: t('system.toolConnectedToSanchonet'), - buttonText: t('ok'), + title: t("system.toolConnectedToSanchonet"), + buttonText: t("ok"), }, }); setItemToLocalStorage(`${SANCHO_INFO_KEY}_${walletName}`, true); @@ -965,15 +965,15 @@ function useCardano() { await context.disconnectWallet(); navigate(PATHS.home); openModal({ - type: 'statusModal', + type: "statusModal", state: { - status: 'warning', - message: e?.error?.replace('Error: ', ''), + status: "warning", + message: e?.error?.replace("Error: ", ""), onSubmit: () => { closeModal(); }, - title: t('modals.common.oops'), - dataTestId: 'wallet-connection-error-modal', + title: t("modals.common.oops"), + dataTestId: "wallet-connection-error-modal", }, }); throw e; diff --git a/govtool/frontend/src/hooks/index.ts b/govtool/frontend/src/hooks/index.ts index 84e427cfa..126c2d0c9 100644 --- a/govtool/frontend/src/hooks/index.ts +++ b/govtool/frontend/src/hooks/index.ts @@ -1,8 +1,9 @@ export { useTranslation } from "react-i18next"; +export * from "./useFetchNextPageDetector"; +export * from "./useOutsideClick"; +export * from "./useSaveScrollPosition"; export * from "./useScreenDimension"; export * from "./useSlider"; -export * from "./useSaveScrollPosition"; -export * from "./useFetchNextPageDetector"; export * from "./useWalletConnectionListener"; export * from "./forms"; diff --git a/govtool/frontend/src/hooks/useOutsideClick.tsx b/govtool/frontend/src/hooks/useOutsideClick.tsx new file mode 100644 index 000000000..619086632 --- /dev/null +++ b/govtool/frontend/src/hooks/useOutsideClick.tsx @@ -0,0 +1,25 @@ +import { MutableRefObject, useEffect } from "react"; + +export function useOnClickOutside( + ref: MutableRefObject, + handler: (event: MouseEvent | TouchEvent) => void, +) { + useEffect(() => { + const listener = (event: MouseEvent | TouchEvent) => { + const target = event.target as Node; + + if (!ref.current || ref.current.contains(target)) { + return; + } + handler(event); + }; + + document.addEventListener("mousedown", listener as EventListener); + document.addEventListener("touchstart", listener as EventListener); + + return () => { + document.removeEventListener("mousedown", listener as EventListener); + document.removeEventListener("touchstart", listener as EventListener); + }; + }, [ref, handler]); +} diff --git a/govtool/frontend/src/hooks/useSlider.ts b/govtool/frontend/src/hooks/useSlider.ts index 78788791e..b2f64adf5 100644 --- a/govtool/frontend/src/hooks/useSlider.ts +++ b/govtool/frontend/src/hooks/useSlider.ts @@ -1,4 +1,4 @@ -import { ChangeEvent, useState } from "react"; +import { useState } from "react"; import { KeenSliderOptions, useKeenSlider } from "keen-slider/react"; import type { KeenSliderInstance } from "keen-slider"; @@ -47,53 +47,28 @@ const WheelControls = (slider: KeenSliderInstance) => { }); }; -export const useSlider = ({ - config, - sliderMaxLength, -}: { - config: KeenSliderOptions; - sliderMaxLength: number; -}) => { +export const useSlider = ({ config }: { config: KeenSliderOptions }) => { const [currentSlide, setCurrentSlide] = useState(0); - const [currentRange, setCurrentRange] = useState(0); const [sliderRef, instanceRef] = useKeenSlider( { ...config, rubberband: false, detailsChanged: (slider) => { - setCurrentRange(slider.track.details.progress * sliderMaxLength); setCurrentSlide(slider.track.details.rel); }, }, [WheelControls], ); - const DATA_LENGTH = instanceRef?.current?.slides?.length ?? 10; - const ITEMS_PER_VIEW = - DATA_LENGTH - (instanceRef?.current?.track?.details?.maxIdx ?? 2); - - const setPercentageValue = (e: ChangeEvent) => { - const target = e?.target; - const currentIndexOfSlide = Math.floor( - +(target?.value ?? 0) / - (sliderMaxLength / (DATA_LENGTH - Math.floor(ITEMS_PER_VIEW))), - ); - - instanceRef.current?.track.add( - (+(target?.value ?? 0) - currentRange) * - (instanceRef.current.track.details.length / sliderMaxLength), - ); - setCurrentRange(+(target?.value ?? 0)); - setCurrentSlide(currentIndexOfSlide); - }; + const dataLength = instanceRef?.current?.slides?.length ?? 10; + const itemsPerView = + dataLength - (instanceRef?.current?.track?.details?.maxIdx ?? 2); return { sliderRef, instanceRef, currentSlide, - currentRange, - setCurrentRange, - setPercentageValue, + itemsPerView, }; }; diff --git a/govtool/frontend/src/i18n/locales/en.ts b/govtool/frontend/src/i18n/locales/en.ts index f70098f2a..19e49e5f0 100644 --- a/govtool/frontend/src/i18n/locales/en.ts +++ b/govtool/frontend/src/i18n/locales/en.ts @@ -314,28 +314,46 @@ export const en = { }, }, govActions: { + about: "About", + abstract: "Abstract:", + backToGovActions: "Back to Governance Actions", + castVote: "<0>You voted {{vote}} for this proposal\nat {{date}}", + castVoteDeadline: + "You can change your vote up to the deadline of {{date}}", changeVote: "Change vote", changeYourVote: "Change your vote", chooseHowToVote: "Choose how you want to vote:", + dataMissing: "Data Missing", + dataMissingTooltipExplanation: + "Please click “View Details” for more information.", details: "Governance Details:", + expiresDateWithEpoch: "Expires: <0>{{date}} <1>(Epoch {{epoch}})", expiryDate: "Expiry date:", filterTitle: "Governance Action Type", forGovAction: "for this Governance Action", governanceActionId: "Governance Action ID:", governanceActionType: "Governance Action Type:", + motivation: "Motivation", myVote: "My Vote:", noResultsForTheSearch: "No results for the search.", + onChainTransactionDetails: "On-chain Transaction Details", optional: "(optional)", provideContext: "Provide context about your vote", + rationale: "Rationale", + seeExternalData: "See external data", selectDifferentOption: "Select a different option to change your vote", showVotes: "Show votes", submissionDate: "Submission date:", + submittedDateWithEpoch: + "Submitted: <0>{{date}} <1>(Epoch {{epoch}})", + supportingLinks: "Supporting links", title: "Governance Actions", toVote: "To vote", + viewDetails: "View Details", viewOtherDetails: "View other details", viewProposalDetails: "View proposal details", vote: "Vote", - voted: "Voted", + votedOnByMe: "Voted on by me", voteOnGovActions: "Vote on Governance Action", voteSubmitted: "Vote submitted", voteTransaction: "Vote transaction", @@ -496,7 +514,7 @@ export const en = { }, }, slider: { - showAll: "Show all", + showAll: "Show All", viewAll: "View all", }, soleVoter: { @@ -586,11 +604,13 @@ export const en = { backToList: "Back to the list", cancel: "Cancel", clear: "Clear", + clickToCopyLink: "Click to copy link", confirm: "Confirm", continue: "Continue", delegate: "Delegate", + filter: "Filter", here: "here", - inProgress: "In Progress", + inProgress: "In progress", learnMore: "Learn more", loading: "Loading...", myDRepId: "My DRep ID:", @@ -602,7 +622,10 @@ export const en = { required: "required", seeTransaction: "See transaction", select: "Select", + share: "Share", + showMore: "Show more", skip: "Skip", + sort: "Sort", sortBy: "Sort by", submit: "Submit", thisLink: "this link", diff --git a/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx b/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx index 8685bfbf8..47384da36 100644 --- a/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx +++ b/govtool/frontend/src/pages/DashboardGovernanceActionsCategory.tsx @@ -1,19 +1,8 @@ import { useCallback, useMemo, useRef, useState } from "react"; -import { - generatePath, - NavLink, - useNavigate, - useParams, -} from "react-router-dom"; -import { - Box, - Breadcrumbs, - CircularProgress, - Link, - Typography, -} from "@mui/material"; +import { generatePath, useNavigate, useParams } from "react-router-dom"; +import { Box, CircularProgress, Link } from "@mui/material"; -import { Background } from "@atoms"; +import { Background, Typography } from "@atoms"; import { ICONS, PATHS } from "@consts"; import { useCardano } from "@context"; import { DataActionsBar, GovernanceActionCard } from "@molecules"; @@ -68,21 +57,6 @@ export const DashboardGovernanceActionsCategory = () => { isProposalsFetching, ); - const breadcrumbs = [ - - - {t("govActions.title")} - - , - - {getProposalTypeLabel(category ?? "")} - , - ]; - const mappedData = useMemo(() => { const uniqueProposals = removeDuplicatedProposals(proposals); @@ -112,23 +86,13 @@ export const DashboardGovernanceActionsCategory = () => { > - - {breadcrumbs} - navigate(PATHS.dashboardGovernanceActions)} > @@ -137,8 +101,8 @@ export const DashboardGovernanceActionsCategory = () => { alt="arrow" style={{ marginRight: "12px", transform: "rotate(180deg)" }} /> - - {t("backToList")} + + {t("govActions.backToGovActions")} { sortingActive={Boolean(chosenSorting)} sortOpen={sortOpen} /> - + + {getProposalTypeLabel(category ?? "")} + {!mappedData || isEnableLoading || isProposalsLoading ? ( ) : !mappedData?.length ? ( - + {t("govActions.withCategoryNotExist.partOne")}   - {` ${category} `} + + {` ${category} `} +   {t("govActions.withCategoryNotExist.partTwo")} @@ -173,14 +155,11 @@ export const DashboardGovernanceActionsCategory = () => { ) : ( {mappedData.map((item) => ( @@ -189,17 +168,22 @@ export const DashboardGovernanceActionsCategory = () => { index={item.index} inProgress={ pendingTransaction.vote?.resourceId === - item.txHash + item.index + `${item.txHash ?? ""}${item.index ?? ""}` } + // TODO: Add data validation + isDataMissing={false} onClick={() => { saveScrollPosition(); - // eslint-disable-next-line no-unused-expressions - pendingTransaction.vote?.resourceId === item.txHash + item.index - ? openInNewTab( + if ( + pendingTransaction.vote?.resourceId === + item.txHash + item.index + ) { + openInNewTab( "https://adanordic.com/latest_transactions", - ) - : navigate( + ); + } else { + navigate( generatePath( PATHS.dashboardGovernanceActionsAction, { @@ -216,6 +200,7 @@ export const DashboardGovernanceActionsCategory = () => { }, }, ); + } }} txHash={item.txHash} /> diff --git a/govtool/frontend/src/pages/GovernanceActionDetails.tsx b/govtool/frontend/src/pages/GovernanceActionDetails.tsx index 34378488d..c914ce978 100644 --- a/govtool/frontend/src/pages/GovernanceActionDetails.tsx +++ b/govtool/frontend/src/pages/GovernanceActionDetails.tsx @@ -3,10 +3,9 @@ import { useNavigate, useLocation, useParams, - NavLink, generatePath, } from "react-router-dom"; -import { Box, Breadcrumbs, CircularProgress, Link } from "@mui/material"; +import { Box, CircularProgress, Link } from "@mui/material"; import { Background, Typography } from "@atoms"; import { ICONS, PATHS } from "@consts"; @@ -24,11 +23,15 @@ import { getItemFromLocalStorage, getShortenedGovActionId, } from "@utils"; +import { Breadcrumbs } from "@molecules"; + +// TODO: Remove when data validation is ready +const isDataMissing = false; export const GovernanceActionDetails = () => { const { state, hash } = useLocation(); const navigate = useNavigate(); - const { pagePadding, screenWidth } = useScreenDimension(); + const { pagePadding, isMobile } = useScreenDimension(); const { isEnabled } = useCardano(); const { t } = useTranslation(); const { proposalId } = useParams(); @@ -48,21 +51,6 @@ export const GovernanceActionDetails = () => { } }, [isEnabled]); - const breadcrumbs = [ - - - {t("govActions.title")} - - , - - {t("govActions.voteOnGovActions")} - , - ]; - return ( { px={pagePadding} > - {screenWidth >= 1024 ? ( - - {t("govActions.title")} - + {isMobile ? ( + + + {t("govActions.title")} + + ) : null} - {breadcrumbs} - + elementOne={t("govActions.title")} + elementOnePath={PATHS.dashboardGovernanceActions} + elementTwo="Fund our project" + isDataMissing={false} + /> { style={{ marginRight: "12px", transform: "rotate(180deg)" }} /> - {t("backToList")} + {t("back")} {isLoading ? ( @@ -130,11 +124,7 @@ export const GovernanceActionDetails = () => { ) : data || state ? ( - + { ? formatDisplayDate(state.createdDate) : formatDisplayDate(data.proposal.createdDate) } - details={state ? state.details : data.proposal.details} + // TODO: Add data validation + isDataMissing={isDataMissing} expiryDate={ state ? formatDisplayDate(state.expiryDate) @@ -156,9 +147,10 @@ export const GovernanceActionDetails = () => { ? getProposalTypeLabel(state.type) : getProposalTypeLabel(data.proposal.type) } - url={state ? state.url : data.proposal.url} + // TODO: To decide if we want to keep it when metadate BE is ready + // url={state ? state.url : data.proposal.url} yesVotes={state ? state.yesVotes : data.proposal.yesVotes} - shortenedGovActionId={shortenedGovActionId} + govActionId={fullProposalId} /> ) : ( diff --git a/govtool/frontend/src/pages/GovernanceActionsCategory.tsx b/govtool/frontend/src/pages/GovernanceActionsCategory.tsx index e29cbe1c3..82b67ce46 100644 --- a/govtool/frontend/src/pages/GovernanceActionsCategory.tsx +++ b/govtool/frontend/src/pages/GovernanceActionsCategory.tsx @@ -1,12 +1,6 @@ import { useState, useCallback, useEffect, useMemo, useRef } from "react"; -import { NavLink, useNavigate, useParams } from "react-router-dom"; -import { - Box, - Breadcrumbs, - CircularProgress, - Divider, - Link, -} from "@mui/material"; +import { useNavigate, useParams } from "react-router-dom"; +import { Box, CircularProgress, Link } from "@mui/material"; import { Background, Typography } from "@atoms"; import { ICONS, PATHS } from "@consts"; @@ -65,21 +59,6 @@ export const GovernanceActionsCategory = () => { isProposalsFetching, ); - const breadcrumbs = [ - - - {t("govActions.title")} - - , - - {getProposalTypeLabel(category ?? "")} - , - ]; - const mappedData = useMemo(() => { const uniqueProposals = removeDuplicatedProposals(proposals); @@ -116,32 +95,7 @@ export const GovernanceActionsCategory = () => { > - - {t("govActions.title")} - - {isMobile && ( - - )} - - {breadcrumbs} - { alt="arrow" style={{ marginRight: "12px", transform: "rotate(180deg)" }} /> - - {t("backToList")} + + {t("govActions.backToGovActions")} { closeSorts={closeSorts} setChosenSorting={setChosenSorting} /> - + + {getProposalTypeLabel(category ?? "")} + {!isProposalsLoading ? ( !mappedData?.length ? ( @@ -202,14 +163,10 @@ export const GovernanceActionsCategory = () => { ) : ( {mappedData.map((item) => ( @@ -218,6 +175,8 @@ export const GovernanceActionsCategory = () => { {...item} txHash={item.txHash} index={item.index} + // TODO: Add data validation + isDataMissing={false} onClick={() => { saveScrollPosition(); diff --git a/govtool/frontend/src/stories/GovernanceAction.stories.ts b/govtool/frontend/src/stories/GovernanceAction.stories.ts index 8ed28776b..770b603c0 100644 --- a/govtool/frontend/src/stories/GovernanceAction.stories.ts +++ b/govtool/frontend/src/stories/GovernanceAction.stories.ts @@ -1,58 +1,61 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { within, userEvent, waitFor, screen } from "@storybook/testing-library"; -import { expect, jest } from "@storybook/jest"; +// Story to be updated when new Gov Actions are finished +/* eslint-disable storybook/default-exports */ -import { formatDisplayDate } from "@utils"; -import { GovernanceActionCard } from "@/components/molecules"; +// import type { Meta, StoryObj } from "@storybook/react"; +// import { within, userEvent, waitFor, screen } from "@storybook/testing-library"; +// import { expect, jest } from "@storybook/jest"; -const meta = { - title: "Example/GovernanceActionCard", - component: GovernanceActionCard, - parameters: { - layout: "centered", - }, - tags: ["autodocs"], -} satisfies Meta; +// import { formatDisplayDate } from "@utils"; +// import { GovernanceActionCard } from "@/components/molecules"; -export default meta; -type Story = StoryObj; +// const meta = { +// title: "Example/GovernanceActionCard", +// component: GovernanceActionCard, +// parameters: { +// layout: "centered", +// }, +// tags: ["autodocs"], +// } satisfies Meta; -export const GovernanceActionCardComponent: Story = { - args: { - createdDate: "1970-01-01T00:00:00Z", - expiryDate: "1970-02-01T00:00:00Z", - type: "exampleType", - txHash: "sad78afdsf7jasd98d", - index: 2, - onClick: jest.fn(), - }, - play: async ({ canvasElement, args }) => { - const canvas = within(canvasElement); - expect(canvas.getByTestId("exampleType-type")).toBeInTheDocument(); - expect(canvas.getByTestId("sad78afdsf7jasd98d#2-id")).toBeInTheDocument(); - expect( - canvas.getByText(formatDisplayDate("1970-01-01T00:00:00Z")), - ).toBeInTheDocument(); - expect( - canvas.getByText(formatDisplayDate("1970-02-01T00:00:00Z")), - ).toBeInTheDocument(); +// export default meta; +// type Story = StoryObj; - const tooltips = canvas.getAllByTestId("InfoOutlinedIcon"); - await userEvent.hover(tooltips[0]); - await waitFor(async () => { - expect(screen.getByRole("tooltip")).toBeInTheDocument(); - expect(screen.getByRole("tooltip")).toHaveTextContent(/Submission Date/i); - await userEvent.unhover(tooltips[0]); - }); - await userEvent.hover(tooltips[1]); - await waitFor(() => { - expect(screen.getByRole("tooltip")).toBeInTheDocument(); - expect(screen.getByRole("tooltip")).toHaveTextContent(/Expiry Date/i); - }); +// export const GovernanceActionCardComponent: Story = { +// args: { +// createdDate: "1970-01-01T00:00:00Z", +// expiryDate: "1970-02-01T00:00:00Z", +// type: "exampleType", +// txHash: "sad78afdsf7jasd98d", +// index: 2, +// onClick: jest.fn(), +// }, +// play: async ({ canvasElement, args }) => { +// const canvas = within(canvasElement); +// expect(canvas.getByTestId("exampleType-type")).toBeInTheDocument(); +// expect(canvas.getByTestId("sad78afdsf7jasd98d#2-id")).toBeInTheDocument(); +// expect( +// canvas.getByText(formatDisplayDate("1970-01-01T00:00:00Z")), +// ).toBeInTheDocument(); +// expect( +// canvas.getByText(formatDisplayDate("1970-02-01T00:00:00Z")), +// ).toBeInTheDocument(); - await userEvent.click( - canvas.getByTestId("govaction-sad78afdsf7jasd98d#2-view-detail"), - ); - await expect(args.onClick).toHaveBeenCalled(); - }, -}; +// const tooltips = canvas.getAllByTestId("InfoOutlinedIcon"); +// await userEvent.hover(tooltips[0]); +// await waitFor(async () => { +// expect(screen.getByRole("tooltip")).toBeInTheDocument(); +// expect(screen.getByRole("tooltip")).toHaveTextContent(/Submission Date/i); +// await userEvent.unhover(tooltips[0]); +// }); +// await userEvent.hover(tooltips[1]); +// await waitFor(() => { +// expect(screen.getByRole("tooltip")).toBeInTheDocument(); +// expect(screen.getByRole("tooltip")).toHaveTextContent(/Expiry Date/i); +// }); + +// await userEvent.click( +// canvas.getByTestId("govaction-sad78afdsf7jasd98d#2-view-detail"), +// ); +// await expect(args.onClick).toHaveBeenCalled(); +// }, +// }; diff --git a/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts b/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts index 8f7d8793d..454db3a52 100644 --- a/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts +++ b/govtool/frontend/src/stories/GovernanceActionDetailsCard.stories.ts @@ -1,62 +1,65 @@ -import type { Meta, StoryObj } from "@storybook/react"; -import { screen, userEvent, waitFor, within } from "@storybook/testing-library"; -import { GovernanceActionDetailsCard } from "@organisms"; -import { expect } from "@storybook/jest"; - -const meta = { - title: "Example/GovernanceActionDetailsCard", - component: GovernanceActionDetailsCard, - parameters: { - layout: "centered", - }, - tags: ["autodocs"], -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const GovernanceActionDetailsCardComponent: Story = { - args: { - abstainVotes: 1000000, - createdDate: new Date().toLocaleDateString(), - expiryDate: new Date().toLocaleDateString(), - shortenedGovActionId: "Example id", - noVotes: 1000000, - type: "Gov Type", - url: "https://exampleurl.com", - yesVotes: 1000000, - details: { - key: "value", - key2: ["key-list", "key-list", "key-list"], - }, - }, - - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const todayDate = new Date().toLocaleDateString(); - await expect(canvas.getAllByText(todayDate)).toHaveLength(2); - - const tooltips = canvas.getAllByTestId("InfoOutlinedIcon"); - await userEvent.hover(tooltips[0]); - await waitFor(async () => { - await expect(screen.getByRole("tooltip")).toBeInTheDocument(); - await expect(screen.getByRole("tooltip")).toHaveTextContent( - /Submission Date/i, - ); - await userEvent.unhover(tooltips[0]); - }); - await userEvent.hover(tooltips[1]); - - await expect(canvas.getByText("Gov Type")).toBeInTheDocument(); - await expect(canvas.getByText("Example id")).toBeInTheDocument(); - - await expect(canvas.getByText("key: value")).toBeInTheDocument(); - await expect(canvas.getAllByText("key-list")).toHaveLength(3); - - await expect(canvas.getByText(/Yes/i)).toBeInTheDocument(); - await expect(canvas.getByText(/Abstain/i)).toBeInTheDocument(); - await expect(canvas.getByText(/No/i)).toBeInTheDocument(); - - await userEvent.click(canvas.getByTestId("view-other-details-button")); - }, -}; +// Story to be updated when new Gov Actions are finished +/* eslint-disable storybook/default-exports */ + +// import type { Meta, StoryObj } from "@storybook/react"; +// import { screen, userEvent, waitFor, within } from "@storybook/testing-library"; +// import { GovernanceActionDetailsCard } from "@organisms"; +// import { expect } from "@storybook/jest"; + +// const meta = { +// title: "Example/GovernanceActionDetailsCard", +// component: GovernanceActionDetailsCard, +// parameters: { +// layout: "centered", +// }, +// tags: ["autodocs"], +// } satisfies Meta; + +// export default meta; +// type Story = StoryObj; + +// export const GovernanceActionDetailsCardComponent: Story = { +// args: { +// abstainVotes: 1000000, +// createdDate: new Date().toLocaleDateString(), +// expiryDate: new Date().toLocaleDateString(), +// shortenedGovActionId: "Example id", +// noVotes: 1000000, +// type: "Gov Type", +// url: "https://exampleurl.com", +// yesVotes: 1000000, +// details: { +// key: "value", +// key2: ["key-list", "key-list", "key-list"], +// }, +// }, + +// play: async ({ canvasElement }) => { +// const canvas = within(canvasElement); +// const todayDate = new Date().toLocaleDateString(); +// await expect(canvas.getAllByText(todayDate)).toHaveLength(2); + +// const tooltips = canvas.getAllByTestId("InfoOutlinedIcon"); +// await userEvent.hover(tooltips[0]); +// await waitFor(async () => { +// await expect(screen.getByRole("tooltip")).toBeInTheDocument(); +// await expect(screen.getByRole("tooltip")).toHaveTextContent( +// /Submission Date/i, +// ); +// await userEvent.unhover(tooltips[0]); +// }); +// await userEvent.hover(tooltips[1]); + +// await expect(canvas.getByText("Gov Type")).toBeInTheDocument(); +// await expect(canvas.getByText("Example id")).toBeInTheDocument(); + +// await expect(canvas.getByText("key: value")).toBeInTheDocument(); +// await expect(canvas.getAllByText("key-list")).toHaveLength(3); + +// await expect(canvas.getByText(/Yes/i)).toBeInTheDocument(); +// await expect(canvas.getByText(/Abstain/i)).toBeInTheDocument(); +// await expect(canvas.getByText(/No/i)).toBeInTheDocument(); + +// await userEvent.click(canvas.getByTestId("view-other-details-button")); +// }, +// }; diff --git a/govtool/frontend/src/stories/GovernanceActionVoted.stories.ts b/govtool/frontend/src/stories/GovernanceActionVoted.stories.ts index 4fdbc5790..3547562ab 100644 --- a/govtool/frontend/src/stories/GovernanceActionVoted.stories.ts +++ b/govtool/frontend/src/stories/GovernanceActionVoted.stories.ts @@ -1,180 +1,183 @@ -import type { Meta, StoryObj } from "@storybook/react"; +// Story to be updated when new Gov Actions are finished +/* eslint-disable storybook/default-exports */ -import { GovernanceVotedOnCard } from "@molecules"; -import { userEvent, waitFor, within, screen } from "@storybook/testing-library"; -import { expect } from "@storybook/jest"; -import { formatDisplayDate } from "@/utils"; +// import type { Meta, StoryObj } from "@storybook/react"; -const meta = { - title: "Example/GovernanceVotedOnCard", - component: GovernanceVotedOnCard, - parameters: { - layout: "centered", - }, +// import { GovernanceVotedOnCard } from "@molecules"; +// import { userEvent, waitFor, within, screen } from "@storybook/testing-library"; +// import { expect } from "@storybook/jest"; +// import { formatDisplayDate } from "@/utils"; - tags: ["autodocs"], -} satisfies Meta; +// const meta = { +// title: "Example/GovernanceVotedOnCard", +// component: GovernanceVotedOnCard, +// parameters: { +// layout: "centered", +// }, -export default meta; -type Story = StoryObj; +// tags: ["autodocs"], +// } satisfies Meta; -async function checkGovActionVisibility(canvas: ReturnType) { - expect(canvas.getByTestId("exampleType-type")).toBeInTheDocument(); - expect(canvas.getByTestId("exampleHash#1-id")).toBeInTheDocument(); - expect(canvas.getByText(/vote submitted/i)).toBeInTheDocument(); +// export default meta; +// type Story = StoryObj; - expect( - canvas.getByText(formatDisplayDate("1970-01-01T00:00:00Z")), - ).toBeInTheDocument(); - expect( - canvas.getByText(formatDisplayDate("1970-02-01T00:00:00Z")), - ).toBeInTheDocument(); +// async function checkGovActionVisibility(canvas: ReturnType) { +// expect(canvas.getByTestId("exampleType-type")).toBeInTheDocument(); +// expect(canvas.getByTestId("exampleHash#1-id")).toBeInTheDocument(); +// expect(canvas.getByText(/vote submitted/i)).toBeInTheDocument(); - const tooltips = canvas.getAllByTestId("InfoOutlinedIcon"); - await userEvent.hover(tooltips[0]); - await waitFor(async () => { - expect(screen.getByRole("tooltip")).toBeInTheDocument(); - expect(screen.getByRole("tooltip")).toHaveTextContent(/Submission Date/i); - await userEvent.unhover(tooltips[0]); - }); - await userEvent.hover(tooltips[1]); - await waitFor(() => { - expect(screen.getByRole("tooltip")).toBeInTheDocument(); - expect(screen.getByRole("tooltip")).toHaveTextContent(/Expiry Date/i); - }); +// expect( +// canvas.getByText(formatDisplayDate("1970-01-01T00:00:00Z")), +// ).toBeInTheDocument(); +// expect( +// canvas.getByText(formatDisplayDate("1970-02-01T00:00:00Z")), +// ).toBeInTheDocument(); - await expect( - canvas.getByTestId("govaction-exampleHash#1-change-your-vote"), - ).toBeInTheDocument(); -} +// const tooltips = canvas.getAllByTestId("InfoOutlinedIcon"); +// await userEvent.hover(tooltips[0]); +// await waitFor(async () => { +// expect(screen.getByRole("tooltip")).toBeInTheDocument(); +// expect(screen.getByRole("tooltip")).toHaveTextContent(/Submission Date/i); +// await userEvent.unhover(tooltips[0]); +// }); +// await userEvent.hover(tooltips[1]); +// await waitFor(() => { +// expect(screen.getByRole("tooltip")).toBeInTheDocument(); +// expect(screen.getByRole("tooltip")).toHaveTextContent(/Expiry Date/i); +// }); -export const GovernanceVotedOnCardComponent: Story = { - args: { - votedProposal: { - proposal: { - createdDate: "1970-01-01T00:00:00Z", - expiryDate: "1970-02-01T00:00:00Z", - id: "exampleId", - type: "exampleType", - index: 1, - txHash: "exampleHash", - details: "some details", - url: "https://example.com", - metadataHash: "exampleHash", - yesVotes: 1, - noVotes: 0, - abstainVotes: 2, - }, - vote: { - vote: "no", - proposalId: "exampleId", - drepId: "exampleId", - url: "https://example.com", - metadataHash: "exampleHash", - }, - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await checkGovActionVisibility(canvas); - }, -}; +// await expect( +// canvas.getByTestId("govaction-exampleHash#1-change-your-vote"), +// ).toBeInTheDocument(); +// } -export const GovernanceVotedOnCardAbstain: Story = { - args: { - votedProposal: { - proposal: { - createdDate: "1970-01-01T00:00:00Z", - expiryDate: "1970-02-01T00:00:00Z", - id: "exampleId", - type: "exampleType", - index: 1, - txHash: "exampleHash", - details: "some details", - url: "https://example.com", - metadataHash: "exampleHash", - yesVotes: 1, - noVotes: 0, - abstainVotes: 2, - }, - vote: { - vote: "abstain", - proposalId: "exampleId", - drepId: "exampleId", - url: "https://example.com", - metadataHash: "exampleHash", - }, - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await checkGovActionVisibility(canvas); - expect(canvas.getByText(/abstain/i)).toBeInTheDocument(); - }, -}; +// export const GovernanceVotedOnCardComponent: Story = { +// args: { +// votedProposal: { +// proposal: { +// createdDate: "1970-01-01T00:00:00Z", +// expiryDate: "1970-02-01T00:00:00Z", +// id: "exampleId", +// type: "exampleType", +// index: 1, +// txHash: "exampleHash", +// details: "some details", +// url: "https://example.com", +// metadataHash: "exampleHash", +// yesVotes: 1, +// noVotes: 0, +// abstainVotes: 2, +// }, +// vote: { +// vote: "no", +// proposalId: "exampleId", +// drepId: "exampleId", +// url: "https://example.com", +// metadataHash: "exampleHash", +// }, +// }, +// }, +// play: async ({ canvasElement }) => { +// const canvas = within(canvasElement); +// await checkGovActionVisibility(canvas); +// }, +// }; -export const GovernanceVotedOnCardYes: Story = { - args: { - votedProposal: { - proposal: { - createdDate: "1970-01-01T00:00:00Z", - expiryDate: "1970-02-01T00:00:00Z", - id: "exampleId", - type: "exampleType", - index: 1, - txHash: "exampleHash", - details: "some details", - url: "https://example.com", - metadataHash: "exampleHash", - yesVotes: 1, - noVotes: 0, - abstainVotes: 2, - }, - vote: { - vote: "yes", - proposalId: "exampleId", - drepId: "exampleId", - url: "https://example.com", - metadataHash: "exampleHash", - }, - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await checkGovActionVisibility(canvas); - expect(canvas.getByText(/yes/i)).toBeInTheDocument(); - }, -}; +// export const GovernanceVotedOnCardAbstain: Story = { +// args: { +// votedProposal: { +// proposal: { +// createdDate: "1970-01-01T00:00:00Z", +// expiryDate: "1970-02-01T00:00:00Z", +// id: "exampleId", +// type: "exampleType", +// index: 1, +// txHash: "exampleHash", +// details: "some details", +// url: "https://example.com", +// metadataHash: "exampleHash", +// yesVotes: 1, +// noVotes: 0, +// abstainVotes: 2, +// }, +// vote: { +// vote: "abstain", +// proposalId: "exampleId", +// drepId: "exampleId", +// url: "https://example.com", +// metadataHash: "exampleHash", +// }, +// }, +// }, +// play: async ({ canvasElement }) => { +// const canvas = within(canvasElement); +// await checkGovActionVisibility(canvas); +// expect(canvas.getByText(/abstain/i)).toBeInTheDocument(); +// }, +// }; -export const GovernanceVotedOnCardNo: Story = { - args: { - votedProposal: { - proposal: { - createdDate: "1970-01-01T00:00:00Z", - expiryDate: "1970-02-01T00:00:00Z", - id: "exampleId", - type: "exampleType", - index: 1, - txHash: "exampleHash", - details: "some details", - url: "https://example.com", - metadataHash: "exampleHash", - yesVotes: 1, - noVotes: 0, - abstainVotes: 2, - }, - vote: { - vote: "no", - proposalId: "exampleId", - drepId: "exampleId", - url: "https://example.com", - metadataHash: "exampleHash", - }, - }, - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await checkGovActionVisibility(canvas); - expect(canvas.getByText(/no/i)).toBeInTheDocument(); - }, -}; +// export const GovernanceVotedOnCardYes: Story = { +// args: { +// votedProposal: { +// proposal: { +// createdDate: "1970-01-01T00:00:00Z", +// expiryDate: "1970-02-01T00:00:00Z", +// id: "exampleId", +// type: "exampleType", +// index: 1, +// txHash: "exampleHash", +// details: "some details", +// url: "https://example.com", +// metadataHash: "exampleHash", +// yesVotes: 1, +// noVotes: 0, +// abstainVotes: 2, +// }, +// vote: { +// vote: "yes", +// proposalId: "exampleId", +// drepId: "exampleId", +// url: "https://example.com", +// metadataHash: "exampleHash", +// }, +// }, +// }, +// play: async ({ canvasElement }) => { +// const canvas = within(canvasElement); +// await checkGovActionVisibility(canvas); +// expect(canvas.getByText(/yes/i)).toBeInTheDocument(); +// }, +// }; + +// export const GovernanceVotedOnCardNo: Story = { +// args: { +// votedProposal: { +// proposal: { +// createdDate: "1970-01-01T00:00:00Z", +// expiryDate: "1970-02-01T00:00:00Z", +// id: "exampleId", +// type: "exampleType", +// index: 1, +// txHash: "exampleHash", +// details: "some details", +// url: "https://example.com", +// metadataHash: "exampleHash", +// yesVotes: 1, +// noVotes: 0, +// abstainVotes: 2, +// }, +// vote: { +// vote: "no", +// proposalId: "exampleId", +// drepId: "exampleId", +// url: "https://example.com", +// metadataHash: "exampleHash", +// }, +// }, +// }, +// play: async ({ canvasElement }) => { +// const canvas = within(canvasElement); +// await checkGovActionVisibility(canvas); +// expect(canvas.getByText(/no/i)).toBeInTheDocument(); +// }, +// }; diff --git a/govtool/frontend/src/theme.ts b/govtool/frontend/src/theme.ts index 8a8736f17..7b9bd2331 100644 --- a/govtool/frontend/src/theme.ts +++ b/govtool/frontend/src/theme.ts @@ -9,6 +9,8 @@ import { successGreen, } from "./consts"; +export type Theme = typeof theme; + export const theme = createTheme({ breakpoints: { values: { @@ -114,8 +116,10 @@ export const theme = createTheme({ palette: { accentOrange: "#F29339", accentYellow: "#F2D9A9", + arcticWhite: "#FBFBFF", boxShadow1: "rgba(0, 18, 61, 0.37)", boxShadow2: "rgba(47, 98, 220, 0.2)", + errorRed: "#9E2323", fadedPurple: "#716E88", highlightBlue: "#C2EFF299", inputRed: "#FAEAEB", diff --git a/govtool/frontend/src/types/@mui.d.ts b/govtool/frontend/src/types/@mui.d.ts index c8ea5da12..e83f05e7e 100644 --- a/govtool/frontend/src/types/@mui.d.ts +++ b/govtool/frontend/src/types/@mui.d.ts @@ -16,8 +16,10 @@ declare module "@mui/material/styles" { interface PaletteOptions extends MuiPalette { accentOrange: string; accentYellow: string; + arcticWhite: string; boxShadow1: string; boxShadow2: string; + errorRed: string; highlightBlue: string; inputRed: string; negativeRed: string; diff --git a/govtool/frontend/src/utils/getProposalTypeLabel.ts b/govtool/frontend/src/utils/getProposalTypeLabel.ts index e44f7905c..7a3dcccdc 100644 --- a/govtool/frontend/src/utils/getProposalTypeLabel.ts +++ b/govtool/frontend/src/utils/getProposalTypeLabel.ts @@ -4,3 +4,6 @@ export const getProposalTypeLabel = (type: string) => { const label = GOVERNANCE_ACTIONS_FILTERS.find((i) => i.key === type)?.label; return label || type; }; + +export const getProposalTypeNoEmptySpaces = (type: string) => + getProposalTypeLabel(type).replace(/ /g, ""); diff --git a/govtool/frontend/src/utils/tests/getProposalTypeNoEmptySpaces.test.ts b/govtool/frontend/src/utils/tests/getProposalTypeNoEmptySpaces.test.ts new file mode 100644 index 000000000..4ec64fe92 --- /dev/null +++ b/govtool/frontend/src/utils/tests/getProposalTypeNoEmptySpaces.test.ts @@ -0,0 +1,25 @@ +import { getProposalTypeNoEmptySpaces } from ".."; + +describe("getProposalTypeNoEmptySpaces", () => { + it("returns correct label with no spaces for a known type", () => { + const type = "NoConfidence"; + const expectedLabel = "NoConfidence"; + expect(getProposalTypeNoEmptySpaces(type)).toBe(expectedLabel); + }); + + it("returns correct label with no spaces for another known type", () => { + const type = "ParameterChange"; + const expectedLabel = "ProtocolParameterChanges"; + expect(getProposalTypeNoEmptySpaces(type)).toBe(expectedLabel); + }); + + it("returns the type itself with no spaces removed when no matching key is found", () => { + const type = "UnknownType"; + expect(getProposalTypeNoEmptySpaces(type)).toBe(type); + }); + + it("returns an empty string when given an empty string", () => { + const type = ""; + expect(getProposalTypeNoEmptySpaces(type)).toBe(type); + }); +});