From fea412ec90d6406a004c3463bd2728f570dc2500 Mon Sep 17 00:00:00 2001 From: Maria Rushkova <66270386+mrushkova@users.noreply.github.com> Date: Mon, 6 Sep 2021 18:02:03 +0200 Subject: [PATCH] Add proposal info card (#52) --- packages/proposal-components/package.json | 10 +- .../src/assets/assets.d.ts | 14 ++ .../src/assets/svg/check.svg | 3 + .../src/assets/svg/checkWinner.svg | 3 + .../src/assets/svg/cross.svg | 3 + .../src/assets/svg/crossWinner.svg | 3 + .../src/assets/svg/external.svg | 3 + .../src/assets/svg/indicator.svg | 3 + .../src/components/Buttons.tsx | 92 ++++++++ .../src/components/Proposal.tsx | 2 + .../src/components/ProposalCard.tsx | 40 ++++ .../src/components/ProposalInfo.tsx | 54 +++++ .../src/components/ProposalList.tsx | 6 + .../ProposalVoteCard/ProposalVote.tsx | 86 +++++++ .../components/ProposalVoteCard/VoteChart.tsx | 215 ++++++++++++++++++ .../ProposalVoteCard/VoteGraphBar.tsx | 112 +++++++++ .../ProposalVoteCard/VoteSubmitButton.tsx | 15 ++ .../src/components/ViewLink.tsx | 40 ++++ .../src/helpers/addCommas.ts | 3 + .../src/helpers/fomatTimeLeft.ts | 5 + yarn.lock | 29 +++ 21 files changed, 739 insertions(+), 2 deletions(-) create mode 100644 packages/proposal-components/src/assets/assets.d.ts create mode 100644 packages/proposal-components/src/assets/svg/check.svg create mode 100644 packages/proposal-components/src/assets/svg/checkWinner.svg create mode 100644 packages/proposal-components/src/assets/svg/cross.svg create mode 100644 packages/proposal-components/src/assets/svg/crossWinner.svg create mode 100644 packages/proposal-components/src/assets/svg/external.svg create mode 100644 packages/proposal-components/src/assets/svg/indicator.svg create mode 100644 packages/proposal-components/src/components/Buttons.tsx create mode 100644 packages/proposal-components/src/components/ProposalCard.tsx create mode 100644 packages/proposal-components/src/components/ProposalInfo.tsx create mode 100644 packages/proposal-components/src/components/ProposalList.tsx create mode 100644 packages/proposal-components/src/components/ProposalVoteCard/ProposalVote.tsx create mode 100644 packages/proposal-components/src/components/ProposalVoteCard/VoteChart.tsx create mode 100644 packages/proposal-components/src/components/ProposalVoteCard/VoteGraphBar.tsx create mode 100644 packages/proposal-components/src/components/ProposalVoteCard/VoteSubmitButton.tsx create mode 100644 packages/proposal-components/src/components/ViewLink.tsx create mode 100644 packages/proposal-components/src/helpers/addCommas.ts create mode 100644 packages/proposal-components/src/helpers/fomatTimeLeft.ts diff --git a/packages/proposal-components/package.json b/packages/proposal-components/package.json index 8f2748b..bd77afa 100644 --- a/packages/proposal-components/package.json +++ b/packages/proposal-components/package.json @@ -7,7 +7,9 @@ "license": "MIT", "watch": { "build": { - "patterns": ["src"], + "patterns": [ + "src" + ], "extensions": "ts,tsx", "runOnChangeOnly": false } @@ -30,17 +32,21 @@ "lint:prettier": "yarn prettier './{src,test}/**/*.{ts,tsx}'" }, "dependencies": { + "@status-waku-voting/core": "^0.1.0", "@status-waku-voting/proposal-hooks": "^0.1.0", "@status-waku-voting/react-components": "^0.1.0", - "@status-waku-voting/core": "^0.1.0", "ethers": "^5.4.4", + "humanize-duration": "^3.27.0", "react": "^17.0.2", + "react-countup": "^5.2.0", "styled-components": "^5.3.0" }, "devDependencies": { "@types/chai": "^4.2.21", + "@types/humanize-duration": "^3.25.1", "@types/mocha": "^9.0.0", "@types/react": "^17.0.16", + "@types/react-countup": "^4.3.1", "@types/styled-components": "^5.1.12", "@typescript-eslint/eslint-plugin": "^4.29.0", "@typescript-eslint/parser": "^4.29.0", diff --git a/packages/proposal-components/src/assets/assets.d.ts b/packages/proposal-components/src/assets/assets.d.ts new file mode 100644 index 0000000..4ce4da1 --- /dev/null +++ b/packages/proposal-components/src/assets/assets.d.ts @@ -0,0 +1,14 @@ +declare module '*.svg' { + const url: string + export default url +} + +declare module '*.jpg' { + const url: string + export default url +} + +declare module '*.png' { + const url: string + export default url +} diff --git a/packages/proposal-components/src/assets/svg/check.svg b/packages/proposal-components/src/assets/svg/check.svg new file mode 100644 index 0000000..a8589b9 --- /dev/null +++ b/packages/proposal-components/src/assets/svg/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/proposal-components/src/assets/svg/checkWinner.svg b/packages/proposal-components/src/assets/svg/checkWinner.svg new file mode 100644 index 0000000..f3200ec --- /dev/null +++ b/packages/proposal-components/src/assets/svg/checkWinner.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/proposal-components/src/assets/svg/cross.svg b/packages/proposal-components/src/assets/svg/cross.svg new file mode 100644 index 0000000..728a295 --- /dev/null +++ b/packages/proposal-components/src/assets/svg/cross.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/proposal-components/src/assets/svg/crossWinner.svg b/packages/proposal-components/src/assets/svg/crossWinner.svg new file mode 100644 index 0000000..62aa890 --- /dev/null +++ b/packages/proposal-components/src/assets/svg/crossWinner.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/proposal-components/src/assets/svg/external.svg b/packages/proposal-components/src/assets/svg/external.svg new file mode 100644 index 0000000..260fa58 --- /dev/null +++ b/packages/proposal-components/src/assets/svg/external.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/proposal-components/src/assets/svg/indicator.svg b/packages/proposal-components/src/assets/svg/indicator.svg new file mode 100644 index 0000000..bb2ece7 --- /dev/null +++ b/packages/proposal-components/src/assets/svg/indicator.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/proposal-components/src/components/Buttons.tsx b/packages/proposal-components/src/components/Buttons.tsx new file mode 100644 index 0000000..a9e99e8 --- /dev/null +++ b/packages/proposal-components/src/components/Buttons.tsx @@ -0,0 +1,92 @@ +import { Button } from '@status-waku-voting/react-components' +import styled from 'styled-components' + +export const Btn = styled(Button)` + padding: 11px 0; + font-weight: 500; + line-height: 22px; + text-align: center; +` + +export const VoteBtn = styled(Btn)` + width: 44%; + padding: 11px 0; + font-weight: 500; + line-height: 22px; + text-align: center; + + &:disabled { + background: #f3f3f3; + color: #939ba1; + } + + @media (max-width: 768px) { + width: 48%; + } +` + +export const VoteBtnAgainst = styled(VoteBtn)` + background-color: #ffeded; + color: #c90a0a; + + &:not(:disabled):hover { + background: #ffdada; + } + + &:not(:disabled):active { + background: #fff5f5; + } +` +export const VoteBtnFor = styled(VoteBtn)` + background-color: #edfff4; + color: #1d920a; + + &:not(:disabled):hover { + background: #ccfee0; + } + + &:not(:disabled):active { + background: #F3FFF8; +` +export const VoteSendingBtn = styled(Btn)` + margin-top: 24px; + padding: 0; + color: #0f3595; + height: auto; + background: transparent; + + &:hover { + color: #5d7be2; + } + + &:active { + color: #0f3595; + } + + &:disabled { + color: #676868; + } +` + +export const FinalBtn = styled(Btn)` + width: 100%; + background: #edf1ff; + color: #0f3595; + + &:not(:disabled):hover { + background: #dbeeff; + } + + &:not(:disabled):active { + background: #f7f9ff; + } + + &:disabled { + background: #f3f3f3; + color: #939ba1; + } + + @media (max-width: 600px) { + margin-top: 32px; + } +` diff --git a/packages/proposal-components/src/components/Proposal.tsx b/packages/proposal-components/src/components/Proposal.tsx index 79dd730..e394a76 100644 --- a/packages/proposal-components/src/components/Proposal.tsx +++ b/packages/proposal-components/src/components/Proposal.tsx @@ -2,11 +2,13 @@ import React from 'react' import styled from 'styled-components' import { ProposalHeader } from './ProposalHeader' import { blueTheme } from '@status-waku-voting/react-components/dist/esm/src/style/themes' +import { ProposalList } from './ProposalList' export function Proposal() { return ( + ) } diff --git a/packages/proposal-components/src/components/ProposalCard.tsx b/packages/proposal-components/src/components/ProposalCard.tsx new file mode 100644 index 0000000..c2eaa58 --- /dev/null +++ b/packages/proposal-components/src/components/ProposalCard.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import styled from 'styled-components' +import { ProposalInfo } from './ProposalInfo' +import { ProposalVote } from './ProposalVoteCard/ProposalVote' + +export function ProposalCard() { + return ( + + + + + ) +} + +export const Card = styled.div` + display: flex; + align-items: stretch; + margin-top: 24px; + + @media (max-width: 768px) { + flex-direction: column; + margin-top: 0; + padding: 16px 0 32px; + border-bottom: 1px solid #e0e0e0; + } + @media (max-width: 600px) { + padding-bottom: 16px; + } + &:not:first-child { + @media (max-width: 768px) { + border-top: 1px solid #e0e0e0; + } + } +` diff --git a/packages/proposal-components/src/components/ProposalInfo.tsx b/packages/proposal-components/src/components/ProposalInfo.tsx new file mode 100644 index 0000000..2c2737a --- /dev/null +++ b/packages/proposal-components/src/components/ProposalInfo.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import styled from 'styled-components' +import { ViewLink } from './ViewLink' + +type ProposalInfoProps = { + heading: string + text: string + address: string +} + +export function ProposalInfo({ heading, text, address }: ProposalInfoProps) { + return ( + + {heading} + {text} + + + ) +} + +export const Card = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 50%; + padding: 24px; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); + border-radius: 6px 0px 0px 6px; + + @media (max-width: 768px) { + width: 100%; + margin: 0; + border: none; + box-shadow: none; + padding-bottom: 0; + } + @media (max-width: 600px) { + padding: 0; + } +` + +const CardHeading = styled.div` + height: 56px; + font-weight: bold; + font-size: 22px; + line-height: 24px; + margin-bottom: 8px; +` + +const CardText = styled.div` + font-size: 13px; + line-height: 18px; + margin-bottom: 16px; +` diff --git a/packages/proposal-components/src/components/ProposalList.tsx b/packages/proposal-components/src/components/ProposalList.tsx new file mode 100644 index 0000000..612fb15 --- /dev/null +++ b/packages/proposal-components/src/components/ProposalList.tsx @@ -0,0 +1,6 @@ +import React from 'react' +import { ProposalCard } from './ProposalCard' + +export function ProposalList() { + return +} diff --git a/packages/proposal-components/src/components/ProposalVoteCard/ProposalVote.tsx b/packages/proposal-components/src/components/ProposalVoteCard/ProposalVote.tsx new file mode 100644 index 0000000..ce4f68b --- /dev/null +++ b/packages/proposal-components/src/components/ProposalVoteCard/ProposalVote.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react' +import styled from 'styled-components' +import { useEthers } from '@usedapp/core' +import { FinalBtn, VoteBtnAgainst, VoteBtnFor } from '../Buttons' +import { VoteSubmitButton } from './VoteSubmitButton' +import { VoteChart } from './VoteChart' + +interface ProposalVoteProps { + vote: number + voteWinner?: number + hideModalFunction?: (val: boolean) => void +} + +export function ProposalVote({ vote, voteWinner, hideModalFunction }: ProposalVoteProps) { + const { account } = useEthers() + const [showVoteModal, setShowVoteModal] = useState(false) + + return ( + + {voteWinner ? Proposal {voteWinner == 1 ? 'rejected' : 'passed'} : } + + + + {voteWinner ? ( + Finalize the vote + ) : ( + + Vote Against + Vote For + + )} + + {vote && } + + ) +} + +export const Card = styled.div` + display: flex; + flex-direction: column; + align-items: center; + width: 50%; + padding: 24px; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); + border-radius: 6px 0px 0px 6px; + background-color: #fbfcfe; + + @media (max-width: 768px) { + width: 100%; + box-shadow: none; + border-radius: unset; + background-color: unset; + padding-bottom: 0; + } + + @media (max-width: 600px) { + flex-direction: column; + padding: 16px 0 0; + border-bottom: none; + } +` + +export const CardHeading = styled.h2` + height: 24px; + font-weight: bold; + font-size: 17px; + line-height: 24px; + margin: 0; + margin-bottom: 15px; +` + +export const VotesBtns = styled.div` + display: flex; + justify-content: space-between; + width: 100%; + + @media (max-width: 600px) { + margin-top: 24px; + } +` + +const CardVoteBottom = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; +` diff --git a/packages/proposal-components/src/components/ProposalVoteCard/VoteChart.tsx b/packages/proposal-components/src/components/ProposalVoteCard/VoteChart.tsx new file mode 100644 index 0000000..ff8cbe2 --- /dev/null +++ b/packages/proposal-components/src/components/ProposalVoteCard/VoteChart.tsx @@ -0,0 +1,215 @@ +import React, { useEffect, useState } from 'react' +import CountUp from 'react-countup' +import styled from 'styled-components' +import { addCommas } from '../../helpers/addCommas' +import { formatTimeLeft } from '../../helpers/fomatTimeLeft' +import { VoteGraphBar } from './VoteGraphBar' +import crossIcon from '../../assets/svg/cross.svg' +import crossWinnerIcon from '../../assets/svg/crossWinner.svg' +import checkIcon from '../../assets/svg/check.svg' +import checkWinnerIcon from '../../assets/svg/checkWinner.svg' + +export interface VoteChartProps { + votesFor: number + votesAgainst: number + timeLeft: number + voteWinner?: number + proposingAmount?: number + selectedVote?: number + isAnimation?: boolean + tabletMode?: (val: boolean) => void +} + +export function VoteChart({ + votesFor, + votesAgainst, + timeLeft, + voteWinner, + proposingAmount, + selectedVote, + isAnimation, + tabletMode, +}: VoteChartProps) { + const [mobileVersion, setMobileVersion] = useState(false) + + useEffect(() => { + const handleResize = () => { + if (window.innerWidth < 600) { + setMobileVersion(true) + } else { + setMobileVersion(false) + } + } + handleResize() + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, []) + + const voteSum = votesFor + votesAgainst + const graphWidth = (100 * votesAgainst) / voteSum + + let balanceWidth = graphWidth + + if (proposingAmount && selectedVote) { + balanceWidth = + selectedVote === 0 + ? (100 * (votesAgainst + proposingAmount)) / (voteSum + proposingAmount) + : (100 * votesAgainst) / (voteSum + proposingAmount) + } + + return ( + + + + + + {' '} + {isAnimation && proposingAmount && selectedVote && selectedVote === 0 ? ( + + ) : ( + addCommas(votesAgainst) + )}{' '} + ABC + + + {formatTimeLeft(timeLeft)} + + + + {' '} + {isAnimation && proposingAmount && selectedVote && selectedVote === 1 ? ( + + ) : ( + addCommas(votesFor) + )}{' '} + ABC + + + + + + {formatTimeLeft(timeLeft)} + + + ) +} + +const Votes = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 32px; + width: 100%; + position: relative; + + @media (max-width: 600px) { + margin-bottom: 0; + } +` +const VotesChart = styled.div` + display: flex; + justify-content: space-between; + align-items: stretch; + position: relative; + margin-bottom: 13px; + + &.notModal { + @media (max-width: 768px) { + margin-bottom: 0; + } + } +` + +const VoteBox = styled.div` + display: flex; + flex-direction: column; + justify-content: space-between; + align-content: center; + font-size: 12px; + text-align: center; + font-weight: normal; + + & > span { + font-weight: bold; + } + + @media (max-width: 768px) { + min-width: 70px; + } + + @media (max-width: 600px) { + min-width: unset; + } +` +const VoteIcon = styled.img` + margin-bottom: 13px; +` +const TimeLeft = styled.div` + position: absolute; + top: 50%; + left: calc(50%); + transform: translateX(-50%); + font-size: 12px; + line-height: 16px; + letter-spacing: 0.1px; + color: #939ba1; + + &.notModal { + @media (max-width: 768px) { + top: -16px; + } + + @media (max-width: 600px) { + top: unset; + } + } +` + +const TimeLeftMobile = styled.div` + position: absolute; + bottom: -23px; + left: calc(50%); + transform: translateX(-50%); + font-size: 0; + line-height: 16px; + letter-spacing: 0.1px; + color: #939ba1; + + @media (max-width: 600px) { + font-size: 12px; + } +` + +const VoteGraphBarWrap = styled.div` + position: static; + display: flex; + justify-content: center; + + &.notModal { + @media (max-width: 768px) { + position: absolute; + width: 65%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + + @media (max-width: 600px) { + width: 70%; + } + } +` diff --git a/packages/proposal-components/src/components/ProposalVoteCard/VoteGraphBar.tsx b/packages/proposal-components/src/components/ProposalVoteCard/VoteGraphBar.tsx new file mode 100644 index 0000000..f3d7cc9 --- /dev/null +++ b/packages/proposal-components/src/components/ProposalVoteCard/VoteGraphBar.tsx @@ -0,0 +1,112 @@ +import React, { useEffect, useState } from 'react' +import styled from 'styled-components' +import indicatorIcon from '../../assets/svg/indicator.svg' + +function createKeyFrames(votesWidth: number, markerWidth: number) { + return ` + @keyframes fade-in { + 0% { + width: ${votesWidth}%; + } + 100% { + width: ${markerWidth}%; + } + } + ` +} +export interface VoteGraphBarProps { + balanceWidth?: number + graphWidth?: number + voteWinner?: number + isAnimation?: boolean +} + +export function VoteGraphBar({ graphWidth, balanceWidth, voteWinner, isAnimation }: VoteGraphBarProps) { + const markerWidth: number = balanceWidth ? balanceWidth : 0 + const votesWidth: number = graphWidth ? graphWidth : 0 + const [keyFrames, setKeyFrames] = useState('') + const [style, setStyle] = useState({ width: `${votesWidth}%` }) + + useEffect(() => { + if (isAnimation) { + setStyle({ width: `${markerWidth}%`, animation: 'fade-in 2s ease' }) + setKeyFrames(createKeyFrames(votesWidth, markerWidth)) + } else { + setStyle({ width: `${votesWidth}%` }) + } + }, [isAnimation, votesWidth]) + + return ( + +