Make react component functional (#8)

This commit is contained in:
Szymon Szlachtowicz 2021-08-11 09:56:20 +02:00 committed by GitHub
parent 9cfb94ece5
commit 7531ad8f3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 285 additions and 1252 deletions

View File

@ -7,6 +7,7 @@ import PollInit from './utils/proto/PollInit'
import { WakuMessage } from 'js-waku' import { WakuMessage } from 'js-waku'
import { TimedPollVoteMsg } from './models/TimedPollVoteMsg' import { TimedPollVoteMsg } from './models/TimedPollVoteMsg'
import TimedPollVote from './utils/proto/TimedPollVote' import TimedPollVote from './utils/proto/TimedPollVote'
import { DetailedTimedPoll } from './models/DetailedTimedPoll'
class WakuVoting { class WakuVoting {
private appName: string private appName: string
@ -60,12 +61,14 @@ class WakuVoting {
} }
} }
public async getTimedPolls() { private async getTimedPolls() {
const messages = await this.waku?.store.queryHistory({ contentTopics: [this.pollInitTopic] }) const messages = await this.waku?.store.queryHistory({ contentTopics: [this.pollInitTopic] })
return messages return (
messages
?.filter((e): e is WakuMessage & { payload: Uint8Array } => !!e?.payload) ?.filter((e): e is WakuMessage & { payload: Uint8Array } => !!e?.payload)
.map((msg) => PollInit.decode(msg.payload, msg.timestamp)) .map((msg) => PollInit.decode(msg.payload, msg.timestamp))
.filter((poll): poll is PollInitMsg => !!poll) .filter((poll): poll is PollInitMsg => !!poll) ?? []
)
} }
public async sendTimedPollVote( public async sendTimedPollVote(
@ -84,12 +87,26 @@ class WakuVoting {
} }
} }
public async getTimedPollVotes(id: string) { private async getTimedPollsVotes() {
const messages = await this.waku?.store.queryHistory({ contentTopics: [this.timedPollVoteTopic] }) const messages = await this.waku?.store.queryHistory({ contentTopics: [this.timedPollVoteTopic] })
return messages return (
messages
?.filter((e): e is WakuMessage & { payload: Uint8Array } => !!e?.payload) ?.filter((e): e is WakuMessage & { payload: Uint8Array } => !!e?.payload)
.map((msg) => TimedPollVote.decode(msg.payload, msg.timestamp)) .map((msg) => TimedPollVote.decode(msg.payload, msg.timestamp))
.filter((poll): poll is TimedPollVoteMsg => !!poll && poll.id === id) .filter((poll): poll is TimedPollVoteMsg => !!poll) ?? []
)
}
public async getDetailedTimedPolls() {
const polls = await this.getTimedPolls()
const votes = await this.getTimedPollsVotes()
return polls.map(
(poll) =>
new DetailedTimedPoll(
poll,
votes.filter((vote) => vote.id === poll.id)
)
)
} }
} }

View File

@ -0,0 +1,39 @@
import { BigNumber } from 'ethers'
import { PollType } from '../types/PollType'
import { PollInitMsg } from './PollInitMsg'
import { TimedPollVoteMsg } from './TimedPollVoteMsg'
export type TimedPollAnswer = {
text: string
votes: BigNumber
}
export class DetailedTimedPoll {
public answers: TimedPollAnswer[]
public poll: PollInitMsg
public votesMessages: TimedPollVoteMsg[]
constructor(poll: PollInitMsg, answers: TimedPollVoteMsg[]) {
this.poll = poll
const summedAnswers = poll.answers.map((answer) => {
return { text: answer, votes: BigNumber.from(0) }
})
const filteredAnswers: TimedPollVoteMsg[] = []
answers
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1))
.forEach((answer) => {
if (filteredAnswers.findIndex((val) => val.voter === answer.voter) === -1) {
if (poll.pollType === PollType.WEIGHTED && answer.tokenAmount) {
filteredAnswers.push(answer)
summedAnswers[answer.answer].votes = summedAnswers[answer.answer].votes.add(answer.tokenAmount)
}
if (poll.pollType === PollType.NON_WEIGHTED) {
filteredAnswers.push(answer)
summedAnswers[answer.answer].votes = summedAnswers[answer.answer].votes.add(1)
}
}
})
this.votesMessages = filteredAnswers
this.answers = summedAnswers
}
}

View File

@ -24,10 +24,6 @@
"styled-components": "^5.3.0" "styled-components": "^5.3.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@testing-library/react-hooks": "^7.0.1", "@testing-library/react-hooks": "^7.0.1",
"@types/chai": "^4.2.21", "@types/chai": "^4.2.21",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
@ -39,8 +35,6 @@
"@types/styled-components": "^5.1.12", "@types/styled-components": "^5.1.12",
"@typescript-eslint/eslint-plugin": "^4.29.0", "@typescript-eslint/eslint-plugin": "^4.29.0",
"@typescript-eslint/parser": "^4.29.0", "@typescript-eslint/parser": "^4.29.0",
"babel-loader": "^8.2.2",
"babel-preset-minify": "^0.5.1",
"chai": "^4.3.4", "chai": "^4.3.4",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-hooks": "^0.2.0", "eslint-plugin-hooks": "^0.2.0",
@ -52,6 +46,7 @@
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"mocha": "^9.0.3", "mocha": "^9.0.3",
"source-map-loader": "^3.0.0", "source-map-loader": "^3.0.0",
"ts-loader": "^9.2.5",
"ts-node": "^10.1.0", "ts-node": "^10.1.0",
"typescript": "^4.3.5", "typescript": "^4.3.5",
"webpack": "^5.48.0", "webpack": "^5.48.0",

View File

@ -4,7 +4,7 @@ import { Example } from '@status-waku-voting/react-components'
ReactDOM.render( ReactDOM.render(
<div> <div>
<Example /> <Example appName={'testApp'} />
</div>, </div>,
document.getElementById('root') document.getElementById('root')
) )

View File

@ -28,25 +28,9 @@ module.exports = (env) => {
module: { module: {
rules: [ rules: [
{ {
test: /\.[jt]sx?$/, test: /\.tsx?$/,
use: { use: 'ts-loader',
loader: 'babel-loader', exclude: /node_modules/,
options: {
cacheDirectory: true,
babelrc: false,
presets: [
'@babel/preset-typescript',
[
'@babel/preset-env',
{ targets: { browsers: '> 0.25%, not dead' } },
],
'@babel/preset-react',
],
plugins: [
'babel-plugin-styled-components',
],
},
},
}, },
{ {
enforce: 'pre', enforce: 'pre',

View File

@ -17,6 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@status-waku-voting/core": "link:../core", "@status-waku-voting/core": "link:../core",
"ethers": "^5.4.4",
"react": "^17.0.2", "react": "^17.0.2",
"react-components": "^0.5.1" "react-components": "^0.5.1"
}, },

View File

@ -0,0 +1,74 @@
import WakuVoting from '@status-waku-voting/core'
import { DetailedTimedPoll } from '@status-waku-voting/core/dist/esm/src/models/DetailedTimedPoll'
import { Wallet, BigNumber } from 'ethers'
import React, { useEffect, useState } from 'react'
import { JsonRpcSigner } from '@ethersproject/providers'
import { PollType } from '@status-waku-voting/core/dist/esm/src/types/PollType'
import styled from 'styled-components'
type PollProps = {
poll: DetailedTimedPoll
wakuVoting: WakuVoting | undefined
signer: Wallet | JsonRpcSigner
}
export function Poll({ poll, wakuVoting, signer }: PollProps) {
const [selectedAnswer, setSelectedAnswer] = useState(0)
const [tokenAmount, setTokenAmount] = useState(0)
const [address, setAddress] = useState('')
useEffect(() => {
signer.getAddress().then((e) => setAddress(e))
}, [signer])
return (
<div>
{poll.poll.question}
<ul>
{!poll.votesMessages.find((vote) => vote.voter === address) && (
<div onChange={(e) => setSelectedAnswer(Number.parseInt((e.target as any).value ?? 0))}>
{poll.poll.answers.map((answer, idx) => {
return (
<div key={idx}>
<input type="radio" value={idx} name={poll.poll.id} /> {answer}
</div>
)
})}
</div>
)}
{poll.votesMessages.find((vote) => vote.voter === address) &&
poll.answers.map((answer, idx) => {
return (
<div key={idx}>
{answer.text}
<VoteCount>Votes : {answer.votes.toString()}</VoteCount>
</div>
)
})}
</ul>
{poll.poll.pollType === PollType.WEIGHTED && (
<div>
Token amount
<input onChange={(e) => setTokenAmount(Number.parseInt(e.target.value))} value={tokenAmount} type="number" />
</div>
)}
<button
onClick={() => {
if (wakuVoting) {
wakuVoting.sendTimedPollVote(
signer,
poll.poll.id,
selectedAnswer,
poll.poll.pollType === PollType.WEIGHTED ? BigNumber.from(tokenAmount) : undefined
)
}
}}
>
{' '}
Vote
</button>
</div>
)
}
const VoteCount = styled.div`
margin-left: 20px;
`

View File

@ -0,0 +1,36 @@
import WakuVoting from '@status-waku-voting/core'
import { DetailedTimedPoll } from '@status-waku-voting/core/dist/esm/src/models/DetailedTimedPoll'
import { Wallet } from 'ethers'
import React, { useEffect, useState } from 'react'
import { Poll } from './Poll'
import { JsonRpcSigner } from '@ethersproject/providers'
type PollListProps = {
wakuVoting: WakuVoting | undefined
signer: Wallet | JsonRpcSigner
}
export function PollList({ wakuVoting, signer }: PollListProps) {
const [polls, setPolls] = useState<DetailedTimedPoll[]>([])
useEffect(() => {
const interval = setInterval(async () => {
if (wakuVoting) {
setPolls(await wakuVoting.getDetailedTimedPolls())
}
}, 1000)
return () => clearInterval(interval)
}, [wakuVoting])
return (
<ul>
{polls.map((poll) => {
return (
<li key={poll.poll.id}>
<Poll poll={poll} wakuVoting={wakuVoting} signer={signer} />
</li>
)
})}
</ul>
)
}

View File

@ -1,7 +1,78 @@
import React from 'react' import React, { useState, useEffect } from 'react'
import WakuVoting from '@status-waku-voting/core'
import { providers } from 'ethers'
import { PollList } from './PollList'
import styled from 'styled-components'
import { PollType } from '@status-waku-voting/core/dist/esm/src/types/PollType'
function Example() { const provider = new providers.Web3Provider((window as any).ethereum)
return <div>This is example components</div>
type ExampleProps = {
appName: string
} }
function Example({ appName }: ExampleProps) {
const [signer, setSigner] = useState(provider.getSigner())
const [wakuVoting, setWakuVoting] = useState<WakuVoting | undefined>(undefined)
const [answers, setAnswers] = useState<string[]>([])
const [question, setQuestion] = useState('')
const [selectedType, setSelectedType] = useState(PollType.WEIGHTED)
useEffect(() => {
;(window as any).ethereum.on('accountsChanged', async () => setSigner(provider.getSigner()))
WakuVoting.create(appName, '0x01').then((e) => setWakuVoting(e))
}, [])
return (
<Wrapper>
<PollList wakuVoting={wakuVoting} signer={signer} />
<NewPollBox>
Question
<input value={question} onChange={(e) => setQuestion(e.target.value)} />
Answers
<button onClick={() => setAnswers((answers) => [...answers, ''])}>Add answer</button>
{answers.map((answer, idx) => (
<input
key={idx}
value={answer}
onChange={(e) =>
setAnswers((answers) => {
const newAnswers = [...answers]
newAnswers[idx] = e.target.value
return newAnswers
})
}
/>
))}
<div onChange={(e) => setSelectedType(Number.parseInt((e.target as any).value))}>
<input type="radio" value={PollType.WEIGHTED} name="newPollType" /> Weighted
<input type="radio" value={PollType.NON_WEIGHTED} name="newPollType" /> Non weighted
</div>
<button
onClick={async () => {
await wakuVoting?.createTimedPoll(signer, question, answers, selectedType)
setAnswers([])
}}
>
Send
</button>
</NewPollBox>
</Wrapper>
)
}
const Wrapper = styled.div`
display: flex;
`
const NewPollBox = styled.div`
display: flex;
flex-direction: column;
margin: 20px;
padding: 20px;
box-shadow: 10px 10px 31px -2px #a3a1a1;
`
export { Example } export { Example }

1236
yarn.lock

File diff suppressed because it is too large Load Diff