mirror of https://gitlab.com/ecentrics/concordia
Ezerous
4 years ago
18 changed files with 407 additions and 20 deletions
@ -0,0 +1,50 @@ |
|||
import React, { memo } from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { |
|||
Button, Form, Icon, Input, |
|||
} from 'semantic-ui-react'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import './styles.css'; |
|||
|
|||
const PollOption = (props) => { |
|||
const { |
|||
id, removable, onChange, onRemove, |
|||
} = props; |
|||
const { t } = useTranslation(); |
|||
|
|||
return ( |
|||
<Form.Field className="form-poll-option" required> |
|||
<label className="form-topic-create-header" htmlFor="form-poll-create-field-subject"> |
|||
{t('poll.create.option.field.label', { id })} |
|||
</label> |
|||
<Input |
|||
id="form-poll-create-field-subject" |
|||
placeholder={t('poll.create.option.field.placeholder', { id })} |
|||
name="pollQuestionInput" |
|||
className="form-input" |
|||
onChange={(e) => onChange(e, id)} |
|||
/> |
|||
{removable |
|||
&& ( |
|||
<Button |
|||
className="remove-option-button" |
|||
key={`form-remove-option-button-${id}`} |
|||
negative |
|||
icon |
|||
onClick={(e) => onRemove(e, id)} |
|||
> |
|||
<Icon name="x" /> |
|||
</Button> |
|||
)} |
|||
</Form.Field> |
|||
); |
|||
}; |
|||
|
|||
PollOption.propTypes = { |
|||
id: PropTypes.number.isRequired, |
|||
onChange: PropTypes.func, |
|||
onRemove: PropTypes.func, |
|||
removable: PropTypes.bool, |
|||
}; |
|||
|
|||
export default memo(PollOption); |
@ -0,0 +1,4 @@ |
|||
.form-poll-option > .form-input{ |
|||
width: 50% !important; |
|||
margin-right: 0.25em; |
|||
} |
@ -0,0 +1,190 @@ |
|||
import React, { |
|||
useMemo, useState, useCallback, useEffect, forwardRef, useImperativeHandle, |
|||
} from 'react'; |
|||
import PropTypes from 'prop-types'; |
|||
import { useSelector } from 'react-redux'; |
|||
import { useTranslation } from 'react-i18next'; |
|||
import { |
|||
Button, Checkbox, Form, Icon, Input, |
|||
} from 'semantic-ui-react'; |
|||
import { VOTING_CONTRACT } from 'concordia-shared/src/constants/contracts/ContractNames'; |
|||
import { POLL_CREATED_EVENT } from 'concordia-shared/src/constants/contracts/events/VotingContractEvents'; |
|||
import { POLLS_DATABASE } from 'concordia-shared/src/constants/orbit/OrbitDatabases'; |
|||
import PollOption from './PollOption'; |
|||
import { breeze, drizzle } from '../../redux/store'; |
|||
import { TRANSACTION_ERROR, TRANSACTION_SUCCESS } from '../../constants/TransactionStatus'; |
|||
import './styles.css'; |
|||
import { POLL_OPTIONS, POLL_QUESTION } from '../../constants/orbit/PollsDatabaseKeys'; |
|||
import generateHash from '../../utils/hashUtils'; |
|||
|
|||
const { contracts: { [VOTING_CONTRACT]: { methods: { createPoll } } } } = drizzle; |
|||
const { orbit: { stores } } = breeze; |
|||
|
|||
const PollCreate = forwardRef((props, ref) => { |
|||
const { account, onChange, onCreated } = props; |
|||
const transactionStack = useSelector((state) => state.transactionStack); |
|||
const transactions = useSelector((state) => state.transactions); |
|||
const [createPollCacheSendStackId, setCreatePollCacheSendStackId] = useState(''); |
|||
const [question, setQuestion] = useState(''); |
|||
const [options, setOptions] = useState( |
|||
[{ id: 1 }, { id: 2 }], |
|||
); |
|||
const [optionValues, setOptionValues] = useState( |
|||
['', ''], |
|||
); |
|||
const [optionsNextId, setOptionsNextId] = useState(3); |
|||
const [allowVoteChanges, setAllowVoteChanges] = useState(false); |
|||
const [creating, setCreating] = useState(false); |
|||
const [errored, setErrored] = useState(false); |
|||
const { t } = useTranslation(); |
|||
|
|||
const handlePollQuestionChange = useCallback((event) => { |
|||
const newQuestion = event.target.value.trim(); |
|||
if (newQuestion !== question) setQuestion(newQuestion); |
|||
}, [question]); |
|||
|
|||
const addOption = useCallback((e) => { |
|||
e.currentTarget.blur(); |
|||
const newOptions = [...options, { id: optionsNextId, removable: true }]; |
|||
const newOptionValues = [...optionValues, '']; |
|||
setOptionsNextId(optionsNextId + 1); |
|||
setOptions(newOptions); |
|||
setOptionValues(newOptionValues); |
|||
}, [optionValues, options, optionsNextId]); |
|||
|
|||
const removeOption = useCallback((e, id) => { |
|||
e.currentTarget.blur(); |
|||
const newOptions = [...options]; |
|||
newOptions.splice(id - 1, 1); |
|||
const newOptionValues = [...optionValues]; |
|||
newOptionValues.splice(id - 1, 1); |
|||
setOptions(newOptions); |
|||
setOptionValues(newOptionValues); |
|||
}, [optionValues, options]); |
|||
|
|||
const handlePollOptionChange = useCallback((event, id) => { |
|||
const newValue = event.target.value.trim(); |
|||
if (newValue !== optionValues[id - 1]) { |
|||
const newOptionValues = [...optionValues]; |
|||
newOptionValues[id - 1] = newValue; |
|||
setOptionValues(newOptionValues); |
|||
} |
|||
}, [optionValues]); |
|||
|
|||
const pollOptions = useMemo(() => options |
|||
.map((option, index) => { |
|||
const { id, removable } = option; |
|||
return ( |
|||
<PollOption |
|||
id={index + 1} |
|||
key={id} |
|||
removable={removable} |
|||
onRemove={removeOption} |
|||
onChange={handlePollOptionChange} |
|||
/> |
|||
); |
|||
}), [handlePollOptionChange, options, removeOption]); |
|||
|
|||
useEffect(() => { |
|||
onChange({ question, optionValues }); |
|||
}, [onChange, optionValues, question]); |
|||
|
|||
const handleCheckboxChange = useCallback((event, data) => { |
|||
setAllowVoteChanges(data.checked); |
|||
}, []); |
|||
|
|||
useImperativeHandle(ref, () => ({ |
|||
createPoll(topicId) { |
|||
setCreating(true); |
|||
const dataHash = generateHash(JSON.stringify({ question, optionValues })); |
|||
setCreatePollCacheSendStackId(createPoll.cacheSend( |
|||
...[topicId, options.length, dataHash, allowVoteChanges], { from: account }, |
|||
)); |
|||
}, |
|||
pollCreating() { |
|||
return creating; |
|||
}, |
|||
pollErrored() { |
|||
return errored; |
|||
}, |
|||
})); |
|||
|
|||
useEffect(() => { |
|||
if (creating && transactionStack && transactionStack[createPollCacheSendStackId] |
|||
&& transactions[transactionStack[createPollCacheSendStackId]]) { |
|||
if (transactions[transactionStack[createPollCacheSendStackId]].status === TRANSACTION_ERROR) { |
|||
setErrored(true); |
|||
setCreating(false); |
|||
onCreated(false); |
|||
} else if (transactions[transactionStack[createPollCacheSendStackId]].status === TRANSACTION_SUCCESS) { |
|||
const { |
|||
receipt: { |
|||
events: { |
|||
[POLL_CREATED_EVENT]: { |
|||
returnValues: { |
|||
topicID: topicId, |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
} = transactions[transactionStack[createPollCacheSendStackId]]; |
|||
|
|||
const pollsDb = Object.values(stores).find((store) => store.dbname === POLLS_DATABASE); |
|||
|
|||
pollsDb |
|||
.put(topicId, { [POLL_QUESTION]: question, [POLL_OPTIONS]: optionValues }) |
|||
.then(() => { |
|||
onCreated(topicId); |
|||
}) |
|||
.catch((reason) => { |
|||
console.error(reason); |
|||
setErrored(true); |
|||
setCreating(false); |
|||
onCreated(false); |
|||
}); |
|||
} |
|||
} |
|||
}, [createPollCacheSendStackId, creating, onCreated, optionValues, question, transactionStack, transactions]); |
|||
|
|||
return useMemo(() => ( |
|||
<div className="poll-create"> |
|||
<Form.Field required> |
|||
<label className="form-topic-create-header" htmlFor="form-poll-create-field-subject"> |
|||
{t('poll.create.question.field.label')} |
|||
</label> |
|||
<Input |
|||
id="form-poll-create-field-subject" |
|||
placeholder={t('poll.create.question.field.placeholder')} |
|||
name="pollQuestionInput" |
|||
className="form-input" |
|||
onChange={handlePollQuestionChange} |
|||
/> |
|||
</Form.Field> |
|||
<Form.Field> |
|||
<Checkbox |
|||
label={t('poll.create.allow.vote.changes.field.label')} |
|||
onClick={handleCheckboxChange} |
|||
/> |
|||
</Form.Field> |
|||
{pollOptions} |
|||
<Button |
|||
id="add-option-button" |
|||
key="form-add-option-button" |
|||
positive |
|||
icon |
|||
labelPosition="left" |
|||
onClick={addOption} |
|||
> |
|||
<Icon name="plus" /> |
|||
{t('poll.create.add.option.button')} |
|||
</Button> |
|||
</div> |
|||
), [addOption, handleCheckboxChange, handlePollQuestionChange, pollOptions, t]); |
|||
}); |
|||
|
|||
PollCreate.propTypes = { |
|||
onChange: PropTypes.func, |
|||
onCreated: PropTypes.func, |
|||
}; |
|||
|
|||
export default PollCreate; |
@ -0,0 +1,10 @@ |
|||
.poll-create { |
|||
padding-top: 1em; |
|||
padding-bottom: 1em; |
|||
} |
|||
|
|||
.poll-create .checkbox > label, label:focus, label:hover{ |
|||
color: white !important; |
|||
font-weight: 700; |
|||
} |
|||
|
@ -0,0 +1,9 @@ |
|||
export const POLL_QUESTION = 'question'; |
|||
export const POLL_OPTIONS = 'options'; |
|||
|
|||
const pollsDatabaseKeys = [ |
|||
POLL_QUESTION, |
|||
POLL_OPTIONS, |
|||
]; |
|||
|
|||
export default pollsDatabaseKeys; |
@ -0,0 +1,7 @@ |
|||
import sha256 from 'crypto-js/sha256'; |
|||
|
|||
function generateHash(message) { |
|||
return sha256(message).toString().substring(0, 16); |
|||
} |
|||
|
|||
export default generateHash; |
@ -0,0 +1,13 @@ |
|||
const POLL_CREATED_EVENT = 'PollCreated'; |
|||
const USER_VOTED_POLL_EVENT = 'UserVotedPoll'; |
|||
|
|||
const votingContractEvents = Object.freeze([ |
|||
POLL_CREATED_EVENT, |
|||
USER_VOTED_POLL_EVENT, |
|||
]); |
|||
|
|||
module.exports = { |
|||
POLL_CREATED_EVENT, |
|||
USER_VOTED_POLL_EVENT, |
|||
votingContractEvents, |
|||
}; |
@ -1,10 +1,12 @@ |
|||
const { forumContractEvents } = require('./ForumContractEvents'); |
|||
const { postVotingContractEvents } = require('./PostVotingContractEvents'); |
|||
const { FORUM_CONTRACT, POST_VOTING_CONTRACT } = require('../ContractNames'); |
|||
const { votingContractEvents } = require('./VotingContractEvents'); |
|||
const { FORUM_CONTRACT, POST_VOTING_CONTRACT, VOTING_CONTRACT } = require('../ContractNames'); |
|||
|
|||
const appEvents = Object.freeze({ |
|||
[FORUM_CONTRACT]: forumContractEvents, |
|||
[POST_VOTING_CONTRACT]: postVotingContractEvents, |
|||
[VOTING_CONTRACT]: votingContractEvents, |
|||
}); |
|||
|
|||
module.exports = appEvents; |
|||
|
Loading…
Reference in new issue