diff --git a/app/actions/ClientActions.js b/app/actions/ClientActions.js index a85f0f6245..64e20eccb5 100644 --- a/app/actions/ClientActions.js +++ b/app/actions/ClientActions.js @@ -655,6 +655,7 @@ export const setVoteChoicesAttempt = (agendaId, choiceId, passphrase) => ( for (let i = 0; i < stakePools.length; i++) { dispatch(getVoteChoicesAttempt(stakePools[i])); } + dispatch(getVoteChoicesAttempt()); }) .catch((error) => dispatch({ error, type: SETVOTECHOICES_FAILED })); }; diff --git a/app/actions/GovernanceActions.js b/app/actions/GovernanceActions.js index 7a115b7d9d..8d7aad66d5 100644 --- a/app/actions/GovernanceActions.js +++ b/app/actions/GovernanceActions.js @@ -616,9 +616,11 @@ export const getProposalDetails = (token) => async (dispatch, getState) => { } }; -export const viewProposalDetails = (token) => (dispatch) => { +export const viewProposalDetails = (token) => (dispatch) => dispatch(pushHistory(`/proposal/details/${token}`)); -}; + +export const viewAgendaDetails = (name) => (dispatch) => + dispatch(pushHistory(`/agenda/details/${name}`)); export const UPDATEVOTECHOICE_ATTEMPT = "UPDATEVOTECHOICE_ATTEMPT"; export const UPDATEVOTECHOICE_SUCCESS = "UPDATEVOTECHOICE_SUCCESS"; diff --git a/app/components/buttons/EyeFilterMenu/EyeFilterMenu.jsx b/app/components/buttons/EyeFilterMenu/EyeFilterMenu.jsx index 9d659bd7b8..c23f4d9454 100644 --- a/app/components/buttons/EyeFilterMenu/EyeFilterMenu.jsx +++ b/app/components/buttons/EyeFilterMenu/EyeFilterMenu.jsx @@ -63,6 +63,7 @@ const EyeFilterMenu = ({ ref={wrapperRef}>
diff --git a/app/components/layout/StandalonePageBody/StandalonePageBody.module.css b/app/components/layout/StandalonePageBody/StandalonePageBody.module.css index d3e619bde4..46b1aa6f3e 100644 --- a/app/components/layout/StandalonePageBody/StandalonePageBody.module.css +++ b/app/components/layout/StandalonePageBody/StandalonePageBody.module.css @@ -8,5 +8,6 @@ @media screen and (max-width: 1179px) { .body { padding-left: 20px; + padding-right: 20px; } } diff --git a/app/components/layout/TabbedPage/TabbedPage.jsx b/app/components/layout/TabbedPage/TabbedPage.jsx index 44b2428bb8..40828db433 100644 --- a/app/components/layout/TabbedPage/TabbedPage.jsx +++ b/app/components/layout/TabbedPage/TabbedPage.jsx @@ -69,7 +69,8 @@ const TabbedPage = ({ tabsClassName, tabContentClassName, onChange, - caret + caret, + activeCaretClassName }) => { const location = useSelector(sel.location); const uiAnimations = useSelector(sel.uiAnimations); @@ -133,6 +134,7 @@ const TabbedPage = ({ tabs={tabHeaders} tabsClassName={tabsClassName} caret={caret} + activeCaretClassName={activeCaretClassName} />
diff --git a/app/components/shared/PoliteiaLink.jsx b/app/components/shared/PoliteiaLink.jsx index 26a1c6ed97..8bf84570dc 100644 --- a/app/components/shared/PoliteiaLink.jsx +++ b/app/components/shared/PoliteiaLink.jsx @@ -7,14 +7,17 @@ const PoliteiaLink = ({ path, className, isTestnet, - CustomComponent + CustomComponent, + hrefProp }) => { const href = useMemo( () => - `https://${isTestnet ? "test-proposals" : "proposals"}.decred.org${ - path || "" - }`, - [isTestnet, path] + hrefProp + ? hrefProp + : `https://${isTestnet ? "test-proposals" : "proposals"}.decred.org${ + path || "" + }`, + [isTestnet, path, hrefProp] ); const onClickHandler = useCallback(() => wallet.openExternalURL(href), [ href diff --git a/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.jsx b/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.jsx index 16f744903b..006aadcf36 100644 --- a/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.jsx +++ b/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.jsx @@ -13,7 +13,12 @@ export const RoutedTab = (path, link, className, activeClassName) => ({ activeClassName }); -const RoutedTabsHeader = ({ tabs, tabsClassName, caret }) => { +const RoutedTabsHeader = ({ + tabs, + tabsClassName, + caret, + activeCaretClassName +}) => { const { uiAnimations, caretLeft, caretWidth, nodes } = useRoutedTabsHeader(); const getAnimatedCaret = useCallback(() => { @@ -26,12 +31,15 @@ const RoutedTabsHeader = ({ tabs, tabsClassName, caret }) => { {(style) => (
-
+
)} ); - }, [caretLeft, caretWidth]); + }, [caretLeft, caretWidth, activeCaretClassName]); const getStaticCaret = useCallback(() => { const style = { diff --git a/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.module.css b/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.module.css index 49dce642f3..8ee3f458c1 100644 --- a/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.module.css +++ b/app/components/shared/RoutedTabsHeader/RoutedTabsHeader.module.css @@ -12,7 +12,7 @@ } .tab a { - margin: 0 5px 0 5px; + margin: 0; color: var(--grey-7); text-decoration: none; font-size: 16px; @@ -30,13 +30,13 @@ .tabCaret { position: absolute; bottom: 0; - height: 5px; + height: 4px; } .tabCaret .active { background-color: var(--accent-color); position: absolute; - height: 5px; + height: 4px; } @media screen and (max-width: 768px) { diff --git a/app/components/views/AgendaDetailsPage/AgendaDetails.jsx b/app/components/views/AgendaDetailsPage/AgendaDetails.jsx new file mode 100644 index 0000000000..48680e5a59 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/AgendaDetails.jsx @@ -0,0 +1,48 @@ +import { classNames } from "pi-ui"; +import styles from "./AgendaDetails.module.css"; +import { FormattedMessage as T } from "react-intl"; +import { useAgendaDetails } from "./hooks"; +import { VoteSection, AgendaCard } from "./helpers"; + +const AgendaDetails = () => { + const { + agenda, + selectedChoice, + newSelectedChoice, + setNewSelectedChoice, + choices, + isLoading, + goBackHistory, + updatePreferences + } = useAgendaDetails(); + return ( +
+
+
+
+
+ +
+ +
+ +
+
+ ); +}; + +export default AgendaDetails; diff --git a/app/components/views/AgendaDetailsPage/AgendaDetails.module.css b/app/components/views/AgendaDetailsPage/AgendaDetails.module.css new file mode 100644 index 0000000000..8f49b2da0c --- /dev/null +++ b/app/components/views/AgendaDetailsPage/AgendaDetails.module.css @@ -0,0 +1,74 @@ +.cardWrapper { + display: flex; +} + +.backButton { + width: 40px; + background-color: var(--governance-nav-button-bg); + cursor: pointer; +} + +.backButton:hover { + background-color: var(--grey-5); +} + +.backArrow { + height: 10px; + width: 10px; + background-image: var(--menu-arrow-up); + background-position: 50% 5px; + background-repeat: no-repeat; + transform: rotate(-90deg); +} + +.backButton:hover .backArrow { + background-image: var(--arrow-left-white); + transform: rotate(0); + background-position: 50% 0; +} +.column { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.detailsText { + font-family: var(--font-family-regular-semi-bold); + padding: 30px 40px; + background-color: var(--background-back-color); + margin: 5px 0 20px 0; + border-top: 1px solid var(--tabbed-page-header-bg); + width: 764px; + font-size: 16px; +} + +.piButtonWrapper { + display: flex; + justify-content: flex-end; + width: 740px; +} + +.loadingPage { + width: 100%; + text-align: center; +} + +.piButton { + border: 0 !important; + text-decoration: none !important; + color: var(--color-white) !important; + font-weight: var(--font-weight-semi-bold) !important; +} + +@media screen and (max-width: 1179px) { + .detailsText { + width: 674px; + } +} + +@media screen and (max-width: 768px) { + .detailsText { + padding-left: 20px; + width: 355px; + } +} diff --git a/app/components/views/AgendaDetailsPage/AgendaDetailsPage.jsx b/app/components/views/AgendaDetailsPage/AgendaDetailsPage.jsx new file mode 100644 index 0000000000..74ae3b34f4 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/AgendaDetailsPage.jsx @@ -0,0 +1,13 @@ +import AgendaDetails from "./AgendaDetails"; +import { Header } from "./helpers"; +import { StandalonePage } from "layout"; + +const AgendaDetailsPage = () => { + return ( + }> + + + ); +}; + +export default AgendaDetailsPage; diff --git a/app/components/views/AgendaDetailsPage/helpers/AgendaCard/AgendaCard.jsx b/app/components/views/AgendaDetailsPage/helpers/AgendaCard/AgendaCard.jsx new file mode 100644 index 0000000000..aefc328055 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/AgendaCard/AgendaCard.jsx @@ -0,0 +1,72 @@ +import styles from "./AgendaCard.module.css"; +import { classNames, Tooltip, StatusTag } from "pi-ui"; +import { FormattedMessage as T } from "react-intl"; + +const AgendaCard = ({ agenda, selectedChoice, className }) => { + const inProgress = !agenda.finished; + + return ( +
+
+
+
{agenda.name}
+
+ {agenda.name} + }} + /> +
+
{`${agenda.description} `}
+
+
+ {!inProgress ? ( + + }> + + + ) : ( + + }> + + + )} +
+ {selectedChoice} + }} + /> +
+
+
+
+ ); +}; + +export default AgendaCard; diff --git a/app/components/views/AgendaDetailsPage/helpers/AgendaCard/AgendaCard.module.css b/app/components/views/AgendaDetailsPage/helpers/AgendaCard/AgendaCard.module.css new file mode 100644 index 0000000000..2a6938c8a7 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/AgendaCard/AgendaCard.module.css @@ -0,0 +1,171 @@ +.overview { + background-color: var(--background-back-color); + width: 724px; + padding: 20px 40px 20px 20px; +} + +.row { + display: flex; + justify-content: space-between; +} + +.titleText { + font-size: 18px; + line-height: 23px; + font-weight: 600; + color: var(--tutorial-header) !important; + letter-spacing: 0em; +} + +.title { + cursor: pointer; +} + +.subTitle { + font-size: 16px; +} + +.creator { + font-weight: 600; +} + +.tooltipTitle { + width: max-content; + max-width: max-content !important; +} + +.updatedEvent, +.version { + color: var(--grey-5); +} + +.token { + padding: 4px 10px; + background-color: var(--color-blue-lighter); + border-radius: 5px; + font-size: 16px; +} + +.token.dark { + color: var(--grey-2); +} + +.proposedToRfp { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: var(--stroke-color-default); +} + +.proposedToRfp a { + font-size: 13px; + line-height: 19px; + color: var(--overview-balance-label); + text-decoration: none; + cursor: pointer; +} + +.proposedToRfp span { + font-size: 13px; + line-height: 19px; + color: var(--overview-balance-label); + text-decoration: none; +} + +.voteStatusBar { + flex-basis: 75%; + margin-right: 10px; +} + +.statusBarRow { + padding: 0 20px 20px; +} + +.voteEnd { + align-items: flex-end; + flex-basis: 25%; + margin-top: -0.3rem; + color: var(--grey-5); + font-size: 13px; +} + +.quorumTooltip { + white-space: nowrap; + font-size: var(--font-size-small); + color: var(--color-gray); +} + +.darkQuorumTooltip { + color: var(--text-color); +} + +.votesQuorum { + color: var(--text-secondary-color) !important; +} + +.votesReceived { + color: var(--tab-text-active-color) !important; +} + +.tooltipContent { + width: max-content; +} + +.preference { + color: var(--main-dark-blue); + display: flex; + flex-direction: row; + margin-top: 8px; + align-items: center; +} + +.preference span { + margin-left: 10px; + padding: 4px 10px; + border-radius: 5px; + color: var(--agenda-preference); + background-color: var(--color-blue-lighter); + font-size: 16px; + text-transform: capitalize; +} + +.agendaId { + font-size: 16px; + color: var(--main-dark-blue); + margin: 10px 0 20px 0; +} + +.description { + font-size: 16px; + color: var(--main-dark-blue); +} + +@media screen and (max-width: 1179px) { + .overview { + width: 634px; + } + + .overviewInfo { + padding: 20px; + } +} + +@media screen and (max-width: 768px) { + .overview { + width: 315px; + padding: 10px !important; + } + + .votingProgress { + width: 315px; + padding-left: 20px; + } + + .overviewVoting { + width: 315px; + } + + .row { + flex-direction: column; + } +} diff --git a/app/components/views/AgendaDetailsPage/helpers/AgendaCard/index.js b/app/components/views/AgendaDetailsPage/helpers/AgendaCard/index.js new file mode 100644 index 0000000000..0bef3d4fd1 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/AgendaCard/index.js @@ -0,0 +1 @@ +export { default } from "./AgendaCard"; diff --git a/app/components/views/AgendaDetailsPage/helpers/Header/Header.jsx b/app/components/views/AgendaDetailsPage/helpers/Header/Header.jsx new file mode 100644 index 0000000000..9d6cef42c0 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/Header/Header.jsx @@ -0,0 +1,19 @@ +import { FormattedMessage as T } from "react-intl"; +import styles from "./Header.module.css"; +import { StandaloneHeader } from "layout"; +import { GOVERNANCE_ICON } from "constants"; +import TabHeader from "../../../GovernancePage/TabHeader/TabHeader"; + +const Header = React.memo(function Header() { + return ( + } + description={ + + } + iconType={GOVERNANCE_ICON} + /> + ); +}); + +export default Header; diff --git a/app/components/views/AgendaDetailsPage/helpers/Header/Header.module.css b/app/components/views/AgendaDetailsPage/helpers/Header/Header.module.css new file mode 100644 index 0000000000..269227b864 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/Header/Header.module.css @@ -0,0 +1,3 @@ +.descriptionHeader { + margin-left: 0; +} diff --git a/app/components/views/AgendaDetailsPage/helpers/Header/index.js b/app/components/views/AgendaDetailsPage/helpers/Header/index.js new file mode 100644 index 0000000000..2764567d96 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/Header/index.js @@ -0,0 +1 @@ +export { default } from "./Header"; diff --git a/app/components/views/AgendaDetailsPage/helpers/VoteSection/CastVoteModalButton/CastVoteModalButton.jsx b/app/components/views/AgendaDetailsPage/helpers/VoteSection/CastVoteModalButton/CastVoteModalButton.jsx new file mode 100644 index 0000000000..5d27a57d03 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/VoteSection/CastVoteModalButton/CastVoteModalButton.jsx @@ -0,0 +1,24 @@ +import { FormattedMessage as T } from "react-intl"; +import { PassphraseModalButton } from "buttons"; +import styles from "./CastVoteModalButton.module.css"; + +const CastVoteModalButton = ({ onSubmit, newVoteChoice, isLoading }) => ( + + +
+
+ {newVoteChoice} +
+ + } + disabled={isLoading} + loading={isLoading} + onSubmit={onSubmit} + className={styles.voteButton} + buttonLabel={} + /> +); + +export default CastVoteModalButton; diff --git a/app/components/views/AgendaDetailsPage/helpers/VoteSection/CastVoteModalButton/CastVoteModalButton.module.css b/app/components/views/AgendaDetailsPage/helpers/VoteSection/CastVoteModalButton/CastVoteModalButton.module.css new file mode 100644 index 0000000000..70ccf8d71d --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/VoteSection/CastVoteModalButton/CastVoteModalButton.module.css @@ -0,0 +1,34 @@ +.voteConfirmation { + background-color: var(--background-container); + font-size: 27px; + line-height: 34px; + margin-left: 10px; + padding-left: 10px; + padding-right: 10px; + display: flex; + flex-direction: row; + text-transform: capitalize; +} + +.yesProposal { + width: 12px; + height: 12px; + border-radius: 100px; + background-color: var(--vote-yes-color); + margin-top: 13px; + margin-right: 5px; +} + +.noProposal { + width: 12px; + height: 12px; + border-radius: 100px; + background-color: var(--vote-no-color); + margin-top: 13px; + margin-right: 5px; +} + +.voteButton { + padding: 5px 8px; + margin-left: 5px; +} diff --git a/app/components/views/AgendaDetailsPage/helpers/VoteSection/CastVoteModalButton/index.js b/app/components/views/AgendaDetailsPage/helpers/VoteSection/CastVoteModalButton/index.js new file mode 100644 index 0000000000..5737838e6a --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/VoteSection/CastVoteModalButton/index.js @@ -0,0 +1 @@ +export { default } from "./CastVoteModalButton"; diff --git a/app/components/views/AgendaDetailsPage/helpers/VoteSection/VoteSection.jsx b/app/components/views/AgendaDetailsPage/helpers/VoteSection/VoteSection.jsx new file mode 100644 index 0000000000..f68f79d753 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/VoteSection/VoteSection.jsx @@ -0,0 +1,56 @@ +import styles from "./VoteSection.module.css"; +import { RadioButtonGroup } from "pi-ui"; +import { FormattedMessage as T } from "react-intl"; +import CastVoteModalButton from "./CastVoteModalButton"; + +const VoteSection = ({ + choices, + selected, + setSelected, + finished, + updatePreferences, + isLoading +}) => { + const options = choices + .map(({ choiceId }) => ({ + label: choiceId, + value: choiceId + })) + .reverse(); + const handleChange = (option) => setSelected(option.value); + const showVotingOptions = !!options.length && (!finished || selected); + return ( + showVotingOptions && ( +
+
+
+ {finished && selected ? ( + + ) : ( + + )} +
+ styles[o.value])} + /> + {!finished && ( + + )} +
+
+ ) + ); +}; + +export default VoteSection; diff --git a/app/components/views/AgendaDetailsPage/helpers/VoteSection/VoteSection.module.css b/app/components/views/AgendaDetailsPage/helpers/VoteSection/VoteSection.module.css new file mode 100644 index 0000000000..85b318005b --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/VoteSection/VoteSection.module.css @@ -0,0 +1,97 @@ +.voteSection { + background-color: var(--background-back-color); + padding: 10px 40px; + width: 764px; + margin-top: 5px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.total { + font-weight: 600; +} + +.votePreference { + line-height: 0; + flex-basis: 73%; + display: flex; + align-items: center; + height: 35px; +} + +.preferenceTitle { + font-size: 13px; +} + +.voteRadioButtons { + margin-left: 10px; +} + +.yes, +.abstain, +.no { + margin-top: 0 !important; +} + +.yes > label, +.abstain > label, +.no > label { + display: flex; + align-items: center; +} + +.yes > label > span:nth-child(1) { + border-color: var(--vote-yes-color); + top: 0 !important; +} + +.yes > label > span:nth-child(2) { + background-color: var(--vote-yes-color); + top: 0.4rem !important; +} + +.no > label > span:nth-child(1) { + border-color: var(--vote-no-color); + top: 0 !important; +} + +.no > label > span:nth-child(2) { + background-color: var(--vote-no-color); + top: 0.4rem !important; +} + +.abstain > label > span:nth-child(1) { + border-color: var(--vote-abstain-color); + top: 0 !important; +} + +.abstain > label > span:nth-child(2) { + background-color: var(--vote-abstain-color); + top: 0.4rem !important; +} + +@media screen and (max-width: 1179px) { + .votePreference { + flex-basis: 70%; + } + + .voteButton { + left: 123px; + } + + .voteSection { + width: 674px; + } +} + +@media screen and (max-width: 768px) { + .voteSection { + width: 355px; + padding: 10px 10px 10px 20px; + } + .votePreference { + flex-basis: initial; + } +} diff --git a/app/components/views/AgendaDetailsPage/helpers/VoteSection/index.js b/app/components/views/AgendaDetailsPage/helpers/VoteSection/index.js new file mode 100644 index 0000000000..642c3b4692 --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/VoteSection/index.js @@ -0,0 +1 @@ +export { default } from "./VoteSection"; diff --git a/app/components/views/AgendaDetailsPage/helpers/index.js b/app/components/views/AgendaDetailsPage/helpers/index.js new file mode 100644 index 0000000000..9e2d027aae --- /dev/null +++ b/app/components/views/AgendaDetailsPage/helpers/index.js @@ -0,0 +1,3 @@ +export { default as Header } from "./Header"; +export { default as VoteSection } from "./VoteSection"; +export { default as AgendaCard } from "./AgendaCard"; diff --git a/app/components/views/AgendaDetailsPage/hooks.js b/app/components/views/AgendaDetailsPage/hooks.js new file mode 100644 index 0000000000..e96219baae --- /dev/null +++ b/app/components/views/AgendaDetailsPage/hooks.js @@ -0,0 +1,54 @@ +import { useCallback, useMemo, useState } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import * as sel from "selectors"; +import * as cli from "actions/ClientActions"; +import { useParams } from "react-router-dom"; +import { find, compose, eq, get } from "fp"; + +export const useAgendaDetails = () => { + const getAgendaSelectedChoice = (agenda, voteChoices) => + get( + ["choiceId"], + find(compose(eq(agenda.name), get(["agendaId"])), voteChoices) + ) || "abstain"; + const { name } = useParams(); + const allAgendas = useSelector(sel.allAgendas); + const agenda = allAgendas.find((agenda) => agenda.name === name); + const voteChoices = useSelector(sel.voteChoices); + + const agendaChoices = agenda.choices; + const choices = useMemo( + () => + agendaChoices.map((choice) => ({ + choiceId: choice.getId() + })), + [agendaChoices] + ); + const selectedChoice = getAgendaSelectedChoice(agenda, voteChoices); + + const [newSelectedChoice, setNewSelectedChoice] = useState(selectedChoice); + const settingVoteChoices = useSelector(sel.setVoteChoicesAttempt); + const settingVspdVoteChoices = useSelector(sel.setVspdVoteChoicesAttempt); + const isLoading = settingVoteChoices || settingVspdVoteChoices; + + const dispatch = useDispatch(); + const goBackHistory = useCallback(() => dispatch(cli.goBackHistory()), [ + dispatch + ]); + const onUpdateVotePreference = (agendaId, choiceId, passphrase) => + dispatch(cli.setVoteChoicesAttempt(agendaId, choiceId, passphrase)); + const updatePreferences = async (passphrase) => { + await onUpdateVotePreference(agenda.name, newSelectedChoice, passphrase); + }; + + return { + agenda, + selectedChoice, + newSelectedChoice, + setNewSelectedChoice, + choices, + isLoading, + goBackHistory, + updatePreferences + }; +}; diff --git a/app/components/views/AgendaDetailsPage/index.js b/app/components/views/AgendaDetailsPage/index.js new file mode 100644 index 0000000000..01f54bdd4f --- /dev/null +++ b/app/components/views/AgendaDetailsPage/index.js @@ -0,0 +1 @@ +export { default } from "./AgendaDetailsPage"; diff --git a/app/components/views/GovernancePage/Blockchain/AgendaOverview/AgendaCard.jsx b/app/components/views/GovernancePage/Blockchain/AgendaOverview/AgendaCard.jsx deleted file mode 100644 index 0913dfa394..0000000000 --- a/app/components/views/GovernancePage/Blockchain/AgendaOverview/AgendaCard.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { classNames } from "pi-ui"; -import { FormattedMessage as T } from "react-intl"; -import AgendaProgressIndicator from "./ProgressIndicator"; -import styles from "./Overview.module.css"; - -const AgendaCard = ({ agenda, onClick, selectedChoice }) => ( -
-
{agenda.name}
-
- Preference:{" "} - {selectedChoice} -
-
- {`${agenda.description} `} - - : - {agenda.name} - -
- -
-); - -export default AgendaCard; diff --git a/app/components/views/GovernancePage/Blockchain/AgendaOverview/AgendaOverview.jsx b/app/components/views/GovernancePage/Blockchain/AgendaOverview/AgendaOverview.jsx index c05bd26de0..a6afbe5e30 100644 --- a/app/components/views/GovernancePage/Blockchain/AgendaOverview/AgendaOverview.jsx +++ b/app/components/views/GovernancePage/Blockchain/AgendaOverview/AgendaOverview.jsx @@ -1,56 +1,20 @@ -import Overview from "./Overview"; -import AgendaCard from "./AgendaCard"; -import { useState, useEffect, useMemo } from "react"; +import AgendaCard from "../../../AgendaDetailsPage/helpers/AgendaCard"; +import styles from "./AgendaOverview.module.css"; +import { classNames } from "pi-ui"; const AgendaOverview = ({ selectedChoice, - disabled, agenda, - onCloseAgenda, - showVoteChoice, - onClick, - onUpdateVotePreference, - isLoading -}) => { - const [selectedChoiceId, setSelectedChoiceId] = useState(selectedChoice); - useEffect(() => { - if (selectedChoice !== selectedChoiceId) { - setSelectedChoiceId(selectedChoiceId); - } - }, [selectedChoice, selectedChoiceId]); - - const updatePreferences = async (passphrase) => { - await onUpdateVotePreference(agenda.name, selectedChoiceId, passphrase); - }; - - const agendaChoices = agenda.choices; - const choices = useMemo( - () => - agendaChoices.map((choice) => ({ - choiceId: choice.getId() - })), - [agendaChoices] - ); - return showVoteChoice ? ( - - ) : ( - - ); -}; + viewAgendaDetailsHandler +}) => ( +
viewAgendaDetailsHandler(agenda.name)} + className={classNames(styles.cardWrapper)}> + +
+
+
+
+); export default AgendaOverview; diff --git a/app/components/views/GovernancePage/Blockchain/AgendaOverview/AgendaOverview.module.css b/app/components/views/GovernancePage/Blockchain/AgendaOverview/AgendaOverview.module.css new file mode 100644 index 0000000000..e2300d7d5f --- /dev/null +++ b/app/components/views/GovernancePage/Blockchain/AgendaOverview/AgendaOverview.module.css @@ -0,0 +1,38 @@ +.cardWrapper { + display: flex; + cursor: pointer; + margin-bottom: 5px; +} + +.cardWrapper:hover { + filter: drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.16)); +} + +.continueButton { + width: 40px; + background-color: var(--governance-nav-button-bg); + cursor: pointer; +} + +.cardWrapper:hover .continueButton { + background-color: var(--grey-5); +} + +.continueArrow { + height: 10px; + width: 10px; + background-image: var(--menu-arrow-up); + background-position: 50% 5px; + background-repeat: no-repeat; + transform: rotate(90deg); +} + +.cardWrapper:hover .continueArrow { + background-image: var(--arrow-left-white); + background-position: 50% 0; + transform: rotate(180deg); +} + +.overview { + padding-left: 35px !important; +} diff --git a/app/components/views/GovernancePage/Blockchain/AgendaOverview/Overview.jsx b/app/components/views/GovernancePage/Blockchain/AgendaOverview/Overview.jsx deleted file mode 100644 index eb46c71e32..0000000000 --- a/app/components/views/GovernancePage/Blockchain/AgendaOverview/Overview.jsx +++ /dev/null @@ -1,118 +0,0 @@ -import { RadioButtonGroup, classNames } from "pi-ui"; -import { PassphraseModalButton } from "buttons"; -import { FormattedMessage as T } from "react-intl"; -import ProgressIndicator from "./ProgressIndicator"; -import styles from "./Overview.module.css"; - -const AgendaDetails = ({ name, onClose, description }) => ( - -); - -const AgendaVotingOptions = ({ - choices, - selected, - setSelected, - finished, - disabled -}) => { - const options = choices.map(({ choiceId }) => ({ - label: choiceId, - value: choiceId - })); - const handleChange = (option) => setSelected(option.value); - const showVotingOptions = !!options.length && (!finished || selected); - - return ( - showVotingOptions && ( -
- {finished && selected ? ( - - ) : ( - - )} - : -
- -
-
- ) - ); -}; - -const Overview = ({ - isFinished, - agendaId, - agendaDescription, - choices, - selectedChoiceId, - closeCurrentAgenda, - setSelectedChoiceId, - updatePreferences, - disabled, - passed, - isLoading -}) => ( -
- - -
-
- } - modalClassName={styles.passphraseModal} - onSubmit={updatePreferences} - className={styles.updatePreferencesButton} - disabled={disabled || isLoading} - buttonLabel={ - isLoading ? ( - - ) : ( - - ) - } - /> -
-
- -
-
-
-); - -export default Overview; diff --git a/app/components/views/GovernancePage/Blockchain/AgendaOverview/Overview.module.css b/app/components/views/GovernancePage/Blockchain/AgendaOverview/Overview.module.css deleted file mode 100644 index 60dd40380f..0000000000 --- a/app/components/views/GovernancePage/Blockchain/AgendaOverview/Overview.module.css +++ /dev/null @@ -1,248 +0,0 @@ -.overview { - position: relative; - margin-bottom: 15px; -} - -.titleArea { - margin-bottom: 29px; - position: relative; -} - -.titleName { - padding-right: 10px; - float: left; - font-size: 19px; -} - -.agenda { - width: 100%; - padding: 20px; - display: flex; - flex-direction: column; - margin-bottom: 20px; - border-left: 1px solid var(--text-color-light); - background-color: var(--background-back-color); -} - -.text { - position: relative; - line-height: 18px; - color: var(--stroke-color-hovered); - font-size: 13px; -} - -.idCt { - color: var(--input-color-default); -} - -.id { - font-weight: 600; -} - -.optionsArea { - flex-direction: column; - align-items: stretch; - font-size: 19px; - margin: 15px 0px; -} - -.optionsGroup { - font-size: 15px; - text-transform: capitalize; -} - -.bottom { - padding-top: 15px; - display: flex; -} - -.bottomOverview { - width: 100%; - position: relative; -} - -.bottomOptions { - min-width: 240px; -} - -.updatePreferencesButton { - width: 134px; -} - -.overviewTitleClose { - width: 11px; - height: 11px; - float: right; - background-image: var(--agenda-close-icon); - background-position: 50% 50%; - background-size: 11px; - background-repeat: no-repeat; - max-width: 100%; - background-color: transparent; - cursor: pointer; -} - -.overviewTitleClose:hover { - opacity: 0.7; -} - -.agendaCard { - width: 225px; - height: 225px; - margin-bottom: 20px; - margin-right: 20px; - padding: 20px; - background-color: var(--background-back-color); - background-image: var(--agenda-card-kebab); - background-position: 93% 27px; - background-size: 10px; - background-repeat: no-repeat; - color: var(--input-color-default); - cursor: pointer; -} - -.agendaCard:hover { - background-image: var(--agenda-card-kebab-hover); - background-size: 10px; -} - -.disabled { - position: relative; - overflow: hidden; - width: 180px; - height: 190px; - margin-bottom: 20px; - margin-right: 20px; - padding: 20px; - float: left; - background-color: var(--disabled-background-color); - background-image: var(--agenda-card-kebab-disabled); - background-size: 10px; - background-position: 93% 27px; - background-repeat: no-repeat; - color: var(--disabled-color); - cursor: default; -} - -.indicatorPending { - float: right; - padding: 5px 8px 5px 20px; - border-style: solid; - border-width: 1px; - border-radius: 3px; - font-size: 12px; - line-height: 8px; - text-align: right; - text-transform: capitalize; - border-color: var(--input-color); - background-image: var(--agenda-indicator-pending); - background-position: 6px 50%; - background-size: 10px; - background-repeat: no-repeat; - color: var(--input-color); - margin-right: 20px; - margin-bottom: 20px; -} - -.tooltip { - float: right; -} - -.tooltipContent { - width: 22rem; -} - -.progressTooltipContent { - width: 15rem; -} - -.bottomCfg { - margin: 10px 0; - float: left; - color: var(--agenda-card-bottom-cfg); -} - -.bottomCfgLast { - float: left; - color: var(--input-color-default); -} - -.bottomCfgDisabled { - float: left; - color: var(--disabled-color); -} - -.bottomCfgLastDisabled { - color: var(--disabled-color); -} - -.bottomCfgLastBold { - font-weight: 600; -} - -.name { - padding-right: 10px; - float: left; - font-size: 19px; -} - -.topPreference { - margin-top: 6px; - float: left; - width: 186px; - color: var(--header-desc-lighter-color); -} - -.topPreference span { - color: var(--input-color-default); -} - -.textHighlightSmall { - padding-right: 2px; - padding-left: 2px; - border-radius: 3px; - background-color: var(--blue-highlight-background); - font-size: 13px; - font-weight: 600; - text-transform: capitalize; -} - -.finishedIndicator { - margin-right: 20px; - margin-bottom: 20px; - border-color: var(--disabled-color); - background-image: var(--agenda-indicator-finished); - background-position: 6px 50%; - background-size: 10px; - background-repeat: no-repeat; - color: var(--disabled-color); - display: inline-block; - padding: 5px 8px 5px 20px; - border-style: solid; - border-width: 1px; - border-radius: 3px; - font-size: 12px; - line-height: 8px; - text-align: right; - text-transform: capitalize; -} - -.inProgressIndicator { - margin-right: 20px; - margin-bottom: 20px; - border-color: var(--color-blue-alt); - background-image: var(--agenda-indicator-pending); - background-position: 6px 50%; - background-size: 10px; - background-repeat: no-repeat; - color: var(--color-blue-alt); - display: inline-block; - padding: 5px 8px 5px 20px; - border-style: solid; - border-width: 1px; - border-radius: 3px; - font-size: 12px; - line-height: 8px; - text-align: right; - text-transform: capitalize; -} diff --git a/app/components/views/GovernancePage/Blockchain/AgendaOverview/ProgressIndicator.jsx b/app/components/views/GovernancePage/Blockchain/AgendaOverview/ProgressIndicator.jsx deleted file mode 100644 index 8a5b339d69..0000000000 --- a/app/components/views/GovernancePage/Blockchain/AgendaOverview/ProgressIndicator.jsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FormattedMessage as T } from "react-intl"; -import { Tooltip } from "pi-ui"; -import styles from "./Overview.module.css"; - -// TODO: we should improve this component and use the StatusTag component -// from pi-ui. But since the component is not able to receive React -// nodes as the `text` prop, we should keep this in order to keep the -// internationalization -const ProgressIndicator = ({ passed, inProgress }) => - !inProgress ? ( - - }> -
- -
-
- ) : ( - - }> -
- -
-
- ); - -export default ProgressIndicator; diff --git a/app/components/views/GovernancePage/Blockchain/AgendaOverview/index.js b/app/components/views/GovernancePage/Blockchain/AgendaOverview/index.js new file mode 100644 index 0000000000..670360e2e1 --- /dev/null +++ b/app/components/views/GovernancePage/Blockchain/AgendaOverview/index.js @@ -0,0 +1 @@ +export { default } from "./AgendaOverview"; diff --git a/app/components/views/GovernancePage/Blockchain/Blockchain.jsx b/app/components/views/GovernancePage/Blockchain/Blockchain.jsx index 372d64c773..f0daaf2601 100644 --- a/app/components/views/GovernancePage/Blockchain/Blockchain.jsx +++ b/app/components/views/GovernancePage/Blockchain/Blockchain.jsx @@ -1,28 +1,56 @@ -import { useState } from "react"; -import VotingPrefs from "./Page"; import { find, compose, eq, get } from "fp"; -import { useVotingPrefs } from "./hooks"; - -// TODO this agenda component needs some love. -const VotingPrefsTab = () => { - const [selectedAgenda, setSelectedAgenda] = useState(null); - const { - configuredStakePools, - defaultStakePool, - stakePool, - allAgendas, - onUpdateVotePreference, - onChangeStakePool, - isLoading, - voteChoices - } = useVotingPrefs(); - - const getStakePool = () => { - const pool = onChangeStakePool && stakePool; - return pool - ? configuredStakePools.find(compose(eq(pool.Host), get("Host"))) - : null; - }; +import { useBlockchain } from "./hooks"; +import AgendaOverview from "./AgendaOverview"; +import { PoliteiaLink as PiLink } from "shared"; +import { FormattedMessage as T, defineMessages } from "react-intl"; +import PageHeader from "../PageHeader"; +import styles from "./Blockchain.module.css"; +import { Button, Tooltip } from "pi-ui"; +import { TextInput } from "inputs"; +import { EyeFilterMenu } from "buttons"; +import { useIntl } from "react-intl"; +import { useState, useEffect } from "react"; + +const messages = defineMessages({ + filterByNamePlaceholder: { + id: "blockchain.filterByNamePlaceholder", + defaultMessage: "Filter by Name" + } +}); + +const sortOptions = [ + { + key: "desc", + value: "desc", + label: + }, + { + key: "asc", + value: "asc", + label: + } +]; + +const Blockchain = () => { + const { allAgendas, voteChoices, viewAgendaDetailsHandler } = useBlockchain(); + const [filterByName, setFilterByName] = useState(""); + const [sortBy, setSortBy] = useState(sortOptions[0]); + const [agendas, setAgendas] = useState(allAgendas); + const intl = useIntl(); + const sortByKey = sortBy.key; + + useEffect(() => { + let newAgendas = + sortByKey === "desc" ? [...allAgendas] : [...allAgendas].reverse(); + + if (filterByName.trim() !== "") { + newAgendas = newAgendas.filter( + (agenda) => agenda.name.search(filterByName.trim()) !== -1 + ); + } + + setAgendas(newAgendas); + }, [allAgendas, filterByName, sortByKey]); const getAgendaSelectedChoice = (agenda) => get( @@ -30,25 +58,94 @@ const VotingPrefsTab = () => { find(compose(eq(agenda.name), get(["agendaId"])), voteChoices) ) || "abstain"; - const onShowAgenda = (index) => setSelectedAgenda(index); - - const onCloseAgenda = () => setSelectedAgenda(null); - return ( - + <> +
+ } + description={ + + docs.decred.org + + ) + }} + /> + } + optionalButton={ +
+ + + +
+ } + /> +
+
+
+ setFilterByName(e.target.value)} + /> +
+
+ }> + setSortBy(v)} + /> + +
+
+
+ {agendas.length > 0 ? ( + agendas.map((agenda) => ( + + )) + ) : filterByName ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+ ); }; -export default VotingPrefsTab; +export default Blockchain; diff --git a/app/components/views/GovernancePage/Blockchain/Blockchain.module.css b/app/components/views/GovernancePage/Blockchain/Blockchain.module.css new file mode 100644 index 0000000000..4dc680f411 --- /dev/null +++ b/app/components/views/GovernancePage/Blockchain/Blockchain.module.css @@ -0,0 +1,71 @@ +.headerWrapper { + background-color: var(--governance-tab-bg); +} + +.agendaWrapper { + margin: 20px 60px; + display: flex; + flex-wrap: wrap; +} + +.politeiaButton { + font-size: 13px !important; + line-height: 17px !important; + border-radius: 5px !important; + padding: 6px 10px !important; + margin: 15px 0 0 0 !important; + color: var(--info-modal-button-text) !important; + background-color: var(--politeia-button-bg) !important; + border: 0 !important; + text-decoration: none !important; + font-weight: 600 !important; +} + +.politeiaButton:hover { + opacity: 0.85; +} + +.proposalsLink { + color: var(--link-color) !important; + cursor: pointer; +} + +.searchByNameInput { + display: inline-block; + width: 150px; +} + +.searchByNameInput input::placeholder, +.searchByNameInput input { + font-size: 16px; +} + +.filters { + display: flex; + flex-direction: row; + justify-content: flex-end; + width: 825px; + margin-top: 22px; +} + +.sortByTooltip { + width: max-content; +} + +@media screen and (max-width: 1179px) { + .agendaWrapper { + margin-left: 35px; + } + .filters { + width: 704px; + } +} + +@media screen and (max-width: 768px) { + .agendaWrapper { + margin: 20px; + } + .filters { + width: 367px; + } +} diff --git a/app/components/views/GovernancePage/Blockchain/Page.jsx b/app/components/views/GovernancePage/Blockchain/Page.jsx deleted file mode 100644 index 2248a02a93..0000000000 --- a/app/components/views/GovernancePage/Blockchain/Page.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import AgendaOverview from "./AgendaOverview/AgendaOverview"; -import { ExternalLink } from "shared"; -import { FormattedMessage as T } from "react-intl"; -import { classNames } from "pi-ui"; -import styles from "./VotingPrefs.module.css"; - -const VotingPrefs = ({ - stakePool, - selectedAgenda, - getAgendaSelectedChoice, - onShowAgenda, - onCloseAgenda, - onUpdateVotePreference, - allAgendas, - settingVoteChoices, - isLoading -}) => ( - <> -
-
-
- -
-

- -

-
-
- - - - -
-
-
- {allAgendas.length > 0 ? ( - allAgendas.map((agenda, index) => ( - onShowAgenda(index)} - /> - )) - ) : ( -
- -
- )} -
- -); - -export default VotingPrefs; diff --git a/app/components/views/GovernancePage/Blockchain/VotingPrefs.module.css b/app/components/views/GovernancePage/Blockchain/VotingPrefs.module.css deleted file mode 100644 index 99902215f3..0000000000 --- a/app/components/views/GovernancePage/Blockchain/VotingPrefs.module.css +++ /dev/null @@ -1,73 +0,0 @@ -.agendaWrapper { - margin: 20px 100px; - display: flex; - flex-wrap: wrap; -} - -.header { - background-color: var(--disabled-background-color); - color: var(--modal-text); - padding-left: 103px; - padding-top: 25px; - padding-bottom: 15px; -} - -.title { - font-size: 21px; - line-height: 27px; - margin-bottom: 10px; - color: var(--title-text-and-button-background); -} - -.description { - font-size: 13px; - line-height: 19px; - margin-top: 0; - width: 460px; -} - -.dashboardButton { - font-size: 13px !important; - line-height: 17px !important; - border-radius: 5px !important; - padding: 6px 15px !important; - margin: 4px !important; - color: var(--info-modal-button-text) !important; - background-color: var(--info-modal-button-bg) !important; - border: 0 !important; - text-decoration: none !important; - box-shadow: 0 5px 13px var(--icons-shadow); -} - -.dashboardButton:hover { - background-color: var(--title-text-and-button-background-hovered) !important; -} - -.links { - margin-left: 130px; -} - -.infoButton { - border-radius: 0px; - background-image: var(--tickets-info-icon); - background-position: 50% 50%; - background-size: 20px; - background-repeat: no-repeat; - box-shadow: none; - outline: 0; - display: inline-block; - vertical-align: middle; - width: 20px; - height: 20px; - padding: 3px; - background-color: transparent; - border: 0; - line-height: inherit; - text-decoration: none; - cursor: pointer; - margin: 4px 0; -} - -.infoButton:hover { - opacity: 0.7; -} diff --git a/app/components/views/GovernancePage/Blockchain/hooks.js b/app/components/views/GovernancePage/Blockchain/hooks.js index a6dd634ce2..1eb4f35fc4 100644 --- a/app/components/views/GovernancePage/Blockchain/hooks.js +++ b/app/components/views/GovernancePage/Blockchain/hooks.js @@ -1,34 +1,18 @@ -import * as sel from "selectors"; -import { useCallback } from "react"; import { useSelector, useDispatch } from "react-redux"; -import * as ca from "actions/ClientActions"; -import * as spa from "actions/VSPActions"; +import * as sel from "selectors"; +import * as gov from "actions/GovernanceActions"; -export function useVotingPrefs() { - const dispatch = useDispatch(); - const configuredStakePools = useSelector(sel.configuredStakePools); - const defaultStakePool = useSelector(sel.defaultStakePool); - const stakePool = useSelector(sel.selectedStakePool); +export function useBlockchain() { const allAgendas = useSelector(sel.allAgendas); - const settingVoteChoices = useSelector(sel.setVoteChoicesAttempt); - const settingVspdVoteChoices = useSelector(sel.setVspdVoteChoicesAttempt); const voteChoices = useSelector(sel.voteChoices); - const onUpdateVotePreference = (agendaId, choiceId, passphrase) => - dispatch(ca.setVoteChoicesAttempt(agendaId, choiceId, passphrase)); - const onChangeStakePool = useCallback( - () => dispatch(spa.changeSelectedStakePool), - [dispatch] - ); - const isLoading = settingVoteChoices || settingVspdVoteChoices; + const dispatch = useDispatch(); + const viewAgendaDetailsHandler = (agendaId) => { + return dispatch(gov.viewAgendaDetails(agendaId)); + }; return { - configuredStakePools, - defaultStakePool, - stakePool, allAgendas, - onUpdateVotePreference, - onChangeStakePool, - voteChoices, - isLoading + viewAgendaDetailsHandler, + voteChoices }; } diff --git a/app/components/views/GovernancePage/Blockchain/index.js b/app/components/views/GovernancePage/Blockchain/index.js new file mode 100644 index 0000000000..ffd9034f37 --- /dev/null +++ b/app/components/views/GovernancePage/Blockchain/index.js @@ -0,0 +1 @@ +export { default } from "./Blockchain"; diff --git a/app/components/views/GovernancePage/GovernancePage.jsx b/app/components/views/GovernancePage/GovernancePage.jsx index e751d3d287..ffa8c49fc1 100644 --- a/app/components/views/GovernancePage/GovernancePage.jsx +++ b/app/components/views/GovernancePage/GovernancePage.jsx @@ -1,9 +1,9 @@ import { TabbedPage, TabbedPageTab as Tab, TitleHeader } from "layout"; import { FormattedMessage as T } from "react-intl"; import { Switch, Redirect } from "react-router-dom"; -import ProposalsTab from "./Proposals/ProposalsTab"; -import VotingPrefsTab from "./Blockchain/Blockchain"; -import TabHeader from "./TabHeader/TabHeader"; +import ProposalsTab from "./Proposals"; +import VotingPrefsTab from "./Blockchain"; +import TabHeader from "./TabHeader"; import { GOVERNANCE_ICON } from "constants"; import styles from "./GovernancePage.module.css"; diff --git a/app/components/views/GovernancePage/PageHeader/PageHeader.jsx b/app/components/views/GovernancePage/PageHeader/PageHeader.jsx new file mode 100644 index 0000000000..7f345190a6 --- /dev/null +++ b/app/components/views/GovernancePage/PageHeader/PageHeader.jsx @@ -0,0 +1,11 @@ +import styles from "./PageHeader.module.css"; + +const PageHeader = ({ title, optionalButton, description }) => ( +
+
{title}
+
{description}
+ {optionalButton} +
+); + +export default PageHeader; diff --git a/app/components/views/GovernancePage/PageHeader/PageHeader.module.css b/app/components/views/GovernancePage/PageHeader/PageHeader.module.css new file mode 100644 index 0000000000..004e386bce --- /dev/null +++ b/app/components/views/GovernancePage/PageHeader/PageHeader.module.css @@ -0,0 +1,46 @@ +.header { + color: var(--modal-text); + padding: 30px 0 30px 95px; + width: 824px; + display: flex; + flex-direction: column; +} + +.title { + font-size: 24px; + line-height: 30px; + color: var(--grey-6); + display: flex; + flex-direction: row; + margin-bottom: 5px; + align-items: center; +} + +.description { + font-size: 16px; + line-height: 20px; + margin-top: 0; + color: var(--main-dark-blue); +} + +@media screen and (max-width: 1179px) { + .header { + width: 696px; + padding-left: 55px; + } + + .description { + width: 484px; + } +} + +@media screen and (max-width: 768px) { + .header { + width: 338px; + padding-left: 20px; + } + + .description { + width: 334px; + } +} diff --git a/app/components/views/GovernancePage/PageHeader/index.js b/app/components/views/GovernancePage/PageHeader/index.js new file mode 100644 index 0000000000..c9b885cbfc --- /dev/null +++ b/app/components/views/GovernancePage/PageHeader/index.js @@ -0,0 +1 @@ +export { default } from "./PageHeader"; diff --git a/app/components/views/GovernancePage/Proposals/ProposalsFilter/ProposalsFilter.jsx b/app/components/views/GovernancePage/Proposals/ProposalsFilter/ProposalsFilter.jsx deleted file mode 100644 index 0f47169142..0000000000 --- a/app/components/views/GovernancePage/Proposals/ProposalsFilter/ProposalsFilter.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import { FormattedMessage as T } from "react-intl"; -import { TabsHeader } from "shared"; -import styles from "./ProposalsFilter.module.css"; - -const tabs = [ - { - label: , - value: "finishedVote", - icon: styles.allIcon - }, - { - label: , - value: "approvedVote", - icon: styles.approvedIcon - }, - { - label: , - value: "rejectedVote", - icon: styles.rejectedIcon - } -]; - -const ProposalsFilter = ({ filterTab, setFilterTab }) => { - const onSelectFilterTab = (index) => { - const newTab = tabs[index].value; - setFilterTab(newTab); - }; - - return ( -
- tab.value === filterTab)} - /> -
- ); -}; - -export default ProposalsFilter; diff --git a/app/components/views/GovernancePage/Proposals/ProposalsFilter/ProposalsFilter.module.css b/app/components/views/GovernancePage/Proposals/ProposalsFilter/ProposalsFilter.module.css deleted file mode 100644 index 5d84c3f554..0000000000 --- a/app/components/views/GovernancePage/Proposals/ProposalsFilter/ProposalsFilter.module.css +++ /dev/null @@ -1,47 +0,0 @@ -.tabs { - width: 800px; - margin-left: 85px; - height: 40px; -} - -.tabs > div > ul { - justify-content: right; - position: relative; - right: -20px; - width: fit-content; - margin-left: auto; - top: 0; -} - -.tabs > div { - background-color: transparent; - padding-top: 0px; -} - -@media screen and (max-width: 1179px) { - .tabs { - margin-left: 0; - width: 700px; - } -} - -@media screen and (max-width: 768px) { - .tabs { - width: 100%; - } - - .allIcon { - background-image: var(--ticket-live-icon); - } - - .approvedIcon { - background-image: var(--ticket-voted-icon); - } - - .rejectedIcon { - background-image: var(--ticket-revoked-icon); - } - .tabs > div > ul { - left: 0px; - } -} diff --git a/app/components/views/GovernancePage/Proposals/ProposalsList/ProposalsList.jsx b/app/components/views/GovernancePage/Proposals/ProposalsList/ProposalsList.jsx index eb03b954cf..0e398815f9 100644 --- a/app/components/views/GovernancePage/Proposals/ProposalsList/ProposalsList.jsx +++ b/app/components/views/GovernancePage/Proposals/ProposalsList/ProposalsList.jsx @@ -1,14 +1,30 @@ import { PoliteiaLoading, NoProposals } from "indicators"; import InfiniteScroll from "react-infinite-scroller"; -import ProposalsListItem from "../ProposalsListItem/ProposalsListItem"; +import ProposalsListItem from "../ProposalsListItem"; import { useProposalsList } from "../hooks"; import { LoadingError } from "shared"; import styles from "./ProposalsList.module.css"; import { useCallback, useLayoutEffect, useState, useEffect } from "react"; -import ProposalsFilter from "../ProposalsFilter/ProposalsFilter"; +import { EyeFilterMenu } from "buttons"; +import { FormattedMessage as T } from "react-intl"; + +const getProposalTypes = () => [ + { + key: "finishedVote", + label: + }, + { + key: "approvedVote", + label: + }, + { + key: "rejectedVote", + label: + } +]; const ProposalsList = ({ finishedVote, tab }) => { - const [filterTab, setFilterTab] = useState(tab); + const [selectedFilter, setSelectedFilter] = useState(tab); const { getProposalError, inventoryError, @@ -17,11 +33,7 @@ const ProposalsList = ({ finishedVote, tab }) => { proposals, state, send - } = useProposalsList(filterTab); - - const handleSetFilterTab = (tab) => { - setFilterTab(tab); - }; + } = useProposalsList(selectedFilter); // This part of the code is meant to solve the situation when the window // is too tall, and the user can not trigger `loadMore` with scrolling. @@ -55,37 +67,37 @@ const ProposalsList = ({ finishedVote, tab }) => { ); case "success": return proposals && - proposals[filterTab] && - proposals[filterTab].length ? ( -
+ proposals[selectedFilter] && + proposals[selectedFilter].length ? ( + <> {tab === "finishedVote" && ( - - )} - -
- {proposals[filterTab].map((v) => ( - - ))} +
+ setSelectedFilter(type.key)} + />
- -
+ )} +
+ +
+ {proposals[selectedFilter].map((v) => ( + + ))} +
+
+
+ ) : ( ); diff --git a/app/components/views/GovernancePage/Proposals/ProposalsList/ProposalsList.module.css b/app/components/views/GovernancePage/Proposals/ProposalsList/ProposalsList.module.css index 2b132c7ac9..c04df9ec71 100644 --- a/app/components/views/GovernancePage/Proposals/ProposalsList/ProposalsList.module.css +++ b/app/components/views/GovernancePage/Proposals/ProposalsList/ProposalsList.module.css @@ -13,7 +13,7 @@ .proposalList { margin-top: 15px; - margin-left: 85px; + margin-left: 60px; padding-bottom: 50px; display: block; } @@ -26,13 +26,34 @@ height: calc(100% - 50px); } -@media screen and (max-width: 768px) { - .proposalList { - padding-bottom: 100px; - } +.filters { + display: flex; + flex-direction: row; + justify-content: flex-end; + width: 825px; + margin-top: 22px; +} + +.scrollWrapper { + height: calc(100% - 54px); + overflow: auto; } + @media screen and (max-width: 1179px) { .proposalList { margin-left: 35px; } + .filters { + width: 704px; + } +} + +@media screen and (max-width: 768px) { + .proposalList { + padding-bottom: 100px; + margin-left: 10px; + } + .filters { + width: 367px; + } } diff --git a/app/components/views/GovernancePage/Proposals/ProposalsList/index.js b/app/components/views/GovernancePage/Proposals/ProposalsList/index.js new file mode 100644 index 0000000000..40221dc141 --- /dev/null +++ b/app/components/views/GovernancePage/Proposals/ProposalsList/index.js @@ -0,0 +1 @@ +export { default } from "./ProposalsList"; diff --git a/app/components/views/GovernancePage/Proposals/ProposalsListItem/CardWrapper/CardWrapper.jsx b/app/components/views/GovernancePage/Proposals/ProposalsListItem/CardWrapper/CardWrapper.jsx new file mode 100644 index 0000000000..2e2d852a5f --- /dev/null +++ b/app/components/views/GovernancePage/Proposals/ProposalsListItem/CardWrapper/CardWrapper.jsx @@ -0,0 +1,13 @@ +import { classNames } from "pi-ui"; +import styles from "./CardWrapper.module.css"; + +const CardWrapper = ({ onClick, className, children }) => ( +
+ {children} +
+
+
+
+); + +export default CardWrapper; diff --git a/app/components/views/GovernancePage/Proposals/ProposalsListItem/CardWrapper/CardWrapper.module.css b/app/components/views/GovernancePage/Proposals/ProposalsListItem/CardWrapper/CardWrapper.module.css new file mode 100644 index 0000000000..47d2e5c016 --- /dev/null +++ b/app/components/views/GovernancePage/Proposals/ProposalsListItem/CardWrapper/CardWrapper.module.css @@ -0,0 +1,46 @@ +.cardWrapper { + display: flex; + cursor: pointer; + margin-bottom: 5px; +} + +.cardWrapper:hover { + filter: drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.16)); +} + +.continueButton { + width: 40px; + background-color: var(--governance-nav-button-bg); + cursor: pointer; +} + +.cardWrapper:hover .continueButton { + background-color: var(--grey-5); +} + +.continueArrow { + height: 10px; + width: 10px; + background-image: var(--menu-arrow-up); + background-position: 50% 5px; + background-repeat: no-repeat; + transform: rotate(90deg); +} + +.cardWrapper:hover .continueArrow { + background-image: var(--arrow-left-white); + background-position: 50% 0; + transform: rotate(180deg); +} + +.cardWrapper.ended.passed { + border-left: 2px solid var(--vote-yes-color); +} + +.cardWrapper.ended.declined { + border-left: 2px solid var(--vote-no-color); +} + +.cardWrapper.ended .proposalName { + color: var(--stroke-color-hovered); +} diff --git a/app/components/views/GovernancePage/Proposals/ProposalsListItem/CardWrapper/index.js b/app/components/views/GovernancePage/Proposals/ProposalsListItem/CardWrapper/index.js new file mode 100644 index 0000000000..a48871930a --- /dev/null +++ b/app/components/views/GovernancePage/Proposals/ProposalsListItem/CardWrapper/index.js @@ -0,0 +1 @@ +export { default } from "./CardWrapper"; diff --git a/app/components/views/GovernancePage/Proposals/ProposalsListItem/ProposalsListItem.jsx b/app/components/views/GovernancePage/Proposals/ProposalsListItem/ProposalsListItem.jsx index 65d3c44031..e0cdce89f0 100644 --- a/app/components/views/GovernancePage/Proposals/ProposalsListItem/ProposalsListItem.jsx +++ b/app/components/views/GovernancePage/Proposals/ProposalsListItem/ProposalsListItem.jsx @@ -1,10 +1,9 @@ -import { FormattedMessage as T } from "react-intl"; -import { VotingProgress } from "indicators"; import { PROPOSAL_VOTING_ACTIVE, PROPOSAL_VOTING_FINISHED } from "constants"; -import { FormattedRelative } from "shared"; import { classNames } from "pi-ui"; import { useProposalsListItem } from "../hooks"; import styles from "./ProposalsListItem.module.css"; +import ProposalCard from "../../../ProposalDetailsPage/helpers/ProposalCard"; +import CardWrapper from "./CardWrapper"; const ProposalsListItem = ({ name, @@ -12,88 +11,78 @@ const ProposalsListItem = ({ token, voteCounts, voteStatus, - currentVoteChoice, - quorumPass, voteResult, modifiedSinceLastAccess, votingSinceLastAccess, quorumMinimumVotes, finishedVote, linkto, - linkedfrom, - approved + approved, + totalVotes, + endTimestamp, + blocksLeft, + creator, + proposalStatus, + version }) => { - const { viewProposalDetailsHandler, tsDate } = useProposalsListItem(token); + const { + viewProposalDetailsHandler, + tsDate, + isTestnet, + isDarkTheme, + linkedProposal + } = useProposalsListItem(token); const isVoting = voteStatus === PROPOSAL_VOTING_ACTIVE; - const isVotingFinished = voteStatus === PROPOSAL_VOTING_FINISHED; const isModified = (!isVoting && modifiedSinceLastAccess) || (isVoting && votingSinceLastAccess); + + const shortToken = token.substring(0, 7); + const shortRFPToken = linkedProposal?.token.substring(0, 7); + const proposalPath = `/record/${shortToken}`; + const isVoteActive = voteStatus === PROPOSAL_VOTING_ACTIVE; + const isVoteActiveOrFinished = + isVoteActive || voteStatus === PROPOSAL_VOTING_FINISHED; + return ( -
-
-
-
{name}
- {linkedfrom && ( -
- -
- )} - {linkto && ( -
- -
- )} -
-
{token.substring(0, 7)}
-
-
- {(isVoting || isVotingFinished) && ( -
-
- -
- )} - {!isVotingFinished ? ( -
- - }} - /> -
- ) : ( -
- {quorumPass ? ( - linkto && !approved && voteResult !== "declined" ? ( - - ) : ( - voteResult - ) - ) : ( - - )} -
- )} -
-
+ + ); }; diff --git a/app/components/views/GovernancePage/Proposals/ProposalsListItem/ProposalsListItem.module.css b/app/components/views/GovernancePage/Proposals/ProposalsListItem/ProposalsListItem.module.css index 91ec50de21..c4c6a763e2 100644 --- a/app/components/views/GovernancePage/Proposals/ProposalsListItem/ProposalsListItem.module.css +++ b/app/components/views/GovernancePage/Proposals/ProposalsListItem/ProposalsListItem.module.css @@ -1,3 +1,7 @@ +.overview { + padding-left: 15px !important; +} + .listItem { padding: 18px 16px 6px 20px; border-left: 2px solid var(--background-back-color); diff --git a/app/components/views/GovernancePage/Proposals/ProposalsListItem/index.js b/app/components/views/GovernancePage/Proposals/ProposalsListItem/index.js new file mode 100644 index 0000000000..cf68692ba3 --- /dev/null +++ b/app/components/views/GovernancePage/Proposals/ProposalsListItem/index.js @@ -0,0 +1 @@ +export { default } from "./ProposalsListItem"; diff --git a/app/components/views/GovernancePage/Proposals/ProposalsTab.jsx b/app/components/views/GovernancePage/Proposals/ProposalsTab.jsx index 80060e5c75..525e22621e 100644 --- a/app/components/views/GovernancePage/Proposals/ProposalsTab.jsx +++ b/app/components/views/GovernancePage/Proposals/ProposalsTab.jsx @@ -1,53 +1,14 @@ import { FormattedMessage as T } from "react-intl"; import { createElement as h } from "react"; -import { Button, Tooltip, classNames } from "pi-ui"; -import ProposalsList from "./ProposalsList/ProposalsList"; +import { Button, Tooltip } from "pi-ui"; +import ProposalsList from "./ProposalsList"; import PoliteiaDisabled from "./PoliteiaDisabled"; import { PoliteiaLink as PiLink } from "shared"; import { TabbedPage, TabbedPageTab as Tab } from "layout"; import { useProposalsTab } from "./hooks"; import styles from "./ProposalsTab.module.css"; - -const PageHeader = ({ isTestnet, onRefreshProposals }) => ( -
- {/* TODO: wrapp this 'header' in a component same header is used in VotingPrefs.jsx */} -
-
- -
-
- - proposals.decred.org - - ) - }} - /> -
-
-
- - - - - } - placement="left"> -
- -
-
-); +import PageHeader from "../PageHeader"; +import { SmallButton } from "buttons"; const ListLink = ({ count, children }) => ( <> @@ -72,19 +33,68 @@ const ProposalsTab = () => { } return ( } header={ - + +
+ +
+
+ + } + placement="right"> + + +
+ + } + description={ + + proposals.decred.org + + ) + }} + /> + } + optionalButton={ +
+ + + +
+ } + /> } headerClassName={styles.tabsHeader} tabsClassName={styles.tabs} - tabContentClassName={styles.tabContent}> + tabContentClassName={styles.tabContent} + activeCaretClassName={styles.activeCaret}> @@ -96,7 +106,6 @@ const ProposalsTab = () => { component={h(ProposalsList, { tab })} key="activevote" className={styles.tab} - activeClassName={styles.activeTab} link={ @@ -108,7 +117,6 @@ const ProposalsTab = () => { component={h(ProposalsList, { finishedVote: true, tab })} key="activevote" className={styles.tab} - activeClassName={styles.activeTab} link={} /> { component={h(ProposalsList, { tab })} key="abandoned" className={styles.tab} - activeClassName={styles.activeTab} link={} />
diff --git a/app/components/views/GovernancePage/Proposals/ProposalsTab.module.css b/app/components/views/GovernancePage/Proposals/ProposalsTab.module.css index 2f22246785..2e7581b238 100644 --- a/app/components/views/GovernancePage/Proposals/ProposalsTab.module.css +++ b/app/components/views/GovernancePage/Proposals/ProposalsTab.module.css @@ -9,63 +9,19 @@ font-size: 9pt; } -.header { - background-color: var(--disabled-background-color); - color: var(--modal-text); - padding-left: 103px; - padding-top: 25px; - padding-bottom: 15px; -} - .tabsHeader { + background-color: var(--governance-tab-bg); padding: 0; } .tabs { - position: relative; - width: 100%; - background-color: var(--tabbed-page-header-bg); - margin-left: 0; - margin-top: 0; - padding: 3px 100px; -} - -.tab { - margin-right: 20px; -} - -.tab a { - margin: 0; - border-radius: 5px; -} - -.tab a span { - color: var(--tabbed-page-header-text); -} - -.activeTab { - background-color: var(--tabbed-page-header-active-bg); - padding: 3px 10px 3px 10px; + margin: 0 0 0 95px; } .tabContent { padding: 0; } -.title { - font-size: 21px; - line-height: 27px; - margin-bottom: 10px; - color: var(--title-text-and-button-background); -} - -.description { - font-size: 13px; - line-height: 19px; - margin-top: 0; - width: 560px; -} - .links { margin-left: 40px; } @@ -74,31 +30,32 @@ font-size: 13px !important; line-height: 17px !important; border-radius: 5px !important; - padding: 6px 15px !important; - margin: 4px !important; + padding: 6px 10px !important; + margin: 15px 0 0 0 !important; color: var(--info-modal-button-text) !important; - background-color: var(--info-modal-button-bg) !important; + background-color: var(--politeia-button-bg) !important; border: 0 !important; text-decoration: none !important; - box-shadow: 0 5px 13px var(--icons-shadow); + font-weight: 600 !important; } .politeiaButton:hover { - background-color: var(--title-text-and-button-background-hovered) !important; + opacity: 0.85; } .refreshProposals { - width: 24px; - height: 24px; - border: none; - outline: none; - background-color: transparent; background-image: var(--proposals-refresh-icon); - background-size: 24px 24px; - cursor: pointer; - position: relative; - top: 6px; - left: 6px; + background-color: var(--refresh-proposals) !important; + margin-left: 10px; +} + +.refreshProposalsTooltip { + display: flex !important; +} + +.refreshProposalsTooltipContent { + width: max-content; + font-size: 16px; } .politeiaDisabled { @@ -110,23 +67,33 @@ } .proposalsLink { - color: var(--button-text) !important; + color: var(--link-color) !important; + cursor: pointer; +} + +.activeCaret { + background-color: var(--grey-7) !important; } @media screen and (max-width: 1179px) { .tabs { - padding-left: 35px; + margin-left: 55px; } .politeiaDisabled { padding-left: 20px; } - .header { - padding-left: 35px; - } - .links { margin-left: 44px; } } + +@media screen and (max-width: 768px) { + .tabs { + margin-left: 20px; + } + .tab { + margin-right: 25px; + } +} diff --git a/app/components/views/GovernancePage/Proposals/hooks.js b/app/components/views/GovernancePage/Proposals/hooks.js index 325ebfa041..1b54337513 100644 --- a/app/components/views/GovernancePage/Proposals/hooks.js +++ b/app/components/views/GovernancePage/Proposals/hooks.js @@ -1,4 +1,4 @@ -import { useState, useEffect, useReducer, useCallback } from "react"; +import { useState, useEffect, useReducer, useCallback, useMemo } from "react"; import { useSelector, useDispatch } from "react-redux"; import { fetchMachine } from "stateMachines/FetchStateMachine"; import { useMachine } from "@xstate/react"; @@ -6,6 +6,7 @@ import * as sel from "selectors"; import * as gov from "actions/GovernanceActions"; import { usePrevious } from "hooks"; import { setLastPoliteiaAccessTime } from "actions/WalletLoaderActions"; +import { useTheme, DEFAULT_DARK_THEME_NAME } from "pi-ui"; const MAX_PAGE_SIZE = 20; // TODO: Get proposallistpagesize from politeia's request: /v1/policy @@ -43,10 +44,36 @@ export function useProposalsTab() { export function useProposalsListItem(token) { const tsDate = useSelector((state) => sel.tsDate(state)); + const isTestnet = useSelector(sel.isTestNet); + const { themeName } = useTheme(); + const isDarkTheme = themeName === DEFAULT_DARK_THEME_NAME; + + const proposals = useSelector(sel.proposals); + const proposalsDetails = useSelector(sel.proposalsDetails); + const viewedProposalDetails = useMemo(() => proposalsDetails[token], [ + token, + proposalsDetails + ]); + + const linkedProposal = useMemo( + () => + viewedProposalDetails?.linkto && + proposals.finishedVote.find( + (proposal) => viewedProposalDetails.linkto === proposal.token + ), + [proposals, viewedProposalDetails] + ); + const dispatch = useDispatch(); const viewProposalDetailsHandler = () => dispatch(gov.viewProposalDetails(token)); - return { tsDate, viewProposalDetailsHandler }; + return { + tsDate, + viewProposalDetailsHandler, + isTestnet, + isDarkTheme, + linkedProposal + }; } export function useProposalsList(tab) { diff --git a/app/components/views/GovernancePage/Proposals/index.js b/app/components/views/GovernancePage/Proposals/index.js new file mode 100644 index 0000000000..7a0716cabf --- /dev/null +++ b/app/components/views/GovernancePage/Proposals/index.js @@ -0,0 +1 @@ +export { default } from "./ProposalsTab"; diff --git a/app/components/views/GovernancePage/TabHeader/TabHeader.jsx b/app/components/views/GovernancePage/TabHeader/TabHeader.jsx index 4a1f59c7b5..6df0274ecd 100644 --- a/app/components/views/GovernancePage/TabHeader/TabHeader.jsx +++ b/app/components/views/GovernancePage/TabHeader/TabHeader.jsx @@ -3,8 +3,9 @@ import { DescriptionHeader } from "layout"; import { useTreasuryInfo } from "../hooks"; import { FormattedMessage as T } from "react-intl"; import styles from "./TabHeader.module.css"; +import { classNames } from "pi-ui"; -const TabHeader = () => { +const TabHeader = ({ descriptionHeaderClassName }) => { const { treasuryBalance } = useTreasuryInfo(); return ( <> @@ -12,7 +13,11 @@ const TabHeader = () => { description={ } - className={styles.descriptionHeader} + className={classNames( + styles.descriptionHeader, + styles.descriptionHeaderMain, + descriptionHeaderClassName + )} /> {treasuryBalance && ( { }} /> } - className={styles.descriptionHeader} + className={classNames( + styles.descriptionHeader, + descriptionHeaderClassName + )} /> )} diff --git a/app/components/views/GovernancePage/TabHeader/TabHeader.module.css b/app/components/views/GovernancePage/TabHeader/TabHeader.module.css index 8e9385792b..f30ba364c3 100644 --- a/app/components/views/GovernancePage/TabHeader/TabHeader.module.css +++ b/app/components/views/GovernancePage/TabHeader/TabHeader.module.css @@ -1,11 +1,16 @@ +.descriptionHeaderMain { + margin-bottom: 10px; + margin-top: 5px; +} + .balanceAmount { display: inline; - font-size: 11px; + font-size: 16px; font-weight: 600; line-height: 14px; - padding: 3px; + padding: 4px 10px; color: var(--input-color-default); - background-color: var(--blue-highlight-background); + background-color: var(--governance-header-balance-bg); margin-left: 5px; margin-right: 5px; border-radius: 3px; diff --git a/app/components/views/GovernancePage/TabHeader/index.js b/app/components/views/GovernancePage/TabHeader/index.js new file mode 100644 index 0000000000..28c669db1f --- /dev/null +++ b/app/components/views/GovernancePage/TabHeader/index.js @@ -0,0 +1 @@ +export { default } from "./TabHeader"; diff --git a/app/components/views/ProposalDetailsPage/ProposalDetails.jsx b/app/components/views/ProposalDetailsPage/ProposalDetails.jsx index 9789f8afae..06e3683ecf 100644 --- a/app/components/views/ProposalDetailsPage/ProposalDetails.jsx +++ b/app/components/views/ProposalDetailsPage/ProposalDetails.jsx @@ -1,19 +1,10 @@ -import { classNames, Button, StatusBar, Tooltip, Text, StatusTag } from "pi-ui"; +import { Button, classNames } from "pi-ui"; import { FormattedMessage as T } from "react-intl"; import { PoliteiaLink } from "shared"; -import { - Event, - VOTE_ENDS_EVENT, - VOTE_ENDED_EVENT, - PROPOSAL_UPDATED_EVENT, - ProposalBody, - VoteSection, - Join -} from "./helpers"; -import { getStatusBarData, getProposalStatusTagProps } from "./utils"; -import { PROPOSAL_VOTING_ACTIVE, PROPOSAL_VOTING_FINISHED } from "constants"; +import { ProposalBody, VoteSection, ProposalCard } from "./helpers"; import { useProposalDetails } from "./hooks"; import styles from "./ProposalDetails.module.css"; +import { PROPOSAL_VOTING_ACTIVE, PROPOSAL_VOTING_FINISHED } from "constants"; const ProposalDetails = ({ viewedProposalDetails, @@ -44,9 +35,9 @@ const ProposalDetails = ({ linkedProposal, isDarkTheme }) => { - const { tsDate, hasTickets, isTestnet } = useProposalDetails(); const shortToken = token.substring(0, 7); const shortRFPToken = linkedProposal?.token.substring(0, 7); + const { tsDate, hasTickets, isTestnet } = useProposalDetails(); const proposalPath = `/record/${shortToken}`; const isVoteActive = voteStatus === PROPOSAL_VOTING_ACTIVE; const isVoteActiveOrFinished = @@ -54,117 +45,39 @@ const ProposalDetails = ({ return (
-
+
-
-
-
-
- - }> - - {name} - - - - {creator} - - - {" "} - {version} - - -
-
- -
- {shortToken} -
-
-
- {linkedProposal && ( -
- - {`${linkedProposal.name} (${shortRFPToken})`} - - ) - }} - /> -
- )} -
- {isVoteActiveOrFinished && ( -
- - - {totalVotes} - - - /{`${quorumMinimumVotes} votes`} - - - } - /> -
- - {isVoteActive && ( -
- {blocksLeft}{" "} - -
- )} -
-
- )} -
+
{isVoteActiveOrFinished && ( ( + className={classNames( + styles.separator, + "margin-left-xs", + "margin-right-xs" + )}> • ); diff --git a/app/components/views/ProposalDetailsPage/helpers/ProposalCard/ProposalCard.jsx b/app/components/views/ProposalDetailsPage/helpers/ProposalCard/ProposalCard.jsx new file mode 100644 index 0000000000..87aaa88931 --- /dev/null +++ b/app/components/views/ProposalDetailsPage/helpers/ProposalCard/ProposalCard.jsx @@ -0,0 +1,156 @@ +import styles from "./ProposalCard.module.css"; +import { classNames, StatusBar, Tooltip, Text, StatusTag } from "pi-ui"; +import { FormattedMessage as T } from "react-intl"; +import { PoliteiaLink } from "shared"; +import { + Event, + VOTE_ENDS_EVENT, + VOTE_ENDED_EVENT, + PROPOSAL_UPDATED_EVENT, + Join +} from "../"; +import { getStatusBarData, getProposalStatusTagProps } from "../../utils"; + +const ProposalCard = ({ + isTestnet, + linkto, + approved, + totalVotes, + endTimestamp, + blocksLeft, + name, + creator, + timestamp, + tsDate, + version, + proposalStatus, + voteStatus, + isDarkTheme, + linkedProposal, + quorumMinimumVotes, + voteCounts, + shortToken, + shortRFPToken, + proposalPath, + isVoteActive, + isVoteActiveOrFinished, + isCardClickable, + className +}) => ( +
+
+
+
+ {isCardClickable ? ( +
{name}
+ ) : ( +
+ } + contentClassName={styles.tooltipTitle} + placement="right"> + +
{name}
+
+
+
+ )} + + + {creator} + + + + {version} + + +
+
+ +
+ {shortToken} +
+
+
+ {linkedProposal && ( +
+ {`${linkedProposal.name} (${shortRFPToken})`} + ) : ( + + {`${linkedProposal.name} (${shortRFPToken})`} + + ) + }} + /> +
+ )} +
+ {isVoteActiveOrFinished && ( +
+ + + {totalVotes} + + + /{`${quorumMinimumVotes} votes`} + + + } + /> +
+ + {isVoteActive && ( +
+ {blocksLeft}{" "} + +
+ )} +
+
+ )} +
+); + +export default ProposalCard; diff --git a/app/components/views/ProposalDetailsPage/helpers/ProposalCard/ProposalCard.module.css b/app/components/views/ProposalDetailsPage/helpers/ProposalCard/ProposalCard.module.css new file mode 100644 index 0000000000..666c85b9f6 --- /dev/null +++ b/app/components/views/ProposalDetailsPage/helpers/ProposalCard/ProposalCard.module.css @@ -0,0 +1,164 @@ +.overview { + background-color: var(--background-back-color); + width: 724px; +} + +.overviewInfo { + padding: 20px 40px 20px 20px; +} + +.row { + display: flex; + justify-content: space-between; +} + +.titleText { + font-size: 18px; + line-height: 23px; + font-weight: 600; + color: var(--tutorial-header) !important; + letter-spacing: 0em; +} + +.title { + cursor: pointer; +} + +.subTitle { + font-size: 16px; +} + +.creator { + font-weight: 600; +} + +.tooltipTitle { + width: max-content; +} + +.updatedEvent, +.version { + color: var(--grey-5); +} + +.token { + padding: 4px 10px; + background-color: var(--color-blue-lighter); + border-radius: 5px; + font-size: 16px; +} + +.token.dark { + color: var(--grey-2); +} + +.proposedToRfp { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: var(--stroke-color-default); +} + +.proposedToRfp a { + font-size: 13px; + line-height: 19px; + color: var(--overview-balance-label); + text-decoration: none; + cursor: pointer; +} + +.proposedToRfp span { + font-size: 13px; + line-height: 19px; + color: var(--overview-balance-label); + text-decoration: none; +} + +.voteStatusBar { + flex-basis: 75%; + margin-right: 10px; +} + +.statusBarRow { + padding: 0 40px 20px 20px; +} + +.voteEnd { + align-items: flex-end; + flex-basis: 25%; + margin-top: -0.3rem; + color: var(--grey-5); + font-size: 13px; +} + +.quorumTooltip { + white-space: nowrap; + font-size: var(--font-size-small); + color: var(--color-gray); +} + +.darkQuorumTooltip { + color: var(--text-color); +} + +.votesQuorum { + color: var(--text-secondary-color) !important; +} + +.votesReceived { + color: var(--tab-text-active-color) !important; +} + +@media screen and (max-width: 1179px) { + .overview { + width: 626px; + } + + .overviewInfo { + padding: 20px; + } + + .statusBarRow { + padding-right: 20px; + } +} + +@media screen and (max-width: 768px) { + .overview { + width: 315px; + padding: 10px !important; + } + + .overviewInfo { + padding-left: 0; + padding-right: 10px; + } + + .votingProgress { + width: 315px; + padding-left: 20px; + } + + .overviewVoting { + width: 315px; + } + + .row { + flex-direction: column; + } + + .subTitle { + margin-bottom: 10px; + } + + .statusBarRow { + flex-direction: column; + padding-bottom: 0; + padding-left: 0; + padding-right: 10px; + } + + .voteEnd { + margin-top: 10px; + } +} diff --git a/app/components/views/ProposalDetailsPage/helpers/ProposalCard/index.js b/app/components/views/ProposalDetailsPage/helpers/ProposalCard/index.js new file mode 100644 index 0000000000..6fc46519de --- /dev/null +++ b/app/components/views/ProposalDetailsPage/helpers/ProposalCard/index.js @@ -0,0 +1 @@ +export { default } from "./ProposalCard"; diff --git a/app/components/views/ProposalDetailsPage/helpers/VotePreferenceWrapper/VotePreference/VotePreference.module.css b/app/components/views/ProposalDetailsPage/helpers/VotePreferenceWrapper/VotePreference/VotePreference.module.css index a99ee42ba4..15b00c744d 100644 --- a/app/components/views/ProposalDetailsPage/helpers/VotePreferenceWrapper/VotePreference/VotePreference.module.css +++ b/app/components/views/ProposalDetailsPage/helpers/VotePreferenceWrapper/VotePreference/VotePreference.module.css @@ -47,7 +47,7 @@ @media screen and (max-width: 1179px) { .votePreference { - flex-basis: 70%; + flex-basis: initial; } .voteButton { diff --git a/app/components/views/ProposalDetailsPage/helpers/VoteSection/VoteSection.module.css b/app/components/views/ProposalDetailsPage/helpers/VoteSection/VoteSection.module.css index eab41d70d6..c758ea6d94 100644 --- a/app/components/views/ProposalDetailsPage/helpers/VoteSection/VoteSection.module.css +++ b/app/components/views/ProposalDetailsPage/helpers/VoteSection/VoteSection.module.css @@ -1,7 +1,7 @@ .voteSection { background-color: var(--background-back-color); padding: 10px 40px; - width: 740px; + width: 764px; margin-top: 5px; display: flex; flex-direction: row; @@ -21,6 +21,6 @@ @media screen and (max-width: 768px) { .voteSection { - width: 100%; + width: 355px; } } diff --git a/app/components/views/ProposalDetailsPage/helpers/index.js b/app/components/views/ProposalDetailsPage/helpers/index.js index 07eb1d53fe..91ca2d5a0d 100644 --- a/app/components/views/ProposalDetailsPage/helpers/index.js +++ b/app/components/views/ProposalDetailsPage/helpers/index.js @@ -16,3 +16,4 @@ export { default as NoTicketsMsg } from "./NoTicketsMsg"; export { default as Join } from "./Join"; export { default as NotVotingMsg } from "./NotVotingMsg"; export { default as NotTicketsMsg } from "./NoTicketsMsg"; +export { default as ProposalCard } from "./ProposalCard"; diff --git a/app/containers/Wallet/Wallet.jsx b/app/containers/Wallet/Wallet.jsx index bd53211744..7f0e4e9849 100644 --- a/app/containers/Wallet/Wallet.jsx +++ b/app/containers/Wallet/Wallet.jsx @@ -14,6 +14,7 @@ import TicketsPage from "components/views/TicketsPage/TicketsPage"; import TutorialsPage from "components/views/TutorialsPage/TutorialsPage"; import GovernancePage from "components/views/GovernancePage/GovernancePage"; import ProposalDetailsPage from "components/views/ProposalDetailsPage/ProposalDetailsPage"; +import AgendaDetailsPage from "components/views/AgendaDetailsPage"; import TrezorPage from "components/views/TrezorPage"; import LNPage from "components/views/LNPage"; import DexPage from "components/views/DexPage"; @@ -67,6 +68,7 @@ const Wallet = ({ setInterval }) => { path="/proposal/details/:token" component={ProposalDetailsPage} /> + ); diff --git a/app/style/themes/darkTheme.js b/app/style/themes/darkTheme.js index ec8fe9da22..e6cb803d60 100644 --- a/app/style/themes/darkTheme.js +++ b/app/style/themes/darkTheme.js @@ -150,6 +150,12 @@ const darkTheme = { "stakeinfo-value": "#e9f8fe", "stakeinfo-border": "#608ace", "purchase-label": "#b7deee", + "governance-tab-bg": "#1F325F", + "governance-header-balance-bg": "var(--blue-highlight-background)", + "politeia-button-bg": "#7DA7D9", + "governance-nav-button-bg": "#283f77", + "refresh-proposals": "var(--politeia-button-bg)", + "agenda-preference": "var(--grey-2)", // override pi-ui's toggle default dark background "toggle-bar-color": "var(--background-copy-color)", diff --git a/app/style/themes/lightTheme.js b/app/style/themes/lightTheme.js index 97c287e955..b61df9f4ec 100644 --- a/app/style/themes/lightTheme.js +++ b/app/style/themes/lightTheme.js @@ -153,6 +153,12 @@ const lightTheme = { "stakeinfo-value": "var(--main-dark-blue)", "stakeinfo-border": "var(--grey-3)", "purchase-label": "var(--grey-7)", + "governance-tab-bg": "var(--grey-3)", + "governance-header-balance-bg": "var(--color-blue-lighter)", + "politeia-button-bg": "var(--accent-blue)", + "governance-nav-button-bg": "var(--grey-3)", + "refresh-proposals": "var(--small-button-bg)", + "agenda-preference": "var(--main-dark-blue)", // override pi-ui's tab colors "tab-default-color": "transparent", // default border diff --git a/test/unit/components/views/AgendaDetailsPage/AgendaDetailsPage.spec.js b/test/unit/components/views/AgendaDetailsPage/AgendaDetailsPage.spec.js new file mode 100644 index 0000000000..f834ceeeb4 --- /dev/null +++ b/test/unit/components/views/AgendaDetailsPage/AgendaDetailsPage.spec.js @@ -0,0 +1,173 @@ +import AgendaDetailsPage from "components/views/AgendaDetailsPage"; +import { render } from "test-utils.js"; +import { screen } from "@testing-library/react"; +import user from "@testing-library/user-event"; + +let mockAllAgendas; +let mockVoteChoices; +const mockViewAgendaDetailsHandler = jest.fn(); +const mockSetSelectedChoice = jest.fn(); +let mockIsLoading = false; + +const mockChoices = [ + { choiceId: "abstain" }, + { choiceId: "no" }, + { choiceId: "yes" } +]; +let mockSelectedChoice; + +jest.mock("components/views/GovernancePage/Blockchain/hooks", () => ({ + useBlockchain: () => { + return { + allAgendas: mockAllAgendas, + voteChoices: mockVoteChoices, + viewAgendaDetailsHandler: mockViewAgendaDetailsHandler + }; + } +})); + +jest.mock("components/views/AgendaDetailsPage/hooks", () => ({ + useAgendaDetails: () => ({ + allAgendas: mockAllAgendas, + agenda: mockAllAgendas[0], + voteChoices: mockVoteChoices, + choices: mockChoices, + selectedChoice: mockSelectedChoice, + newSelectedChoice: mockSelectedChoice, + setNewSelectedChoice: mockSetSelectedChoice, + isLoading: mockIsLoading + }) +})); + +const testAgendaCardElements = ( + mockAgendaName, + mockDescription, + mockChoice, + expectedTooltipText, + expectedStatusText +) => { + expect(screen.getAllByText(mockAgendaName).length).toBe(2); //one title and one agenda ID + expect(screen.getByText("Agenda ID:").parentNode.textContent).toMatch( + `Agenda ID: ${mockAgendaName}` + ); + expect(screen.getByText("Preference:").parentNode.textContent).toMatch( + `Preference: ${mockChoice}` + ); + expect(screen.getByText(expectedTooltipText)).toBeInTheDocument(); + expect(screen.getByText(expectedStatusText)).toBeInTheDocument(); + expect(screen.getByText(mockDescription)).toBeInTheDocument(); +}; + +/* AgendaDetailsPage */ + +const testAgendaDetailsPage = ( + mockChoice, + finished, + passed, + expectedTooltipText, + expectedStatusText, + expectedVotedText, + isLoading = false +) => { + const mockAgendaName = "test-agenda-name"; + const mockDescription = "test-desc-test"; + mockAllAgendas = [ + { + description: mockDescription, + finished: finished, + name: mockAgendaName, + passed: passed + } + ]; + + mockVoteChoices = [{ agendaId: mockAgendaName, choiceId: mockChoice }]; + mockSelectedChoice = mockChoice; + mockIsLoading = isLoading; + + render(); + testAgendaCardElements( + mockAgendaName, + mockDescription, + mockChoice, + expectedTooltipText, + expectedStatusText + ); + expect(screen.getByText(expectedVotedText)).toBeInTheDocument(); + + const yesButton = screen.getByRole("radio", { name: "yes" }); + const noButton = screen.getByRole("radio", { name: "no" }); + const abstainButton = screen.getByRole("radio", { name: "abstain" }); + expect(yesButton.disabled).toBe(finished || isLoading); + expect(noButton.disabled).toBe(finished || isLoading); + expect(abstainButton.disabled).toBe(finished || isLoading); + + expect(screen.getByRole("radio", { name: mockChoice }).checked).toBe(true); + + if (!finished && !isLoading && mockChoice != "yes") { + user.click(yesButton); + expect(mockSetSelectedChoice).toHaveBeenCalledWith("yes"); + } +}; + +test.each([ + [ + "yes", + true, + true, + "This agenda has finished voting and PASSED.", + "Finished", + "Voted for:" + ], // finished passed voted yes + [ + "no", + true, + true, + "This agenda has finished voting and PASSED.", + "Finished", + "Voted for:" + ], // finished passed voted no + [ + "abstain", + true, + false, + "This agenda has finished voting and NOT PASSED.", + "Finished", + "Voted for:" + ], // finished passed voted abstain + [ + "yes", + false, + null, + "Voting is still in progress.", + "In Progress", + "Voting for:" + ], // not finished voted yes + [ + "no", + false, + null, + "Voting is still in progress.", + "In Progress", + "Voting for:" + ], // not finished voted no + [ + "abstain", + false, + null, + "Voting is still in progress.", + "In Progress", + "Voting for:" + ], // not finished not voted yet + [ + "abstain", + false, + null, + "Voting is still in progress.", + "In Progress", + "Voting for:", + true + ] // not finished not voted yet in loading state. controls should be disabled +])( + "test agendaDetailsPage ( choice: %s, finished: %s, passed: %s)", + testAgendaDetailsPage +); diff --git a/test/unit/components/views/GovernancePage/Blockchain/Blockchain.spec.js b/test/unit/components/views/GovernancePage/Blockchain/Blockchain.spec.js new file mode 100644 index 0000000000..5654dfc089 --- /dev/null +++ b/test/unit/components/views/GovernancePage/Blockchain/Blockchain.spec.js @@ -0,0 +1,173 @@ +import Blockchain from "components/views/GovernancePage/Blockchain"; +import { render } from "test-utils.js"; +import { screen, wait } from "@testing-library/react"; +import user from "@testing-library/user-event"; + +let mockAllAgendas; +let mockVoteChoices; +const mockViewAgendaDetailsHandler = jest.fn(); + +jest.mock("components/views/GovernancePage/Blockchain/hooks", () => ({ + useBlockchain: () => { + return { + allAgendas: mockAllAgendas, + voteChoices: mockVoteChoices, + viewAgendaDetailsHandler: mockViewAgendaDetailsHandler + }; + } +})); + +const testAgendaCardElements = ( + mockAgendaName, + mockDescription, + mockChoice, + expectedTooltipText, + expectedStatusText +) => { + expect(screen.getAllByText(mockAgendaName).length).toBe(2); //one title and one agenda ID + expect(screen.getByText("Agenda ID:").parentNode.textContent).toMatch( + `Agenda ID: ${mockAgendaName}` + ); + expect(screen.getByText("Preference:").parentNode.textContent).toMatch( + `Preference: ${mockChoice}` + ); + expect(screen.getByText(expectedTooltipText)).toBeInTheDocument(); + expect(screen.getByText(expectedStatusText)).toBeInTheDocument(); + + user.click(screen.getByText(mockDescription)); + expect(mockViewAgendaDetailsHandler).toHaveBeenCalled(); +}; + +const testAgendaCard = ( + mockChoice, + finished, + passed, + expectedTooltipText, + expectedStatusText +) => { + const mockAgendaName = "test-agenda-name"; + const mockDescription = "test-desc-test"; + mockAllAgendas = [ + { + description: mockDescription, + finished: finished, + name: mockAgendaName, + passed: passed + } + ]; + + mockVoteChoices = [{ agendaId: mockAgendaName, choiceId: mockChoice }]; + render(); + testAgendaCardElements( + mockAgendaName, + mockDescription, + mockChoice, + expectedTooltipText, + expectedStatusText + ); +}; + +test.each([ + [ + "yes", + true, + true, + "This agenda has finished voting and PASSED.", + "Finished" + ], // finished passed voted yes + ["no", true, true, "This agenda has finished voting and PASSED.", "Finished"], // finished passed voted no + [ + "abstain", + true, + false, + "This agenda has finished voting and NOT PASSED.", + "Finished" + ], // finished passed voted abstain + ["yes", false, null, "Voting is still in progress.", "In Progress"], // not finished voted yes + ["no", false, null, "Voting is still in progress.", "In Progress"], // not finished voted no + ["abstain", false, null, "Voting is still in progress.", "In Progress"] // not finished not voted yet +])("test agendaCard ( choice: %s, finished: %s, passed: %s)", testAgendaCard); + +test("no agendas", () => { + mockAllAgendas = []; + render(); + expect( + screen.getByText(/there are currently no agendas for voting/i) + ).toBeInTheDocument(); +}); + +test("test agenda search and sort controls", async () => { + mockAllAgendas = [ + { + name: "test-name-1", + description: "test-desc-1" + }, + { + name: "test-name-12", + description: "test-desc-12" + }, + { + name: "test-name-3", + description: "test-desc-3" + } + ]; + render(); + expect( + screen + .getAllByText(/test-desc-/i) + .map((element) => element.textContent.trim()) + ).toStrictEqual(["test-desc-1", "test-desc-12", "test-desc-3"]); + + const filterControl = screen.getByPlaceholderText("Filter by Name"); + + user.type(filterControl, "1"); + expect( + screen + .getAllByText(/test-desc-/i) + .map((element) => element.textContent.trim()) + ).toStrictEqual(["test-desc-1", "test-desc-12"]); + + const eyeFilterMenu = screen.getByRole("button", { name: "EyeFilterMenu" }); + user.click(eyeFilterMenu); + user.click(screen.getByText("Oldest")); + await wait(() => + expect( + screen + .getAllByText(/test-desc-/i) + .map((element) => element.textContent.trim()) + ).toStrictEqual(["test-desc-12", "test-desc-1"]) + ); + + user.clear(filterControl); + user.type(filterControl, "12"); + expect( + screen + .getAllByText(/test-desc-/i) + .map((element) => element.textContent.trim()) + ).toStrictEqual(["test-desc-12"]); + + user.clear(filterControl); + user.type(filterControl, "4"); + expect(screen.queryByText(/test-desc-/i)).not.toBeInTheDocument(); + expect( + screen.getByText(/no agendas matched your search/i) + ).toBeInTheDocument(); + + user.clear(filterControl); + expect( + screen + .getAllByText(/test-desc-/i) + .map((element) => element.textContent.trim()) + ).toStrictEqual(["test-desc-3", "test-desc-12", "test-desc-1"]); + + // Newest first + user.click(eyeFilterMenu); + user.click(screen.getByText("Newest")); + await wait(() => + expect( + screen + .getAllByText(/test-desc-/i) + .map((element) => element.textContent.trim()) + ).toStrictEqual(["test-desc-1", "test-desc-12", "test-desc-3"]) + ); +});