Browse Source

feat: init poll view

develop
Apostolos Fanakis 4 years ago
parent
commit
8bc5855a3b
Signed by: Apostolof GPG Key ID: 8600B4C4163B3269
  1. 2
      packages/concordia-app/package.json
  2. 5
      packages/concordia-app/public/locales/en/translation.json
  3. 82
      packages/concordia-app/src/components/PollView/PollGraph/index.jsx
  4. 62
      packages/concordia-app/src/components/PollView/PollVote/index.jsx
  5. 99
      packages/concordia-app/src/components/PollView/index.jsx
  6. 2
      packages/concordia-app/src/constants/polls/PollGraph.js
  7. 16
      packages/concordia-app/src/constants/polls/PollTabs.js
  8. 49
      packages/concordia-app/src/views/Topic/TopicView/index.jsx
  9. 79
      yarn.lock

2
packages/concordia-app/package.json

@ -29,6 +29,7 @@
"@ezerous/eth-identity-provider": "~0.1.2",
"@reduxjs/toolkit": "~1.4.0",
"@welldone-software/why-did-you-render": "~6.0.5",
"apexcharts": "^3.26.0",
"concordia-contracts": "~0.1.0",
"concordia-shared": "~0.1.0",
"i18next": "^19.8.3",
@ -37,6 +38,7 @@
"lodash": "^4.17.20",
"prop-types": "~15.7.2",
"react": "~16.13.1",
"react-apexcharts": "^1.3.7",
"react-avatar": "~3.9.7",
"react-copy-to-clipboard": "^5.0.3",
"react-dom": "~16.13.1",

5
packages/concordia-app/public/locales/en/translation.json

@ -74,6 +74,11 @@
"topic.create.form.subject.field.label": "Topic subject",
"topic.create.form.subject.field.placeholder": "Subject",
"topic.list.row.topic.id": "#{{id}}",
"topic.poll.tab.graph.title": "Results",
"topic.poll.tab.results.votes.count": "{{totalVotes}} votes casted",
"topic.poll.tab.vote.form.button.submit": "Submit",
"topic.poll.tab.vote.form.radio.label": "Select one of the options:",
"topic.poll.tab.vote.title": "Vote",
"username.selector.error.username.empty.message": "Username is required",
"username.selector.error.username.taken.message": "The username {{username}} is already taken.",
"username.selector.username.field.label": "Username",

82
packages/concordia-app/src/components/PollView/PollGraph/index.jsx

@ -0,0 +1,82 @@
import React, { useMemo } from 'react';
import Chart from 'react-apexcharts';
import { Grid, Header } from 'semantic-ui-react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { CASTED_OPTION_COLOR, DEFAULT_OPTION_COLOR } from '../../../constants/polls/PollGraph';
const PollGraph = (props) => {
const {
pollOptions, voteCounts, hasUserVoted, userVoteHash,
} = props;
const { t } = useTranslation();
const chartOptions = useMemo(() => ({
chart: {
id: 'topic-poll',
},
plotOptions: {
bar: {
horizontal: true,
},
},
colors: [
(value) => {
if (hasUserVoted && pollOptions[value.dataPointIndex].hash === userVoteHash) {
return CASTED_OPTION_COLOR;
}
return DEFAULT_OPTION_COLOR;
},
],
xaxis: {
categories: pollOptions.map((pollOption) => pollOption.label),
},
}), [hasUserVoted, pollOptions, userVoteHash]);
const chartSeries = useMemo(() => [{
name: 'votes',
data: voteCounts,
}], [voteCounts]);
return (
<Grid columns="equal">
<Grid.Row>
<Grid.Column />
<Grid.Column width={8}>
<Chart
options={chartOptions}
series={chartSeries}
type="bar"
/>
</Grid.Column>
<Grid.Column />
</Grid.Row>
<Grid.Row>
<Grid.Column textAlign="center">
<Header as="h4">
{t('topic.poll.tab.results.votes.count', {
totalVotes: voteCounts.reduce((accumulator, voteCount) => accumulator + voteCount, 0),
})}
</Header>
</Grid.Column>
</Grid.Row>
</Grid>
);
};
PollGraph.defaultProps = {
hasUserVoted: false,
userVoteHash: '',
};
PollGraph.propTypes = {
pollOptions: PropTypes.arrayOf(PropTypes.exact({
label: PropTypes.string,
hash: PropTypes.string,
})).isRequired,
voteCounts: PropTypes.arrayOf(PropTypes.number).isRequired,
hasUserVoted: PropTypes.bool,
userVoteHash: PropTypes.string,
};
export default PollGraph;

62
packages/concordia-app/src/components/PollView/PollVote/index.jsx

@ -0,0 +1,62 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Form } from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
const PollVote = (props) => {
const {
pollOptions, enableVoteChanges, hasUserVoted, userVoteHash,
} = props;
const [selectedOptionHash, setSelectedOptionHash] = useState(userVoteHash);
const { t } = useTranslation();
const onOptionSelected = (e, { value }) => {
setSelectedOptionHash(value);
};
const onCastVote = () => {
console.log('vote');
// TODO
// TODO: callback for immediate poll data refresh on vote?
};
return (
<Form onSubmit={onCastVote}>
<Form.Group grouped>
<label htmlFor="poll">{t('topic.poll.tab.vote.form.radio.label')}</label>
{pollOptions.map((pollOption) => (
<Form.Radio
key={pollOption.hash}
label={pollOption.label}
value={pollOption.hash}
checked={pollOption.hash === selectedOptionHash}
disabled={hasUserVoted && !enableVoteChanges && pollOption.hash !== selectedOptionHash}
onChange={onOptionSelected}
/>
))}
</Form.Group>
<Form.Button
type="submit"
disabled={(hasUserVoted && !enableVoteChanges) || (selectedOptionHash === userVoteHash)}
>
{t('topic.poll.tab.vote.form.button.submit')}
</Form.Button>
</Form>
);
};
PollVote.defaultProps = {
userVoteHash: '',
};
PollVote.propTypes = {
pollOptions: PropTypes.arrayOf(PropTypes.exact({
label: PropTypes.string,
hash: PropTypes.string,
})).isRequired,
enableVoteChanges: PropTypes.bool.isRequired,
hasUserVoted: PropTypes.bool.isRequired,
userVoteHash: PropTypes.string,
};
export default PollVote;

99
packages/concordia-app/src/components/PollView/index.jsx

@ -0,0 +1,99 @@
import React, { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import {
Container, Header, Icon, Tab,
} from 'semantic-ui-react';
import { useTranslation } from 'react-i18next';
import { drizzle } from '../../redux/store';
import PollGraph from './PollGraph';
import CustomLoadingTabPane from '../CustomLoadingTabPane';
import { GRAPH_TAB, VOTE_TAB } from '../../constants/polls/PollTabs';
import PollVote from './PollVote';
const { contracts: { [VOTING_CONTRACT]: { methods: { pollExists: { cacheCall: pollExistsChainData } } } } } = drizzle;
const hashOption = (val) => {
let hash = 0;
let i;
let chr;
for (i = 0; i < val.length; i++) {
chr = val.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0;
}
return `${hash}`;
};
const PollView = (props) => {
const { topicId } = props;
const userAddress = useSelector((state) => state.user.address);
const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
const getPollInfoResults = useSelector((state) => state.contracts[VOTING_CONTRACT].getPollInfo);
const [getPollInfoCallHash, setGetPollInfoCallHash] = useState(null);
const [pollOptions, setPollOptions] = useState([
{
label: 'option 1',
hash: hashOption('option 1'),
},
{
label: 'option 2',
hash: hashOption('option 2'),
},
{
label: 'option 3',
hash: hashOption('option 3'),
},
{
label: 'option 4',
hash: hashOption('option 4'),
},
{
label: 'option 5',
hash: hashOption('option 5'),
},
]);
const [voteCounts, setVoteCounts] = useState([2, 8, 4, 12, 7]);
const [voteLoading, setVoteLoading] = useState(false);
const [resultsLoading, setResultsLoading] = useState(false);
const { t } = useTranslation();
// TODO: get vote options
// TODO: get current results
// TODO: check poll hash validity, add invalid view
const pollVoteTab = useMemo(() => (
<PollVote pollOptions={pollOptions} enableVoteChanges hasUserVoted userVoteHash={pollOptions[2].hash} />
), [pollOptions]);
const pollGraphTab = useMemo(() => (
<PollGraph pollOptions={pollOptions} voteCounts={voteCounts} hasUserVoted userVoteHash={pollOptions[2].hash} />
), [pollOptions, voteCounts]);
const panes = useMemo(() => {
const pollVotePane = (<CustomLoadingTabPane loading={voteLoading}>{pollVoteTab}</CustomLoadingTabPane>);
const pollGraphPane = (<CustomLoadingTabPane loading={resultsLoading}>{pollGraphTab}</CustomLoadingTabPane>);
return ([
{ menuItem: t(VOTE_TAB.intl_display_name_id), render: () => pollVotePane },
{ menuItem: t(GRAPH_TAB.intl_display_name_id), render: () => pollGraphPane },
]);
}, [pollGraphTab, pollVoteTab, resultsLoading, t, voteLoading]);
return (
<Container id="topic-poll-container" textAlign="left">
<Header as="h3">
<Icon name="chart pie" size="large" />
Do you thing asdf or fdsa?
</Header>
<Tab panes={panes} />
</Container>
);
};
export default PollView;

2
packages/concordia-app/src/constants/polls/PollGraph.js

@ -0,0 +1,2 @@
export const DEFAULT_OPTION_COLOR = '#3B5066';
export const CASTED_OPTION_COLOR = '#0b2540';

16
packages/concordia-app/src/constants/polls/PollTabs.js

@ -0,0 +1,16 @@
export const VOTE_TAB = {
id: 'vote-tab',
intl_display_name_id: 'topic.poll.vote.tab.title',
};
export const GRAPH_TAB = {
id: 'graph-tab',
intl_display_name_id: 'topic.poll.graph.tab.title',
};
const pollTabs = [
VOTE_TAB,
GRAPH_TAB,
];
export default pollTabs;

49
packages/concordia-app/src/views/Topic/TopicView/index.jsx

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import {
@ -6,19 +6,25 @@ import {
} from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { useHistory } from 'react-router';
import { FORUM_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { FORUM_CONTRACT, VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames';
import { TOPICS_DATABASE, USER_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases';
import ReactMarkdown from 'react-markdown';
import { breeze, drizzle } from '../../../redux/store';
import { FETCH_USER_DATABASE } from '../../../redux/actions/peerDbReplicationActions';
import './styles.css';
import TopicPostList from './TopicPostList';
import PollView from '../../../components/PollView';
import determineKVAddress from '../../../utils/orbitUtils';
import { TOPIC_SUBJECT } from '../../../constants/orbit/TopicsDatabaseKeys';
import PostCreate from '../../../components/PostCreate';
import targetBlank from '../../../utils/markdownUtils';
const { contracts: { [FORUM_CONTRACT]: { methods: { getTopic: { cacheCall: getTopicChainData } } } } } = drizzle;
const {
contracts: {
[FORUM_CONTRACT]: { methods: { getTopic: { cacheCall: getTopicChainData } } },
[VOTING_CONTRACT]: { methods: { pollExists: { cacheCall: pollExistsChainData } } },
},
} = drizzle;
const { orbit } = breeze;
const TopicView = (props) => {
@ -29,15 +35,18 @@ const TopicView = (props) => {
const userAddress = useSelector((state) => state.user.address);
const hasSignedUp = useSelector((state) => state.user.hasSignedUp);
const getTopicResults = useSelector((state) => state.contracts[FORUM_CONTRACT].getTopic);
const pollExistsResults = useSelector((state) => state.contracts[VOTING_CONTRACT].pollExists);
const topics = useSelector((state) => state.orbitData.topics);
const users = useSelector((state) => state.orbitData.users);
const [getTopicCallHash, setGetTopicCallHash] = useState([]);
const [getTopicCallHash, setGetTopicCallHash] = useState(null);
const [pollExistsCallHash, setPollExistsCallHash] = useState(null);
const [topicAuthorAddress, setTopicAuthorAddress] = useState(initialTopicAuthorAddress || null);
const [topicAuthor, setTopicAuthor] = useState(initialTopicAuthor || null);
const [timestamp, setTimestamp] = useState(initialTimestamp || null);
const [postIds, setPostIds] = useState(initialPostIds || null);
const [numberOfReplies, setReplyCount] = useState(0);
const [topicSubject, setTopicSubject] = useState(null);
const [hasPoll, setHasPoll] = useState(false);
const history = useHistory();
const dispatch = useDispatch();
@ -52,6 +61,10 @@ const TopicView = (props) => {
}
}, [postIds, timestamp, topicAuthor, topicAuthorAddress, topicId]);
useEffect(() => {
setPollExistsCallHash(pollExistsChainData(topicId));
}, [topicId]);
useEffect(() => {
if (getTopicCallHash && getTopicResults && getTopicResults[getTopicCallHash]) {
if (getTopicResults[getTopicCallHash].value == null) {
@ -62,9 +75,9 @@ const TopicView = (props) => {
setTopicAuthorAddress(getTopicResults[getTopicCallHash].value[0]);
setTopicAuthor(getTopicResults[getTopicCallHash].value[1]);
setTimestamp(getTopicResults[getTopicCallHash].value[2] * 1000);
const postIds = getTopicResults[getTopicCallHash].value[3].map((postId) => parseInt(postId, 10));
setPostIds(postIds);
setReplyCount(postIds.length - 1);
const fetchedPostIds = getTopicResults[getTopicCallHash].value[3].map((postId) => parseInt(postId, 10));
setPostIds(fetchedPostIds);
setReplyCount(fetchedPostIds.length - 1);
const topicFound = topics
.find((topic) => topic.id === topicId);
@ -80,6 +93,12 @@ const TopicView = (props) => {
}
}, [dispatch, getTopicCallHash, getTopicResults, history, topicId, topics, userAddress]);
useEffect(() => {
if (pollExistsCallHash && pollExistsResults && pollExistsResults[pollExistsCallHash]) {
setHasPoll(pollExistsResults[pollExistsCallHash].value);
}
}, [pollExistsCallHash, pollExistsResults, topicId]);
useEffect(() => {
if (topicAuthorAddress !== null) {
determineKVAddress({ orbit, dbName: USER_DATABASE, userAddress: topicAuthorAddress })
@ -111,6 +130,8 @@ const TopicView = (props) => {
}
}, [topicId, topics]);
const poll = useMemo(() => hasPoll && <PollView />, [hasPoll]);
const stopClickPropagation = (event) => {
event.stopPropagation();
};
@ -147,7 +168,9 @@ const TopicView = (props) => {
&nbsp;&nbsp;&nbsp;
<Icon name="user" fitted />
&nbsp;
<Link to={`/users/${topicAuthorAddress}`} onClick={stopClickPropagation}>{ topicAuthor }</Link>
<Link to={`/users/${topicAuthorAddress}`} onClick={stopClickPropagation}>
{topicAuthor}
</Link>
&nbsp;&nbsp;&nbsp;
<Icon name="reply" fitted />
&nbsp;
@ -155,6 +178,16 @@ const TopicView = (props) => {
</div>
</div>
<Divider />
{
hasPoll && (
<>
<div id="topic-poll">
{poll}
</div>
<Divider />
</>
)
}
</Dimmer.Dimmable>
<TopicPostList topicId={topicId} loading={postIds === null} focusOnPost={focusOnPost} />
</Segment>

79
yarn.lock

@ -3084,6 +3084,18 @@ anymatch@~3.1.1:
normalize-path "^3.0.0"
picomatch "^2.0.4"
apexcharts@^3.26.0:
version "3.26.0"
resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.26.0.tgz#a78abc108b2e1b3086a738f0ec7c98e292f4a14b"
integrity sha512-zdYHs3k3tgmCn1BpYLj7rhGEndBYF33Pq1+g0ora37xAr+3act5CJrpdXM2jx2boVUyXgavoSp6sa8WpK7RkSA==
dependencies:
svg.draggable.js "^2.2.2"
svg.easing.js "^2.0.0"
svg.filter.js "^2.0.2"
svg.pathmorphing.js "^0.1.3"
svg.resize.js "^1.4.3"
svg.select.js "^3.0.1"
app-module-path@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5"
@ -14910,7 +14922,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@~15.7.2:
prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@~15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -15185,6 +15197,13 @@ rc@^1.2.7, rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-apexcharts@^1.3.7:
version "1.3.7"
resolved "https://registry.yarnpkg.com/react-apexcharts/-/react-apexcharts-1.3.7.tgz#42c8785e260535a4f8db1aadbe7b66552770944d"
integrity sha512-2OFhEHd70/WHN0kmrJtVx37UfaL71ZogVkwezmDqwQWgwhK6upuhlnEEX7tEq4xvjA+RFDn6hiUTNIuC/Q7Zqw==
dependencies:
prop-types "^15.5.7"
react-app-polyfill@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz#890f8d7f2842ce6073f030b117de9130a5f385f0"
@ -16469,6 +16488,9 @@ snake-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==
dependencies:
dot-case "^3.0.4"
tslib "^2.0.3"
snapdragon-node@^2.0.1:
version "2.1.1"
@ -17163,6 +17185,61 @@ svg-parser@^2.0.0:
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
svg.draggable.js@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba"
integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==
dependencies:
svg.js "^2.0.1"
svg.easing.js@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/svg.easing.js/-/svg.easing.js-2.0.0.tgz#8aa9946b0a8e27857a5c40a10eba4091e5691f12"
integrity sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=
dependencies:
svg.js ">=2.3.x"
svg.filter.js@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/svg.filter.js/-/svg.filter.js-2.0.2.tgz#91008e151389dd9230779fcbe6e2c9a362d1c203"
integrity sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=
dependencies:
svg.js "^2.2.5"
svg.js@>=2.3.x, svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5:
version "2.7.1"
resolved "https://registry.yarnpkg.com/svg.js/-/svg.js-2.7.1.tgz#eb977ed4737001eab859949b4a398ee1bb79948d"
integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==
svg.pathmorphing.js@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz#c25718a1cc7c36e852ecabc380e758ac09bb2b65"
integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==
dependencies:
svg.js "^2.4.0"
svg.resize.js@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/svg.resize.js/-/svg.resize.js-1.4.3.tgz#885abd248e0cd205b36b973c4b578b9a36f23332"
integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==
dependencies:
svg.js "^2.6.5"
svg.select.js "^2.1.2"
svg.select.js@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-2.1.2.tgz#e41ce13b1acff43a7441f9f8be87a2319c87be73"
integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==
dependencies:
svg.js "^2.2.5"
svg.select.js@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-3.0.1.tgz#a4198e359f3825739226415f82176a90ea5cc917"
integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==
dependencies:
svg.js "^2.6.5"
svgo@^1.0.0, svgo@^1.2.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"

Loading…
Cancel
Save