Add proposal info card (#52)
This commit is contained in:
parent
ce07cd30fb
commit
fea412ec90
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="18" height="15" viewBox="0 0 18 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.00004 11.2326L1.83004 7.04645L0.415039 8.46692L6.00004 14.0735L18 2.0271L16.585 0.606628L6.00004 11.2326Z" fill="#3BA881"/>
|
||||
</svg>
|
After Width: | Height: | Size: 239 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="19" viewBox="0 0 24 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99989 14.5601L2.43989 9.00005L0.553223 10.8867L7.99989 18.3334L23.9999 2.33339L22.1132 0.44672L7.99989 14.5601Z" fill="#3BA881"/>
|
||||
</svg>
|
After Width: | Height: | Size: 245 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1.01935L7.99999 8.04642M7.99999 8.04642L15 15.0735M7.99999 8.04642L15 1.01935M7.99999 8.04642L1 15.0735" stroke="#FF6F80" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 305 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.6665 1.66667L10.9998 11M10.9998 11L20.3332 20.3333M10.9998 11L20.3332 1.66667M10.9998 11L1.6665 20.3333" stroke="#FF6F80" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 305 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.49984 3.0665C8.49984 3.28742 8.32075 3.4665 8.09984 3.4665L5.29984 3.4665C4.56346 3.4665 3.9665 4.06346 3.9665 4.79984L3.9665 11.1998C3.9665 11.9362 4.56346 12.5332 5.29984 12.5332H11.6998C12.4362 12.5332 13.0332 11.9362 13.0332 11.1998V8.39984C13.0332 8.17892 13.2123 7.99984 13.4332 7.99984C13.6541 7.99984 13.8332 8.17892 13.8332 8.39984V11.1998C13.8332 12.378 12.878 13.3332 11.6998 13.3332H5.29984C4.12163 13.3332 3.1665 12.378 3.1665 11.1998V4.79984C3.1665 3.62163 4.12163 2.6665 5.29984 2.6665H8.09984C8.32075 2.6665 8.49984 2.84559 8.49984 3.0665ZM12.8562 2.66765C13.4299 2.66465 13.4328 2.66463 13.5317 2.70563C13.6504 2.75456 13.7448 2.84863 13.7942 2.96714C13.8009 2.98344 13.8066 2.99699 13.8113 3.01087C13.853 3.13344 13.8201 3.28131 13.8314 5.57301C13.8314 5.70145 13.7804 5.82462 13.6896 5.91544C13.5988 6.00626 13.4757 6.05728 13.3472 6.05728C13.2188 6.05728 13.0957 6.00626 13.0049 5.91544C12.9141 5.82462 12.863 5.70145 12.863 5.57301V4.32117L7.33231 9.85199C7.28764 9.89825 7.23421 9.93514 7.17514 9.96052C7.11607 9.9859 7.05253 9.99926 6.98824 9.99982C6.92395 10.0004 6.86019 9.98812 6.80068 9.96378C6.74118 9.93943 6.68712 9.90347 6.64165 9.858C6.59619 9.81253 6.56024 9.75846 6.53589 9.69894C6.51155 9.63943 6.4993 9.57566 6.49986 9.51136C6.50041 9.44706 6.51377 9.38351 6.53915 9.32443C6.56452 9.26534 6.60141 9.21191 6.64766 9.16723L12.1784 3.63592H10.9263C10.7979 3.63592 10.6747 3.5849 10.5839 3.49408C10.4931 3.40326 10.4421 3.28009 10.4421 3.15165C10.4421 3.02321 10.4931 2.90004 10.5839 2.80922C10.6747 2.7184 10.7979 2.66738 10.9263 2.66738C11.9071 2.67262 12.4949 2.66954 12.8562 2.66765Z" fill="#4360DF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="14" height="5" viewBox="0 0 14 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.19619 4.16989C7.1213 4.21097 7.03062 4.21097 6.95573 4.16989L1.06846 0.940522C0.841661 0.816112 0.930014 0.471333 1.1887 0.471333L12.9632 0.471332C13.2219 0.471332 13.3103 0.816112 13.0835 0.940522L7.19619 4.16989Z" fill="#7E98F4"/>
|
||||
</svg>
|
After Width: | Height: | Size: 346 B |
|
@ -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;
|
||||
}
|
||||
`
|
|
@ -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 (
|
||||
<ProposalWrapper>
|
||||
<ProposalHeader theme={blueTheme} />
|
||||
<ProposalList />
|
||||
</ProposalWrapper>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<Card>
|
||||
<ProposalInfo
|
||||
heading={'This is a very long, explainative and sophisticated title for a proposal.'}
|
||||
text={
|
||||
'This is a longer description of the proposal. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque interdum rutrum sodales. Nullam mattis fermentum libero, non volutpat. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque interdum rutrum sodales. Nullam mattis fermentum libero. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque interdum rutrum sodales. Nullam mattis fermentum libero.'
|
||||
}
|
||||
address={'#'}
|
||||
/>
|
||||
<ProposalVote vote={2345678} voteWinner={2} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
`
|
|
@ -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 (
|
||||
<Card>
|
||||
<CardHeading>{heading}</CardHeading>
|
||||
<CardText>{text}</CardText>
|
||||
<ViewLink address={address} />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
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;
|
||||
`
|
|
@ -0,0 +1,6 @@
|
|||
import React from 'react'
|
||||
import { ProposalCard } from './ProposalCard'
|
||||
|
||||
export function ProposalList() {
|
||||
return <ProposalCard />
|
||||
}
|
|
@ -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 (
|
||||
<Card>
|
||||
{voteWinner ? <CardHeading>Proposal {voteWinner == 1 ? 'rejected' : 'passed'}</CardHeading> : <CardHeading />}
|
||||
|
||||
<VoteChart votesFor={1865567} votesAgainst={1740235} timeLeft={48} voteWinner={2} />
|
||||
|
||||
{voteWinner ? (
|
||||
<FinalBtn disabled={!account}>Finalize the vote</FinalBtn>
|
||||
) : (
|
||||
<VotesBtns>
|
||||
<VoteBtnAgainst disabled={!account}>Vote Against</VoteBtnAgainst>
|
||||
<VoteBtnFor disabled={!account}>Vote For</VoteBtnFor>
|
||||
</VotesBtns>
|
||||
)}
|
||||
|
||||
<CardVoteBottom>{vote && <VoteSubmitButton votes={vote} disabled={!account} />}</CardVoteBottom>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
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;
|
||||
`
|
|
@ -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 (
|
||||
<Votes>
|
||||
<VotesChart className={selectedVote || tabletMode ? '' : 'notModal'}>
|
||||
<VoteBox
|
||||
style={{
|
||||
filter: voteWinner && voteWinner === 2 ? 'grayscale(1)' : 'none',
|
||||
alignItems: mobileVersion ? 'flex-start' : 'center',
|
||||
}}
|
||||
>
|
||||
<VoteIcon src={voteWinner === 1 ? crossWinnerIcon : crossIcon} width={voteWinner === 1 ? '18px' : '14px'} />
|
||||
<span>
|
||||
{' '}
|
||||
{isAnimation && proposingAmount && selectedVote && selectedVote === 0 ? (
|
||||
<CountUp end={votesAgainst + proposingAmount} separator="," />
|
||||
) : (
|
||||
addCommas(votesAgainst)
|
||||
)}{' '}
|
||||
<span style={{ fontWeight: 'normal' }}>ABC</span>
|
||||
</span>
|
||||
</VoteBox>
|
||||
<TimeLeft className={selectedVote ? '' : 'notModal'}>{formatTimeLeft(timeLeft)}</TimeLeft>
|
||||
<VoteBox
|
||||
style={{
|
||||
filter: voteWinner && voteWinner === 1 ? 'grayscale(1)' : 'none',
|
||||
alignItems: mobileVersion ? 'flex-start' : 'center',
|
||||
}}
|
||||
>
|
||||
<VoteIcon src={voteWinner === 2 ? checkWinnerIcon : checkIcon} width={voteWinner === 2 ? '24px' : '18px'} />
|
||||
<span>
|
||||
{' '}
|
||||
{isAnimation && proposingAmount && selectedVote && selectedVote === 1 ? (
|
||||
<CountUp end={votesFor + proposingAmount} separator="," />
|
||||
) : (
|
||||
addCommas(votesFor)
|
||||
)}{' '}
|
||||
<span style={{ fontWeight: 'normal' }}>ABC</span>
|
||||
</span>
|
||||
</VoteBox>
|
||||
</VotesChart>
|
||||
<VoteGraphBarWrap className={selectedVote || tabletMode ? '' : 'notModal'}>
|
||||
<VoteGraphBar
|
||||
graphWidth={graphWidth}
|
||||
balanceWidth={balanceWidth}
|
||||
voteWinner={voteWinner}
|
||||
isAnimation={isAnimation}
|
||||
/>
|
||||
<TimeLeftMobile className={selectedVote ? '' : 'notModal'}>{formatTimeLeft(timeLeft)}</TimeLeftMobile>
|
||||
</VoteGraphBarWrap>
|
||||
</Votes>
|
||||
)
|
||||
}
|
||||
|
||||
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%;
|
||||
}
|
||||
}
|
||||
`
|
|
@ -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<any>({ width: `${votesWidth}%` })
|
||||
|
||||
useEffect(() => {
|
||||
if (isAnimation) {
|
||||
setStyle({ width: `${markerWidth}%`, animation: 'fade-in 2s ease' })
|
||||
setKeyFrames(createKeyFrames(votesWidth, markerWidth))
|
||||
} else {
|
||||
setStyle({ width: `${votesWidth}%` })
|
||||
}
|
||||
}, [isAnimation, votesWidth])
|
||||
|
||||
return (
|
||||
<VoteGraph theme={{ voteWinner }}>
|
||||
<style children={keyFrames} />
|
||||
<VoteGraphAgainst theme={{ voteWinner }} style={style} />
|
||||
<VoteBalance style={{ width: `${markerWidth}%` }} />
|
||||
</VoteGraph>
|
||||
)
|
||||
}
|
||||
|
||||
const VoteGraph = styled.div<VoteGraphBarProps>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
background-color: ${({ theme }) => (theme.voteWinner === 1 ? '#f1f2f5' : '#edfff4')};
|
||||
border-radius: 10px;
|
||||
padding-top: 5px;
|
||||
border-left: 13px solid ${({ theme }) => (theme.voteWinner === 2 ? '#f1f2f5' : '#ffeded')};
|
||||
border-right: 13px solid ${({ theme }) => (theme.voteWinner === 1 ? '#f1f2f5' : '#edfff4')};
|
||||
|
||||
@media (max-width: 600px) {
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 5px;
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: calc(50% - 2px);
|
||||
transform: translateX(-50%);
|
||||
background-image: url(${indicatorIcon});
|
||||
background-size: cover;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 9px;
|
||||
height: 3px;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
top: -4px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const VoteGraphAgainst = styled.div<VoteGraphBarProps>`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 16px;
|
||||
background-color: ${({ theme }) => (theme.voteWinner === 2 ? '#f1f2f5' : '#ffeded')};
|
||||
transition: width 2s;
|
||||
z-index: 2;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
height: 13px;
|
||||
}
|
||||
`
|
||||
|
||||
const VoteBalance = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 16px;
|
||||
background-color: transparent;
|
||||
border-right: 2px solid #7e98f4;
|
||||
z-index: 2;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
height: 13px;
|
||||
transition: width 2s;
|
||||
}
|
||||
`
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react'
|
||||
import { addCommas } from '../../helpers/addCommas'
|
||||
import { VoteSendingBtn } from '../Buttons'
|
||||
|
||||
interface VoteSubmitButtonProps {
|
||||
votes: number
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
export function VoteSubmitButton({ votes, disabled }: VoteSubmitButtonProps) {
|
||||
if (votes > 0) {
|
||||
return <VoteSendingBtn disabled={disabled}> {addCommas(votes)} votes need saving</VoteSendingBtn>
|
||||
}
|
||||
return null
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import externalIcon from '../assets/svg/external.svg'
|
||||
|
||||
interface ViewLinkProps {
|
||||
address: string
|
||||
}
|
||||
|
||||
export function ViewLink({ address }: ViewLinkProps) {
|
||||
return <Link href={address}>View on Etherscan</Link>
|
||||
}
|
||||
|
||||
export const Link = styled.a`
|
||||
color: #4360df;
|
||||
position: relative;
|
||||
padding-right: 20px;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
background-image: url(${externalIcon});
|
||||
background-size: contain;
|
||||
}
|
||||
`
|
|
@ -0,0 +1,3 @@
|
|||
export function addCommas(votes: number) {
|
||||
return votes.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import timeConvert from 'humanize-duration'
|
||||
|
||||
export function formatTimeLeft(timeLeft: number) {
|
||||
return timeLeft > 0 ? timeConvert(timeLeft, { largest: 1, round: true }) + ' left' : 'Vote ended'
|
||||
}
|
29
yarn.lock
29
yarn.lock
|
@ -1174,6 +1174,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57"
|
||||
integrity sha512-h4lTMgMJctJybDp8CQrxTUiiYmedihHWkjnF/8Pxseu2S6Nlfcy8kwboQ8yejh456rP2yWoEVm1sS/FVsfM48w==
|
||||
|
||||
"@types/humanize-duration@^3.25.1":
|
||||
version "3.25.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/humanize-duration/-/humanize-duration-3.25.1.tgz#b6140d5fc00ff3917b3f521784abef4bc0387ccc"
|
||||
integrity sha512-WZU/4bb+lvzyDmZzjJtp++9mfKy6B3lH6gGISgkcz6SU8hMILKRM0vi08TxIsb0dQB4Gzo68MWLmctu6xqUi9g==
|
||||
|
||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
|
||||
|
@ -1271,6 +1276,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
|
||||
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
|
||||
|
||||
"@types/react-countup@^4.3.1":
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-countup/-/react-countup-4.3.1.tgz#5bba8e11c18549c92f9a6efe07e406c92c970f89"
|
||||
integrity sha512-8KwOdVVKhMOdtXJoOg3YXZwWxnFnZtPojNs/DZRlhHTpOYIb1If+9+rVPnwnXDhNhphj5diUCds9E26VtVK8IA==
|
||||
dependencies:
|
||||
react-countup "*"
|
||||
|
||||
"@types/react-dom@>=16.9.0", "@types/react-dom@^17.0.9":
|
||||
version "17.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
|
||||
|
@ -3659,6 +3671,11 @@ cosmiconfig@^6.0.0:
|
|||
path-type "^4.0.0"
|
||||
yaml "^1.7.2"
|
||||
|
||||
countup.js@^2.0.8:
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/countup.js/-/countup.js-2.0.8.tgz#eca0c31c9db3f7769cba494d9315cd52dbaaf1b9"
|
||||
integrity sha512-pW3xwwD+hB+xmtI16xFcuLS0D5hSQqPQWkZOdgpKQyzxCquDNo2VCFPkRw12vmvdpnicXVTcjmYiakG6biwINg==
|
||||
|
||||
create-ecdh@^4.0.0:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
|
||||
|
@ -6294,6 +6311,11 @@ human-signals@^2.1.0:
|
|||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
|
||||
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
|
||||
|
||||
humanize-duration@^3.27.0:
|
||||
version "3.27.0"
|
||||
resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.27.0.tgz#3f781b7cf8022ad587f76b9839b60bc2b29636b2"
|
||||
integrity sha512-qLo/08cNc3Tb0uD7jK0jAcU5cnqCM0n568918E7R2XhMr/+7F37p4EY062W/stg7tmzvknNn9b/1+UhVRzsYrQ==
|
||||
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
|
@ -9845,6 +9867,13 @@ rc@^1.2.8:
|
|||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-countup@*, react-countup@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-countup/-/react-countup-5.2.0.tgz#58be5d97acebf767d5c61df5c3a3417b95c45a16"
|
||||
integrity sha512-R6+FIrW8ypwoAe0Q0CZ16OhrgAntnnnch7HrnRAy9miXFKk8jQzVADjNtGSoNUuJeq/ZFiiXCGJCJIAmRJ5fLg==
|
||||
dependencies:
|
||||
countup.js "^2.0.8"
|
||||
|
||||
react-dom@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||
|
|
Loading…
Reference in New Issue