diff --git a/app/src/renderer/components/staking/CardCandidate.vue b/app/src/renderer/components/staking/CardCandidate.vue
index 0937f66da7..f747e33ec4 100644
--- a/app/src/renderer/components/staking/CardCandidate.vue
+++ b/app/src/renderer/components/staking/CardCandidate.vue
@@ -1,25 +1,25 @@
-transition(name='ts-card-candidate'): div(:class='cssClass')
+transition(name='ts-card-candidate'): div(:class='styles')
.card-candidate-container
.values
- .value.keybaseID
- span {{ candidate.keybaseID }}
.value.id
span
- template(v-if='signedIn')
+ template
i.fa.fa-check-square-o(v-if='inCart' @click='rm(candidate)')
i.fa.fa-square-o(v-else @click='add(candidate)')
router-link(:to="{ name: 'candidate', params: { candidate: candidate.id }}")
- | {{ candidate.pubkey.data }}
+ | {{ candidate.keybaseID}}
+ .value {{ candidate.country }}
.value.voting_power.num.bar
span {{ num.prettyInt(candidate.voting_power) }}
- .bar(:style='atomsCss')
- .value.delegated.num.bar.delegated(v-if='signedIn')
- span {{ num.prettyInt(candidate.delegatedCoins) }}
- .bar(:style='delegatedAtomsCss')
- menu(v-if='signedIn')
+ .bar(:style='vpStyles')
+ .value.delegated.num.bar.delegated
+ span {{ num.prettyInt(candidate.shares) }}
+ .bar(:style='sharesStyles')
+ .value {{ (candidate.commission * 100).toFixed(2) }}%
+ menu
btn(theme='cosmos' v-if='inCart'
- icon='times' value='Remove' size='sm' @click.native='rm(candidate)')
+ icon='delete' value='Remove' size='sm' @click.native='rm(candidate)')
btn(v-else='' theme='cosmos'
icon='check' value='Add' size='sm' @click.native='add(candidate)')
@@ -28,7 +28,7 @@ transition(name='ts-card-candidate'): div(:class='cssClass')
import { mapGetters } from 'vuex'
import num from 'scripts/num'
import Btn from '@nylira/vue-button'
-// import { maxBy } from 'lodash'
+import { maxBy } from 'lodash'
export default {
name: 'card-candidate',
props: ['candidate'],
@@ -36,52 +36,44 @@ export default {
Btn
},
computed: {
- ...mapGetters(['shoppingCart', 'candidates', 'user']),
- cssClass () {
+ ...mapGetters(['shoppingCart', 'candidates']),
+ styles () {
let value = 'card-candidate'
if (this.inCart) value += ' card-candidate-active '
return value
},
- signedIn () {
- return this.user.signedIn
+ vpMax () {
+ if (this.candidates.length > 0) {
+ let richestCandidate = maxBy(this.candidates, 'voting_power')
+ return richestCandidate.voting_power
+ } else { return 0 }
},
- maxAtoms () {
- // if (this.candidates.length > 0) {
- // let richestCandidate = maxBy(this.candidates, 'atoms')
- // return richestCandidate.atoms
- // } else { return 0 }
- return 0
- },
- atomsCss () {
- let percentage = Math.round((this.candidate.atoms / this.maxAtoms) * 100)
+ vpStyles () {
+ let percentage =
+ Math.round((this.candidate.voting_power / this.vpMax) * 100)
return { width: percentage + '%' }
},
- maxDelegatedAtoms () {
- // if (this.candidates) {
- // let richestCandidate = maxBy(this.candidates, 'computed.delegatedAtoms')
- // return richestCandidate.computed.delegatedAtoms
- // } else { return 0 }
- return 0
+ sharesMax () {
+ if (this.candidates) {
+ let richestCandidate = maxBy(this.candidates, 'shares')
+ return richestCandidate.shares
+ } else { return 0 }
},
- delegatedAtomsCss () {
- let percentage = Math.round((this.candidate.delegatedAtoms /
- this.maxDelegatedAtoms) * 100)
+ sharesStyles () {
+ let percentage =
+ Math.round((this.candidate.shares / this.sharesMax) * 100)
return { width: percentage + '%' }
},
inCart () {
- return this.shoppingCart.find(c => c.id === this.candidate.id)
+ return this.shoppingCart.candidates.find(c => c.id === this.candidate.id)
}
},
data: () => ({
num: num
}),
methods: {
- add (candidate) {
- this.$store.commit('addToCart', candidate)
- },
- rm (candidate) {
- this.$store.commit('removeFromCart', candidate.id)
- }
+ add (candidate) { this.$store.commit('addToCart', candidate) },
+ rm (candidate) { this.$store.commit('removeFromCart', candidate.id) }
}
}
@@ -99,6 +91,9 @@ export default {
.card-candidate-container
position relative
+ &:hover
+ menu
+ display block
.values
display flex
@@ -111,9 +106,7 @@ export default {
align-items center
justify-content space-between
- color dim
padding 0 0.25rem
- font-size sm
min-width 0
@@ -121,11 +114,9 @@ export default {
i.fa
margin-right 0.5rem
a
- color txt
+ color link
&:hover
- color bright
- &.num
- mono()
+ color hover
&.bar
position relative
@@ -138,16 +129,14 @@ export default {
line-height 2rem
padding 0 0.375rem
+ color txt
.bar
height 1.5rem
- background darken(app-bg, 20%)
- margin-right 1rem
- &.delegated
- span
- color dim
- .bar
- background accent
+ background alpha(link, 33.3%)
+
+ &.delegated .bar
+ background alpha(accent, 33.3%)
span
display block
@@ -166,6 +155,7 @@ export default {
display flex
align-items center
justify-content center
+ display none
@media screen and (max-width: 414px)
.card-candidate-container menu .ni-btn .ni-btn-value
diff --git a/app/src/renderer/components/staking/PageCandidates.vue b/app/src/renderer/components/staking/PageCandidates.vue
index ed0775b4a8..1dc8f530b5 100644
--- a/app/src/renderer/components/staking/PageCandidates.vue
+++ b/app/src/renderer/components/staking/PageCandidates.vue
@@ -4,7 +4,7 @@ page(:title='pageTitle')
a(@click='setSearch(true)')
i.material-icons search
.label Search
- router-link(v-if="" to='/staking/delegate')
+ router-link(to='/staking/delegate')
i.material-icons check_circle
.label Delegate
modal-search(v-if="filters.candidates.search.visible" type="candidates")
@@ -42,41 +42,34 @@ export default {
ToolBar
},
computed: {
- ...mapGetters(['candidates', 'filters', 'shoppingCart', 'user']),
+ ...mapGetters(['candidates', 'filters', 'shoppingCart']),
pageTitle () {
- if (this.user.signedIn) return `Candidates (${this.candidatesNum} Selected)`
- else return 'Candidates'
+ return `Delegate (${this.candidatesNum} Candidates Selected)`
},
filteredCandidates () {
let query = this.filters.candidates.search.query
let list = orderBy(this.candidates, [this.sort.property], [this.sort.order])
if (this.filters.candidates.search.visible) {
- return list.filter(i => includes(i.keybaseID.toLowerCase(), query))
+ return list.filter(i => includes(i.keybaseID.toLowerCase(), query.toLowerCase()))
} else {
return list
}
},
- candidatesNum () {
- return this.shoppingCart.length
- },
- sort () {
- let props = [
- { id: 1, title: 'Keybase ID', value: 'keybaseID' },
- { id: 2, title: 'Public Key', value: 'id' },
- { id: 3, title: 'Delegated', value: 'voting_power', initial: true }
- ]
- if (this.user.signedIn) {
- props.push({ id: 4, title: 'Delegated (Yours)', value: 'delegated' })
- }
- return {
- property: 'voting_power',
- order: 'desc',
- properties: props
- }
- }
+ candidatesNum () { return this.shoppingCart.candidates.length }
},
data: () => ({
- query: ''
+ query: '',
+ sort: {
+ property: 'keybaseID',
+ order: 'asc',
+ properties: [
+ { id: 1, title: 'Keybase ID', value: 'keybaseID', initial: true },
+ { id: 2, title: 'Country', value: 'country' },
+ { id: 3, title: 'Voting Power', value: 'voting_power' },
+ { id: 4, title: 'Delegated Power', value: 'shares' },
+ { id: 5, title: 'Commission', value: 'commission' }
+ ]
+ }
}),
methods: {
setSearch (bool) { this.$store.commit('setSearchVisible', ['candidates', bool]) }
diff --git a/app/src/renderer/components/staking/PanelSort.vue b/app/src/renderer/components/staking/PanelSort.vue
index 33a9cb033b..2222f4d087 100644
--- a/app/src/renderer/components/staking/PanelSort.vue
+++ b/app/src/renderer/components/staking/PanelSort.vue
@@ -14,10 +14,8 @@ export default {
methods: {
orderBy (property, event) {
let sortBys = $(this.$el).find('.sort-by')
- console.log(sortBys)
$(sortBys).removeClass('active desc asc')
let el = $(event.target).parent()
- console.log('el', el)
if (this.sort.property === property) {
if (this.sort.order === 'asc') {
@@ -65,6 +63,7 @@ export default {
.label
font-size sm
color dim
+ text-transform uppercase
padding-right 0.5rem
white-space nowrap
@@ -75,8 +74,10 @@ export default {
display block
font-family FontAwesome
color dim
+
&.asc:after
content '\f0d8'
+
&.desc:after
content '\f0d7'
diff --git a/app/src/renderer/vuex/modules/shoppingCart.js b/app/src/renderer/vuex/modules/shoppingCart.js
index 5004784a39..bb4d3378d6 100644
--- a/app/src/renderer/vuex/modules/shoppingCart.js
+++ b/app/src/renderer/vuex/modules/shoppingCart.js
@@ -1,4 +1,3 @@
-import { findIndex } from 'lodash'
export default ({ commit, basecoin }) => {
let state = { candidates: [] }
@@ -9,16 +8,9 @@ export default ({ commit, basecoin }) => {
candidate: Object.assign({}, candidate),
atoms: 0
})
- console.log(`+ ADD ${candidate.keybaseID} to cart`)
},
removeFromCart (state, candidate) {
- let index = findIndex(state.candidates, c => {
- return c.candidate.id === candidate
- })
- // console.log(`- RM ${JSON.stringify(state.candidates[index])} from cart[${index}]`)
- let candidates = state.candidates.slice()
- candidates.splice(index, 1)
- state.candidates = candidates
+ state.candidates = state.candidates.filter(c => c.id !== candidate)
}
}
diff --git a/test/unit/specs/CardCandidate.spec.js b/test/unit/specs/CardCandidate.spec.js
new file mode 100644
index 0000000000..58c4e933cf
--- /dev/null
+++ b/test/unit/specs/CardCandidate.spec.js
@@ -0,0 +1,104 @@
+import Vuex from 'vuex'
+import { mount, createLocalVue } from 'vue-test-utils'
+import CardCandidate from 'renderer/components/staking/CardCandidate'
+
+const shoppingCart = require('renderer/vuex/modules/shoppingCart').default({})
+const candidates = require('renderer/vuex/modules/candidates').default({})
+
+const localVue = createLocalVue()
+localVue.use(Vuex)
+
+describe('CardCandidate', () => {
+ let wrapper, store, candidate
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ getters: {
+ shoppingCart: () => shoppingCart.state,
+ candidates: () => candidates.state
+ },
+ modules: {
+ shoppingCart,
+ candidates
+ }
+ })
+
+ store.commit('addCandidate', {
+ pubkey: 'pubkeyX',
+ description: JSON.stringify({
+ id: 'idX',
+ description: 'descriptionX',
+ voting_power: 10000,
+ shares: 5000,
+ keybaseID: 'keybaseX',
+ country: 'USA'
+ })
+ })
+ store.commit('addCandidate', {
+ pubkey: 'pubkeyY',
+ description: JSON.stringify({
+ id: 'idY',
+ description: 'descriptionY',
+ voting_power: 30000,
+ shares: 10000,
+ keybaseID: 'keybaseY',
+ country: 'Canada'
+ })
+ })
+
+ candidate = store.state.candidates[0]
+
+ wrapper = mount(CardCandidate, {
+ localVue,
+ store,
+ propsData: {
+ candidate
+ }
+ })
+
+ jest.spyOn(store, 'commit')
+ })
+
+ it('has the expected html structure', () => {
+ expect(wrapper.vm.$el).toMatchSnapshot()
+ })
+
+ it('should show the country', () => {
+ expect(wrapper.html()).toContain('USA')
+ })
+
+ it('should show the voting power', () => {
+ expect(wrapper.html()).toContain('10,000')
+ })
+
+ it('should show the relative voting power as a bar', () => {
+ expect(wrapper.vm.$el.querySelector('.voting_power .bar').style.width).toBe(Math.floor(10000 / 30000 * 100) + '%')
+ })
+
+ it('should show the relative shares hold as a bar', () => {
+ expect(wrapper.vm.$el.querySelector('.voting_power .bar').style.width).toBe(Math.floor(5000 / 15000 * 100) + '%')
+ })
+
+ it('should add to cart', () => {
+ expect(wrapper.vm.shoppingCart.candidates).toEqual([])
+ expect(wrapper.vm.inCart).toBeFalsy()
+ expect(wrapper.find('menu .ni-btn').text()).toContain('Add')
+ expect(wrapper.html()).not.toContain('card-candidate-active')
+ wrapper.find('menu .ni-btn').trigger('click')
+ expect(wrapper.vm.inCart).toBeTruthy()
+ expect(store.commit).toHaveBeenCalledWith('addToCart', store.state.candidates[0])
+ expect(wrapper.html()).toContain('card-candidate-active')
+ })
+
+ it('should remove from cart', () => {
+ store.commit('addToCart', store.state.candidates[0])
+ wrapper.update()
+ expect(wrapper.vm.inCart).toBeTruthy()
+ expect(wrapper.find('menu .ni-btn').text()).toContain('Remove')
+ wrapper.find('menu .ni-btn').trigger('click')
+ expect(store.commit).toHaveBeenCalledWith('removeFromCart', candidate.id)
+ expect(wrapper.vm.shoppingCart.candidates).toEqual([])
+ expect(wrapper.vm.inCart).toBeFalsy()
+ expect(wrapper.html()).not.toContain('card-candidate-active')
+ })
+})
diff --git a/test/unit/specs/PageCandidates.spec.js b/test/unit/specs/PageCandidates.spec.js
new file mode 100644
index 0000000000..8dc54cda7e
--- /dev/null
+++ b/test/unit/specs/PageCandidates.spec.js
@@ -0,0 +1,118 @@
+import Vuex from 'vuex'
+import { mount, createLocalVue } from 'vue-test-utils'
+import PageCandidates from 'renderer/components/staking/PageCandidates'
+
+const shoppingCart = require('renderer/vuex/modules/shoppingCart').default({})
+const candidates = require('renderer/vuex/modules/candidates').default({})
+const filters = require('renderer/vuex/modules/filters').default({})
+
+const localVue = createLocalVue()
+localVue.use(Vuex)
+
+describe('PageCandidates', () => {
+ let wrapper, store
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ getters: {
+ shoppingCart: () => shoppingCart.state,
+ candidates: () => candidates.state,
+ filters: () => filters.state
+ },
+ modules: {
+ shoppingCart,
+ candidates,
+ filters
+ }
+ })
+
+ store.commit('addCandidate', {
+ pubkey: 'pubkeyY',
+ description: JSON.stringify({
+ id: 'idY',
+ description: 'descriptionY',
+ voting_power: 30000,
+ shares: 10000,
+ keybaseID: 'keybaseY',
+ country: 'Canada'
+ })
+ })
+ store.commit('addCandidate', {
+ pubkey: 'pubkeyX',
+ description: JSON.stringify({
+ id: 'idX',
+ description: 'descriptionX',
+ voting_power: 2000,
+ shares: 5000,
+ keybaseID: 'keybaseX',
+ country: 'USA'
+ })
+ })
+
+ wrapper = mount(PageCandidates, {
+ localVue,
+ store,
+ stubs: {
+ 'data-error': '