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 { forumContractEvents } = require('./ForumContractEvents'); |
||||
const { postVotingContractEvents } = require('./PostVotingContractEvents'); |
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({ |
const appEvents = Object.freeze({ |
||||
[FORUM_CONTRACT]: forumContractEvents, |
[FORUM_CONTRACT]: forumContractEvents, |
||||
[POST_VOTING_CONTRACT]: postVotingContractEvents, |
[POST_VOTING_CONTRACT]: postVotingContractEvents, |
||||
|
[VOTING_CONTRACT]: votingContractEvents, |
||||
}); |
}); |
||||
|
|
||||
module.exports = appEvents; |
module.exports = appEvents; |
||||
|
Loading…
Reference in new issue