diff --git a/app/components/add-contributor/component.js b/app/components/add-contributor/component.js index 75398c2..6aee5b6 100644 --- a/app/components/add-contributor/component.js +++ b/app/components/add-contributor/component.js @@ -1,41 +1,58 @@ import Ember from 'ember'; +import Contributor from 'kredits-web/models/contributor'; -export default Ember.Component.extend({ +const { + Component, + isPresent, + inject: { + service + }, + computed +} = Ember; - id: null, - realName: null, - address: null, - ipfsHash: null, - isCore: false, +export default Component.extend({ + kredits: service(), + + newContributor: null, inProgress: false, - isValidId: function() { - return Ember.isPresent(this.get('id')); - }.property('id'), - - isValidRealName: function() { - return Ember.isPresent(this.get('realName')); - }.property('realName'), - isValidAddress: function() { - return this.get('kredits.web3Instance').isAddress(this.get('address')); - }.property('address'), + return this.get('kredits.web3') + .isAddress(this.get('newContributor.address')); + }.property('kredits.web3', 'newContributor.address'), - isValid: function() { - return this.get('isValidId') && this.get('isValidRealName') && this.get('isValidAddress'); - }.property('isValidAddress', 'isValidId', 'isValidRealName'), + isValidName: function() { + return isPresent(this.get('newContributor.name')); + }.property('newContributor.name'), + + isValidURL: function() { + return isPresent(this.get('newContributor.url')); + }.property('newContributor.url'), + + isValidGithubUID: function() { + return isPresent(this.get('newContributor.github_uid')); + }.property('newContributor.github_uid'), + + isValidGithubUsername: function() { + return isPresent(this.get('newContributor.github_username')); + }.property('newContributor.github_username'), + + isValidWikiUsername: function() { + return isPresent(this.get('newContributor.wiki_username')); + }.property('newContributor.wiki_username'), + + isValid: computed.and( + 'isValidAddress', + 'isValidName', + 'isValidGithubUID' + ), reset: function() { this.setProperties({ - id: null, - realName: null, - address: null, - ipfsHash: null, - isCore: true, + newContributor: Contributor.create({ kind: 'person' }), inProgress: false }); - }, actions: { @@ -49,12 +66,7 @@ export default Ember.Component.extend({ if (this.get('isValid')) { this.set('inProgress', true); - this.get('kredits').addContributor( - this.get('address'), - this.get('realName'), - this.get('isCore'), - this.get('id') - ).then(contributor => { + this.get('kredits').addContributor(this.get('newContributor')).then(contributor => { this.reset(); this.get('contributors').pushObject(contributor); window.scroll(0,0); diff --git a/app/components/add-contributor/template.hbs b/app/components/add-contributor/template.hbs index 076156e..eaa5818 100644 --- a/app/components/add-contributor/template.hbs +++ b/app/components/add-contributor/template.hbs @@ -1,31 +1,58 @@

- {{input type="checkbox" name="is-core" id="is-core" checked=isCore}} + {{input type="checkbox" name="is-core" id="is-core" checked=newContributor.isCore}}

-

- {{input name="id" - type="text" - placeholder="GitHub UID (123)" - value=id - class=(if isValidId 'valid' '')}} -

-

- {{input name="realname" - type="text" - placeholder="GitHub username" - value=realName - class=(if isValidRealName 'valid' '')}} -

{{input name="address" type="text" placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" - value=address + value=newContributor.address class=(if isValidAddress 'valid' '')}}

+

+ +

+

+ {{input name="name" + type="text" + placeholder="Name" + value=newContributor.name + class=(if isValidName 'valid' '')}} +

+

+ {{input name="url" + type="text" + placeholder="URL" + value=newContributor.url + class=(if isValidURL 'valid' '')}} +

+

+ {{input name="github_uid" + type="text" + placeholder="GitHub UID (123)" + value=newContributor.github_uid + class=(if isValidGithubUID 'valid' '')}} +

+

+ {{input name="github_username" + type="text" + placeholder="GitHub username" + value=newContributor.github_username + class=(if isValidGithubUsername 'valid' '')}} +

+

+ {{input name="wiki_username" + type="text" + placeholder="Wiki Username" + value=newContributor.wiki_username + class=(if isValidWikiUsername 'valid' '')}} +

{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}

diff --git a/app/components/add-proposal/component.js b/app/components/add-proposal/component.js index 70a990a..8abf03c 100644 --- a/app/components/add-proposal/component.js +++ b/app/components/add-proposal/component.js @@ -2,6 +2,7 @@ import Ember from 'ember'; const { Component, + isPresent, inject: { service }, @@ -13,47 +14,52 @@ export default Component.extend({ kredits: service(), proposal: null, + contributors: null, inProgress: false, isValidRecipient: computed('proposal.recipientAddress', function() { - return this.get('kredits.web3Instance').isAddress(this.get('proposal.recipientAddress')); + return this.get('kredits.web3').isAddress(this.get('proposal.recipientAddress')); }), isValidAmount: computed('proposal.amount', function() { - // TODO - return true; + return parseInt(this.get('proposal.amount'), 10) > 0; }), isValidUrl: computed('proposal.url', function() { - // TODO - return true; + return isPresent(this.get('proposal.url')); }), - isValidIpfsHash: computed('proposal.ipfsHash', function() { - // TODO - return true; + isValidDescription: computed('proposal.description', function() { + return isPresent(this.get('proposal.description')); }), - isValid: computed.and('isValidRecipient', 'isValidAmount', 'isValidUrl', - 'isValidIpfsHash'), + isValid: computed.and('isValidRecipient', + 'isValidAmount', + 'isValidDescription'), actions: { save() { - if (this.get('isValid')) { - this.set('inProgress', true); - - this.get('kredits').addProposal(this.get('proposal')) - .then(() => { - this.attrs.onSave(); - }).catch((error) => { - Ember.Logger.error('Error creating the proposal', error); - alert('Something went wrong.'); - }).finally(() => { - this.set('inProgress', false); - }); - } else { + if (! this.get('isValid')) { alert('Invalid data. Please review and try again.'); + return false; } + this.set('inProgress', true); + let proposal = this.get('proposal'); + + // Set the recipient's IPFS profile hash so it can be used in the + // contribution object (which is to be stored in IPFS as well) + let contributor = this.get('contributors').findBy('address', proposal.get('recipientAddress')); + proposal.set('recipientProfile', contributor.get('ipfsHash')); + + this.get('kredits').addProposal(proposal) + .then(() => { + this.attrs.onSave(); + }).catch((error) => { + Ember.Logger.error('[add-proposal] error creating the proposal', error); + alert('Something went wrong.'); + }).finally(() => { + this.set('inProgress', false); + }); } } diff --git a/app/components/add-proposal/template.hbs b/app/components/add-proposal/template.hbs index 7864ee4..f04db3b 100644 --- a/app/components/add-proposal/template.hbs +++ b/app/components/add-proposal/template.hbs @@ -7,6 +7,15 @@ {{/each}}

+

+ +

{{input type="text" placeholder="100" @@ -15,15 +24,15 @@

{{input type="text" - placeholder="URL" - value=proposal.url - class=(if isValidUrl 'valid' '')}} + placeholder="Description" + value=proposal.description + class=(if isValidDescription 'valid' '')}}

{{input type="text" - placeholder="IPFS Hash" - value=proposal.ipfsHash - class=(if isValidIpfsHash 'valid' '')}} + placeholder="URL (optional)" + value=proposal.url + class=(if isValidUrl 'valid' '')}}

{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}} diff --git a/app/components/contributor-list/template.hbs b/app/components/contributor-list/template.hbs index dae31e5..86062ac 100644 --- a/app/components/contributor-list/template.hbs +++ b/app/components/contributor-list/template.hbs @@ -3,7 +3,7 @@ - {{contributor.github_username}} + {{contributor.name}} {{contributor.kredits}} @@ -12,14 +12,12 @@ -

-
Ethereum address
-
{{contributor.address}}
+
+ {{/each}} diff --git a/app/components/proposal-list/template.hbs b/app/components/proposal-list/template.hbs index 0418dfb..f346830 100644 --- a/app/components/proposal-list/template.hbs +++ b/app/components/proposal-list/template.hbs @@ -1,9 +1,8 @@ {{#each proposals as |proposal|}} -
  • - #{{proposal.id}} - Issue{{#if proposal.executed}}d{{/if}} +
  • + {{proposal.amount}}₭S - to {{proposal.recipientName}} + for {{proposal.recipientName}} ({{proposal.votesCount}}/{{proposal.votesNeeded}} votes) {{#unless proposal.executed}}{{/unless}} diff --git a/app/controllers/index.js b/app/controllers/index.js index 2c85b02..ed7c0c5 100644 --- a/app/controllers/index.js +++ b/app/controllers/index.js @@ -23,7 +23,7 @@ export default Ember.Controller.extend({ let proposals = this.get('model.proposals') .filterBy('executed', false) .map(p => { - p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).github_username); + p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name); return p; }); return proposals; @@ -33,7 +33,7 @@ export default Ember.Controller.extend({ let proposals = this.get('model.proposals') .filterBy('executed', true) .map(p => { - p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).github_username); + p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name); return p; }); return proposals; diff --git a/app/models/contributor.js b/app/models/contributor.js index 386d757..293bdcd 100644 --- a/app/models/contributor.js +++ b/app/models/contributor.js @@ -1,10 +1,18 @@ import Ember from 'ember'; +const { + isPresent, +} = Ember; + export default Ember.Object.extend({ address: null, + name: null, + kind: null, + url: null, github_username: null, github_uid: null, + wiki_username: null, ipfsHash: null, kredits: null, isCore: false, @@ -14,18 +22,98 @@ export default Ember.Object.extend({ return `https\:\/\/avatars2.githubusercontent.com/u/${this.get('github_uid')}?v=3&s=128`; }.property('github_uid'), - serialize() { - return JSON.stringify({ - profiles: { - 'github.com': { - uid: this.get('github_uid'), - username: this.get('github_username'), + + /** + * Loads the contributor's profile data from IPFS and sets local instance + * properties from it + * + * @method + * @public + */ + loadProfile(ipfs) { + let promise = new Ember.RSVP.Promise((resolve, reject) => { + ipfs.getFile(this.get('ipfsHash')).then(content => { + let profileJSON = JSON.parse(content); + let profile = Ember.Object.create(profileJSON); + + this.setProperties({ + name: profile.get('name'), + kind: profile.get('kind') + }); + + let accounts = profile.get('accounts'); + let github = accounts.findBy('site', 'github.com'); + let wiki = accounts.findBy('site', 'wiki.kosmos.org'); + + if (isPresent(github)) { + this.setProperties({ + github_username: github.username, + github_uid: github.uid, + }); } - // 'wiki.kosmos.org': { - // username: this.get('wiki_username') - // } - } + if (isPresent(wiki)) { + this.setProperties({ + wiki_username: wiki.username + }); + } + + Ember.Logger.debug('[contributor] loaded contributor profile', profile); + resolve(); + }).catch((err) => { + Ember.Logger.error('[contributor] error trying to load contributor profile', this.get('ipfsHash'), err); + reject(err); + }); }); + + return promise; + }, + + /** + * Creates a JSON-LD object of the contributor, according to + * https://github.com/67P/kosmos-schemas/blob/master/schemas/contributor.json + * + * @method + * @public + */ + toJSON() { + let contributor = { + "@context": "https://schema.kosmos.org", + "@type": "Contributor", + "kind": this.get('kind'), + "name": this.get('name'), + "accounts": [] + }; + + if (Ember.isPresent(this.get('url'))) { + contributor["url"] = this.get('url'); + } + if (Ember.isPresent(this.get('github_uid'))) { + contributor.accounts.push({ + "site": "github.com", + "uid": this.get('github_uid'), + "username": this.get('github_username'), + "url": `https://github.com/${this.get('github_username')}` + }); + } + if (Ember.isPresent(this.get('wiki_username'))) { + contributor.accounts.push({ + "site": "wiki.kosmos.org", + "username": this.get('wiki_username'), + "url": `https://wiki.kosmos.org/User:${this.get('wiki_username')}` + }); + } + + return contributor; + }, + + /** + * Returns the JSON-LD representation of the model as a string + * + * @method + * @public + */ + serialize() { + return JSON.stringify(this.toJSON()); } }); diff --git a/app/models/proposal.js b/app/models/proposal.js index 1ac4b1c..6d01d27 100644 --- a/app/models/proposal.js +++ b/app/models/proposal.js @@ -1,16 +1,102 @@ import Ember from 'ember'; +const { + isPresent, + isEmpty, +} = Ember; + export default Ember.Object.extend({ id: null, creatorAddress: null, recipientAddress: null, recipientName: null, + recipientProfile: null, votesCount: null, votesNeeded: null, amount: null, executed: null, + contribution: null, + kind: null, + description: null, url: null, - ipfsHash: null + details: null, + ipfsHash: null, + + /** + * Loads the contribution details from IPFS and sets local instance + * properties from it + * + * @method + * @public + */ + loadContribution(ipfs) { + let promise = new Ember.RSVP.Promise((resolve, reject) => { + ipfs.getFile(this.get('ipfsHash')).then(content => { + let contributionJSON = JSON.parse(content); + let contribution = Ember.Object.create(contributionJSON); + + this.setProperties({ + kind: contribution.get('kind'), + description: contribution.get('description'), + url: contribution.get('url') + }); + + // TODO load details + // let details = profile.get('accounts'); + + Ember.Logger.debug('[proposal] loaded contribution details', contributionJSON); + resolve(); + }).catch((err) => { + Ember.Logger.error('[proposal] error trying to load contribution details', this.get('ipfsHash'), err); + reject(err); + }); + }); + + return promise; + }, + + /** + * Creates a JSON-LD object of the contribution, according to + * https://github.com/67P/kosmos-schemas/blob/master/schemas/contribution.json + * + * @method + * @public + */ + contributionToJSON() { + if (isEmpty(this.get('recipientProfile'))) { + throw new Error('IPFS hash for recipient profile missing from proposal object'); + } + if (isEmpty(this.get('kind')) || isEmpty(this.get('description'))) { + throw new Error('Missing one or more required properties: kind, description'); + } + + let contribution = { + "@context": "https://schema.kosmos.org", + "@type": "Contribution", + "contributor": { + "ipfs": this.get('recipientProfile') + }, + "kind": this.get('kind'), + "description": this.get('description'), + "details": {} + }; + + if (isPresent(this.get('url'))) { + contribution["url"] = this.get('url'); + } + + return contribution; + }, + + /** + * Returns the JSON-LD representation of the contribution as a string + * + * @method + * @public + */ + serializeContribution() { + return JSON.stringify(this.contributionToJSON()); + } }); diff --git a/app/routes/index.js b/app/routes/index.js index ada98a1..b60d8d5 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import Contributor from 'kredits-web/models/contributor'; export default Ember.Route.extend({ @@ -10,7 +11,8 @@ export default Ember.Route.extend({ return Ember.RSVP.hash({ contributors: kredits.getContributors(), totalSupply: kredits.getValueFromContract('tokenContract', 'totalSupply'), - proposals: kredits.getProposals() + proposals: kredits.getProposals(), + newContributor: Contributor.create({ kind: 'person' }) }); } diff --git a/app/routes/proposals/new.js b/app/routes/proposals/new.js index 26b77a5..f5870d9 100644 --- a/app/routes/proposals/new.js +++ b/app/routes/proposals/new.js @@ -18,6 +18,7 @@ export default Route.extend({ recipientAddress: params.recipient, amount: params.amount, url: params.url, + kind: params.kind || 'dev', ipfsHash: params.ipfsHash }); diff --git a/app/services/ipfs.js b/app/services/ipfs.js index 453a005..df41fb7 100644 --- a/app/services/ipfs.js +++ b/app/services/ipfs.js @@ -26,6 +26,9 @@ export default Ember.Service.extend({ getFile(hash) { return this.get('ipfs').cat(hash, { buffer: true }).then(res => { return res.toString(); + }, err => { + Ember.Logger.error('[ipfs] error trying to fetch file', hash, err); + throw err; }); } diff --git a/app/services/kredits.js b/app/services/kredits.js index a44da20..7a844f2 100644 --- a/app/services/kredits.js +++ b/app/services/kredits.js @@ -4,6 +4,7 @@ import config from 'kredits-web/config/environment'; import Contributor from 'kredits-web/models/contributor'; import Proposal from 'kredits-web/models/proposal'; import kreditsContracts from 'npm:kredits-contracts'; +import uuid from 'npm:uuid'; const { Service, @@ -101,17 +102,19 @@ export default Service.extend({ this.getValueFromContract('kreditsContract', 'contributorAddresses', i).then(address => { this.getValueFromContract('kreditsContract', 'contributors', address).then(person => { this.getValueFromContract('tokenContract', 'balanceOf', address).then(balance => { - Ember.Logger.debug('person', person); + Ember.Logger.debug('[kredits] person', address, person); + let contributor = Contributor.create({ address: address, - github_username: person[1], - github_uid: person[0], ipfsHash: person[2], kredits: balance.toNumber(), isCurrentUser: this.get('currentUserAccounts').includes(address) }); - Ember.Logger.debug('[kredits] contributor', contributor); - resolve(contributor); + + contributor.loadProfile(this.get('ipfs')).then( + () => resolve(contributor), + err => reject(err) + ); }); }); }).catch(err => reject(err)); @@ -145,8 +148,16 @@ export default Service.extend({ url : p[6], ipfsHash : p[7] }); - Ember.Logger.debug('[kredits] proposal', proposal); - resolve(proposal); + + if (proposal.get('ipfsHash')) { + proposal.loadContribution(this.get('ipfs')).then( + () => resolve(proposal), + err => reject(err) + ); + } else { + Ember.Logger.warn('[kredits] proposal from blockchain is missing IPFS hash', proposal); + resolve(proposal); + } }).catch(err => reject(err)); }); return promise; @@ -175,23 +186,20 @@ export default Service.extend({ }); }, - addContributor(address, name, isCore, id) { - Ember.Logger.debug('[kredits] add contributor', name, address); + addContributor(contributor) { + Ember.Logger.debug('[kredits] add contributor', contributor); - let contributor = Contributor.create({ - address: address, - github_username: name, - github_uid: id, + contributor.setProperties({ kredits: 0, - isCore: isCore, - isCurrentUser: this.get('currentUserAccounts').includes(address) + isCurrentUser: this.get('currentUserAccounts').includes(contributor.address) }); + let id = uuid.v4(); + return new Ember.RSVP.Promise((resolve, reject) => { this.get('ipfs').storeFile(contributor.serialize()).then(ipfsHash => { contributor.set('ipfsHash', ipfsHash); - Ember.Logger.debug('ADD', address, name, ipfsHash, isCore, id); - this.get('kreditsContract').addContributor(address, name, ipfsHash, isCore, id, (err, data) => { + this.get('kreditsContract').addContributor(contributor.address, contributor.name, contributor.ipfsHash, contributor.isCore, id, (err, data) => { if (err) { reject(err); return; } Ember.Logger.debug('[kredits] add contributor response', data); resolve(contributor); @@ -205,14 +213,15 @@ export default Service.extend({ const { recipientAddress, amount, - url, - ipfsHash - } = proposal.getProperties('recipientAddress', 'amount', 'url', 'ipfsHash'); + url + } = proposal.getProperties('recipientAddress', 'amount', 'url'); - this.get('kreditsContract').addProposal(recipientAddress, amount, url, ipfsHash, (err, data) => { - if (err) { reject(err); return; } - Ember.Logger.debug('[kredits] add proposal response', data); - resolve(); + this.get('ipfs').storeFile(proposal.serializeContribution()).then(ipfsHash => { + this.get('kreditsContract').addProposal(recipientAddress, amount, url, ipfsHash, (err, data) => { + if (err) { reject(err); return; } + Ember.Logger.debug('[kredits] add proposal response', data); + resolve(); + }); }); }); }, diff --git a/app/styles/_colors.scss b/app/styles/_colors.scss new file mode 100644 index 0000000..9578f1d --- /dev/null +++ b/app/styles/_colors.scss @@ -0,0 +1,8 @@ +$blue: #68d7fb; +$purple: #8f68fb; +$pink: #e068fb; +$green: #97fb68; +$yellow: #fbe468; +$red: #fb6868; + +$primaryColor: $blue; diff --git a/app/styles/app.scss b/app/styles/app.scss index d8f2e8a..afca809 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -15,6 +15,7 @@ @import "neat"; @import "settings/breakpoints"; @import "layout"; +@import "colors"; $font-family-sans: 'Open Sans', sans-serif; @@ -23,7 +24,7 @@ body { background-repeat: none; background-attachment: fixed; font-family: $font-family-sans; - font-size: 16px; + font-size: 14px; font-weight: 300; color: #fff; } @@ -43,7 +44,7 @@ h1, h2, h3, h4, h5, input, button { section { h2 { font-size: 2.8rem; - color: lightblue; + color: $primaryColor; @include media($mobile) { font-size: 2rem; } @@ -54,7 +55,7 @@ section { p.stats { padding-top: 3rem; font-size: 1rem; - color: lightblue; + color: white; text-align: center; span.number { font-weight: 600; @@ -70,14 +71,14 @@ section { .actions { padding-top: 3rem; font-size: 1rem; - color: lightblue; + color: $primaryColor; text-align: center; @include media($mobile) { padding-top: 2rem; } a { - color: lightblue; + color: $primaryColor; } } } @@ -87,7 +88,7 @@ button, input[type=submit] { display: inline-block; border: 1px solid rgba(22, 21, 40, 1); background-color: rgba(22, 21, 40, 0.6); - color: lightblue; + color: $primaryColor; border-radius: 3px; font-weight: 500; text-transform: uppercase; diff --git a/app/styles/components/_add-contributor.scss b/app/styles/components/_add-contributor.scss index 6e5fe5a..379b91e 100644 --- a/app/styles/components/_add-contributor.scss +++ b/app/styles/components/_add-contributor.scss @@ -9,7 +9,7 @@ section#add-contributor, section#add-proposal { padding-top: 1rem; text-align: center; a { - color: lightblue; + color: $primaryColor; margin-left: 1rem; } } diff --git a/app/styles/components/_contributor-list.scss b/app/styles/components/_contributor-list.scss index 63554e9..4e9158f 100644 --- a/app/styles/components/_contributor-list.scss +++ b/app/styles/components/_contributor-list.scss @@ -15,54 +15,40 @@ table.contributor-list { &.metadata { height: 0; + transition: all 0.2s ease-out; td { padding: 0 1.2rem; - transition: all 0.2s ease-out; } a { - color: lightblue; + color: $primaryColor; &:hover, &:active { color: #fff; } } - dl { + ul { + list-style: none; display: block; - width: 100%; - font-size: 1rem; - dt, dd { - display: inline-block; - overflow: hidden; - height: 0; - line-height: 1.6rem; - transition: all 0.2s ease-out; - } - dt { - clear: both; - float: left; - width: 30%; - } - dd { - float: right; - width: 70%; - text-align: right; - } - } + overflow: hidden; + height: 0; + transition: all 0.2s ease-out; - &.visible { - td { - padding: 1rem 1.2rem; - } - - dl { - dt, dd { - height: auto; + li { + display: inline; + &+li { + margin-left: 1rem; } } } + &.visible { + height: auto; + ul { + height: auto; + } + } } td { diff --git a/app/styles/components/_loading-spinner.scss b/app/styles/components/_loading-spinner.scss index 17ed468..6ba4057 100644 --- a/app/styles/components/_loading-spinner.scss +++ b/app/styles/components/_loading-spinner.scss @@ -11,7 +11,7 @@ margin-top: 12rem; text-align: center; font-size: 1.4rem; - color: lightblue; + color: $primaryColor; @include media($mobile) { margin-top: 6rem; @@ -23,7 +23,7 @@ margin-bottom: 2rem; #path-comet { - fill: lightblue; + fill: $primaryColor; opacity: 0.1; animation-name: pulse; diff --git a/app/styles/components/_proposal-list.scss b/app/styles/components/_proposal-list.scss index 8c7a1aa..a35f17c 100644 --- a/app/styles/components/_proposal-list.scss +++ b/app/styles/components/_proposal-list.scss @@ -14,9 +14,14 @@ ul.proposal-list { border-top: 1px solid rgba(255,255,255,0.2); } - .id { - color: lightblue; + .category { + color: $blue; padding-right: 0.2rem; + &.community { color: $red; } + &.dev { color: $pink; } + &.design { color: $yellow; } + &.docs { color: $green; } + &.ops { color: $purple; } } .amount { font-weight: 500; @@ -31,8 +36,8 @@ ul.proposal-list { } .votes { font-size: 1rem; - color: lightblue; - padding-left: 1rem; + color: $primaryColor; + padding-left: 0.5rem; } button { diff --git a/app/templates/index.hbs b/app/templates/index.hbs index 656d766..23f9a8a 100644 --- a/app/templates/index.hbs +++ b/app/templates/index.hbs @@ -54,8 +54,8 @@
    - {{add-contributor kredits=kredits - contributors=model.contributors + {{add-contributor contributors=model.contributors + newContributor=model.newContributor contractInteractionEnabled=contractInteractionEnabled}}
    diff --git a/config/environment.js b/config/environment.js index b1278ca..74323b9 100644 --- a/config/environment.js +++ b/config/environment.js @@ -23,6 +23,7 @@ module.exports = function(environment) { }, browserify: { + tests: true, transform: [ ["babelify", { presets: ["es2015"], @@ -38,7 +39,7 @@ module.exports = function(environment) { ipfs: { host: 'ipfs.kosmos.org', port: '5444', - protocol: 'http' + protocol: 'https' } }; diff --git a/package.json b/package.json index 87c5cf3..de892aa 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "Contribution dashboard of the Kosmos project", "license": "MIT", - "author": "Kosmos Community", + "author": "Kosmos Contributors ", "contributors": [ "Garret Alfert ", "Sebastian Kippe ", @@ -55,8 +55,11 @@ "ember-resolver": "^2.0.3", "ember-truth-helpers": "1.3.0", "ipfs-api": "^12.1.7", + "kosmos-schemas": "~1.1.1", "kredits-contracts": "^2.4.0", "loader.js": "^4.0.10", + "tv4": "^1.3.0", + "uuid": "^3.0.1", "web3": "^0.18.2" }, "engines": { diff --git a/tests/unit/models/contributor-test.js b/tests/unit/models/contributor-test.js index 2a7dbe4..39651f1 100644 --- a/tests/unit/models/contributor-test.js +++ b/tests/unit/models/contributor-test.js @@ -1,9 +1,26 @@ import { moduleFor, test } from 'ember-qunit'; +import schemas from 'npm:kosmos-schemas'; +import tv4 from 'npm:tv4'; moduleFor('model:contributor', 'Unit | Model | contributor'); -test('avatarURL returns correct URL', function(assert) { +test('#avatarURL() returns correct URL', function(assert) { let model = this.subject(); model.set('github_uid', '318'); + assert.equal(model.get('avatarURL'), 'https://avatars2.githubusercontent.com/u/318?v=3&s=128'); }); + +test('#toJSON() returns a valid JSON-LD representation of the model', function(assert) { + let model = this.subject(); + + model.setProperties({ + name: 'Satoshi Nakamoto', + kind: 'person', + github_uid: 123, + github_username: 'therealsatoshi', + wiki_username: 'Satoshi', + }); + + assert.ok(tv4.validate(model.toJSON(), schemas['contributor'])); +}); diff --git a/tests/unit/models/proposal-test.js b/tests/unit/models/proposal-test.js index d91008f..023708b 100644 --- a/tests/unit/models/proposal-test.js +++ b/tests/unit/models/proposal-test.js @@ -1,9 +1,49 @@ import { moduleFor, test } from 'ember-qunit'; +import schemas from 'npm:kosmos-schemas'; +import tv4 from 'npm:tv4'; moduleFor('model:proposal', 'Unit | Model | proposal'); -test('it exists', function(assert) { +test('#toJSON() requires a recipient profile IPFS hash to be set', function(assert) { let model = this.subject(); - // let store = this.store(); - assert.ok(!!model); + + model.setProperties({ + recipientAddress: '0xd4a64570b12da659ee4bbd41c3509b7b1f9c51ac' + }); + + try { + let json = model.contributionToJSON(); + assert.notOk(json, true); + } catch(e) { + assert.ok(e.message.match(/IPFS hash .* missing/i)); + } +}); + +test('#toJSON() requires kind and description to be set', function(assert) { + let model = this.subject(); + + model.setProperties({ + recipientProfile: 'QmT2A7rY4e7uoKktkcFHQNN7BD1oXdZTgd8wNkr1u9nNVE' + }); + + try { + let json = model.contributionToJSON(); + assert.notOk(json, true); + } catch(e) { + assert.ok(e.message.match(/Missing .* kind.*description/i)); + } +}); + +test('#toJSON() returns a valid JSON-LD representation of the model', function(assert) { + let model = this.subject(); + + model.setProperties({ + recipientAddress: '0xd4a64570b12da659ee4bbd41c3509b7b1f9c51ac', + kind: 'design', + description: 'New logo design', + url: 'http://opensourcedesign.org', + recipientProfile: 'QmT2A7rY4e7uoKktkcFHQNN7BD1oXdZTgd8wNkr1u9nNVE' + }); + + assert.ok(tv4.validate(model.contributionToJSON(), schemas['contribution'])); });