diff --git a/app/components/proposal-list/component.js b/app/components/proposal-list/component.js index 272e28e..1ce555d 100644 --- a/app/components/proposal-list/component.js +++ b/app/components/proposal-list/component.js @@ -1,20 +1,30 @@ import Component from '@ember/component'; +import { inject } from '@ember/service'; +import { empty } from '@ember/object/computed'; export default Component.extend({ + kredits: inject(), tagName: 'ul', classNames: ['proposal-list'], + selectedProposals: [], + submitButtonDisabled: empty('selectedProposals'), + actions: { + confirm() { + this.confirmProposals(this.selectedProposals) + .then(() => { + this.selectedProposals = []; + }); + }, - confirm(proposalId) { - if (this.contractInteractionEnabled) { - this.confirmProposal(proposalId); + toggleSelect(proposalId, selected) { + if (selected) { + this.selectedProposals.removeObject(proposalId); } else { - window.alert('Only members can vote on proposals. Please ask someone to set you up.'); + this.selectedProposals.addObject(proposalId); } - } - + }, } - }); diff --git a/app/components/proposal-list/item/component.js b/app/components/proposal-list/item/component.js new file mode 100644 index 0000000..66c000e --- /dev/null +++ b/app/components/proposal-list/item/component.js @@ -0,0 +1,29 @@ +import Component from '@ember/component'; +import tag from 'ember-awesome-macros/tag'; +import { computed } from '@ember/object'; +import { reads } from '@ember/object/computed'; +import { inject } from '@ember/service'; + +export default Component.extend({ + kredits: inject(), + + tagName: 'li', + classNames: ['proposal-list-item'], + + attributeBindings: ['data-proposal-id', 'title'], + + title: tag`(${'proposal.kind'}) ${'proposal.description'}`, + 'data-proposal-id': reads('proposal.id'), + + canBeVoted: computed('voterIds.[]', 'kredits.currentUser', function() { + let { isExecuted, voterIds } = this.proposal; + let { currentUser } = this.kredits; + voterIds = voterIds.map((id) => id.toString()); + + return currentUser.isCore && !isExecuted && !voterIds.includes(currentUser.id); + }), + + selected: computed('selectedProposals.[]', function() { + return this.selectedProposals.includes(this.proposal.id); + }) +}); diff --git a/app/components/proposal-list/item/template.hbs b/app/components/proposal-list/item/template.hbs new file mode 100644 index 0000000..75ab52e --- /dev/null +++ b/app/components/proposal-list/item/template.hbs @@ -0,0 +1,8 @@ +♥ +{{proposal.amount}}₭S +for {{proposal.contributor.name}} +({{proposal.votesCount}}/{{proposal.votesNeeded}} votes) + +{{#if canBeVoted}} + +{{/if}} diff --git a/app/components/proposal-list/template.hbs b/app/components/proposal-list/template.hbs index 87bdbae..24ca50f 100644 --- a/app/components/proposal-list/template.hbs +++ b/app/components/proposal-list/template.hbs @@ -1,12 +1,14 @@ {{#each proposals as |proposal|}} -
+ +
+{{/if}} diff --git a/app/controllers/index.js b/app/controllers/index.js index 10c8920..868b76d 100644 --- a/app/controllers/index.js +++ b/app/controllers/index.js @@ -1,6 +1,7 @@ import Controller from '@ember/controller'; import { alias, filter, filterBy, sort } from '@ember/object/computed'; import { inject as injectService } from '@ember/service'; +import RSVP from 'rsvp'; export default Controller.extend({ kredits: injectService(), @@ -20,10 +21,17 @@ export default Controller.extend({ proposalsOpenSorted: sort('proposalsOpen', 'proposalsSorting'), actions: { - confirmProposal(proposalId) { - this.kredits.vote(proposalId).then(transaction => { - window.confirm('Vote submitted to Ethereum blockhain: '+transaction.hash); - }); + confirmProposals(proposalIds) { + if (this.kredits.currentUser.isCore) { + return this.kredits.batchVote(proposalIds) + .then((transaction) => { + window.confirm('Vote submitted to Ethereum blockhain: '+transaction.hash); + return transaction; + }); + } else { + window.alert('Only members can vote on proposals. Please ask someone to set you up.'); + return RSVP.reject(); + } }, save(contributor) { diff --git a/app/services/kredits.js b/app/services/kredits.js index 15ed22a..d37d401 100644 --- a/app/services/kredits.js +++ b/app/services/kredits.js @@ -4,7 +4,7 @@ import RSVP from 'rsvp'; import Service from '@ember/service'; import { computed } from '@ember/object'; -import { alias, notEmpty } from '@ember/object/computed'; +import { notEmpty } from '@ember/object/computed'; import { isEmpty } from '@ember/utils'; import config from 'kredits-web/config/environment'; @@ -16,8 +16,6 @@ export default Service.extend({ currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded. currentUser: null, - currentUserIsContributor: notEmpty('currentUser'), - currentUserIsCore: alias('currentUser.isCore'), hasAccounts: notEmpty('currentUserAccounts'), accountNeedsUnlock: computed('currentUserAccounts', function() { return this.currentUserAccounts && isEmpty(this.currentUserAccounts); @@ -72,7 +70,7 @@ export default Service.extend({ this.set('kredits', kredits); if (this.currentUserAccounts && this.currentUserAccounts.length > 0) { this.getCurrentUser.then((contributorData) => { - this.set('currentUser', contributorData); + this.set('currentUser', Contributor.create(contributorData)); }); } return kredits; @@ -134,10 +132,11 @@ export default Service.extend({ }); }, - vote(proposalId) { - console.debug('[kredits] vote for', proposalId); + batchVote(proposalIds) { + proposalIds = proposalIds.map((id) => parseInt(id)) + console.debug('[kredits] vote for', proposalIds); - return this.kredits.Operator.functions.vote(proposalId) + return this.kredits.Operator.functions.batchVote(proposalIds) .then((data) => { console.debug('[kredits] vote response', data); return data; diff --git a/app/styles/_forms.scss b/app/styles/_forms.scss new file mode 100644 index 0000000..d4486d7 --- /dev/null +++ b/app/styles/_forms.scss @@ -0,0 +1,91 @@ +button, input[type=submit] { + display: inline-block; + border: 1px solid rgba(22, 21, 40, 1); + background-color: rgba(22, 21, 40, 0.6); + color: $primaryColor; + border-radius: 3px; + font-weight: 500; + text-transform: uppercase; + cursor: pointer; + letter-spacing: 0.1em; + + &:hover { + background-color: rgba(22, 21, 40, 0.8); + } + + &:disabled { + cursor: not-allowed; + background-color: transparent; + } +} + +input[type=submit] { + padding: 0.6rem 2rem; +} + +input[type=text], select { + width: 100%; + padding: 1rem; + border: none; + border-bottom: 1px solid rgba(255,255,255,0.2); + background-color: rgba(22, 21, 40, 0.3); + color: #fff; + font-size: 1.2rem; + &:focus, &.valid { + background-color: rgba(22, 21, 40, 0.6); + } + @include placeholder { + color: rgba(238, 238, 238, 0.5); + } +} + +select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border-radius: 0; + background-color: rgba(22, 21, 40, 0.6); + background-image: + linear-gradient(45deg, transparent 50%, gray 50%), + linear-gradient(135deg, gray 50%, transparent 50%); + background-position: + calc(100% - 1.5rem) calc(1rem + 0.5rem), + calc(100% - 1rem) calc(1rem + 0.5rem); + background-size: + 0.5rem 0.5rem, + 0.5rem 0.5rem; + background-repeat: no-repeat; + + &:invalid { + color: rgba(238, 238, 238, 0.5); + } +} + +input[type=checkbox] { + display: none; +} + +label.checkbox { + line-height: 3.2rem; + font-size: 1.2rem; + &::before { + display: inline-block; + margin-right: 0.8rem; + height: 3.2rem; + width: 3.2rem; + font-size: 2rem; + background-color: rgba(22, 21, 40, 0.3); + border-bottom: 1px solid rgba(255,255,255,0.2); + text-align: center; + vertical-align: middle; + content: '✓'; + color: rgba(255,255,255,0.2); + } +} + +input[type=checkbox]:checked + label.checkbox { + &::before { + background-color: rgba(22, 21, 40, 0.6); + color: #fff; + } +} diff --git a/app/styles/app.scss b/app/styles/app.scss index 8c04cf8..fcd7ca8 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -84,21 +84,7 @@ section { } } -button, input[type=submit] { - display: inline-block; - border: 1px solid rgba(22, 21, 40, 1); - background-color: rgba(22, 21, 40, 0.6); - color: $primaryColor; - border-radius: 3px; - font-weight: 500; - text-transform: uppercase; - cursor: pointer; - letter-spacing: 0.1em; - - &:hover { - background-color: rgba(22, 21, 40, 0.8); - } -} +@import "forms"; @import "components/loading-spinner"; @import "components/contributor-list"; diff --git a/app/styles/components/_add-contributor.scss b/app/styles/components/_add-contributor.scss index 379b91e..929f05f 100644 --- a/app/styles/components/_add-contributor.scss +++ b/app/styles/components/_add-contributor.scss @@ -1,7 +1,6 @@ section#add-contributor, section#add-proposal { form { - p { margin-bottom: 1rem; @@ -14,81 +13,6 @@ section#add-contributor, section#add-proposal { } } } - - input[type=text], select { - width: 100%; - padding: 1rem; - border: none; - border-bottom: 1px solid rgba(255,255,255,0.2); - background-color: rgba(22, 21, 40, 0.3); - color: #fff; - font-size: 1.2rem; - &:focus, &.valid { - background-color: rgba(22, 21, 40, 0.6); - } - @include placeholder { - color: rgba(238, 238, 238, 0.5); - } - } - - select { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - border-radius: 0; - background-color: rgba(22, 21, 40, 0.6); - background-image: - linear-gradient(45deg, transparent 50%, gray 50%), - linear-gradient(135deg, gray 50%, transparent 50%); - background-position: - calc(100% - 1.5rem) calc(1rem + 0.5rem), - calc(100% - 1rem) calc(1rem + 0.5rem); - background-size: - 0.5rem 0.5rem, - 0.5rem 0.5rem; - background-repeat: no-repeat; - - &:invalid { - color: rgba(238, 238, 238, 0.5); - } - } - - input[type=submit] { - padding: 0.6rem 2rem; - &:disabled { - background-color: transparent; - } - } - - input[type=checkbox] { - display: none; - } - - label.checkbox { - line-height: 3.2rem; - font-size: 1.2rem; - &::before { - display: inline-block; - margin-right: 0.8rem; - height: 3.2rem; - width: 3.2rem; - font-size: 2rem; - background-color: rgba(22, 21, 40, 0.3); - border-bottom: 1px solid rgba(255,255,255,0.2); - text-align: center; - vertical-align: middle; - content: '✓'; - color: rgba(255,255,255,0.2); - } - } - - input[type=checkbox]:checked + label.checkbox { - &::before { - background-color: rgba(22, 21, 40, 0.6); - color: #fff; - } - } - } } diff --git a/app/styles/components/_proposal-list.scss b/app/styles/components/_proposal-list.scss index a35f17c..64530ed 100644 --- a/app/styles/components/_proposal-list.scss +++ b/app/styles/components/_proposal-list.scss @@ -46,6 +46,10 @@ ul.proposal-list { height: 2rem; line-height: 2rem; padding: 0 0.6rem; + + &.selected { + background-color: rgba(224,104,251, 0.6); + } } } diff --git a/app/templates/index.hbs b/app/templates/index.hbs index 99ccd78..bf72f44 100644 --- a/app/templates/index.hbs +++ b/app/templates/index.hbs @@ -2,7 +2,7 @@