Compare commits

..

3 Commits

Author SHA1 Message Date
basti 970b390509 Show contribution history from events 2017-08-14 17:33:38 +02:00
basti b8f4040def Get contributions from minted events (WIP) 2017-08-14 17:15:11 +02:00
basti 8fbecd928b Update deps 2017-08-14 17:14:17 +02:00
175 changed files with 2097 additions and 40472 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"directory": "bower_components",
"analytics": false
}
+1 -1
View File
@@ -5,5 +5,5 @@
Setting `disableAnalytics` to true will prevent any data from being sent. Setting `disableAnalytics` to true will prevent any data from being sent.
*/ */
"disableAnalytics": true "disableAnalytics": false
} }
-21
View File
@@ -1,21 +0,0 @@
# unconventional js
/blueprints/*/files/
/vendor/
# compiled output
/dist/
/tmp/
/release/
# dependencies
/bower_components/
/node_modules/
# misc
/coverage/
!.*
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try
-55
View File
@@ -1,55 +0,0 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module'
},
globals: {
console: true
},
plugins: [
'ember'
],
extends: [
'eslint:recommended',
'plugin:ember/recommended'
],
env: {
browser: true
},
rules: {
'ember/avoid-leaking-state-in-ember-objects': 'warn',
'no-console': 'off'
},
overrides: [
// node files
{
files: [
'.eslintrc.js',
'.template-lintrc.js',
'ember-cli-build.js',
'testem.js',
'blueprints/*/index.js',
'config/**/*.js',
'lib/*/index.js',
'server/**/*.js'
],
parserOptions: {
sourceType: 'script',
ecmaVersion: 2015
},
env: {
browser: false,
node: true
},
plugins: ['node'],
rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {
// add your custom rules and overrides for node files here
// this can be removed once the following is fixed
// https://github.com/mysticatea/eslint-plugin-node/issues/77
'node/no-unpublished-require': 'off'
})
}
]
};
-4
View File
@@ -1,4 +0,0 @@
template: |
## Changes
$CHANGES
+7 -16
View File
@@ -1,26 +1,17 @@
# See https://help.github.com/ignore-files/ for more about ignoring files. # See https://help.github.com/ignore-files/ for more about ignoring files.
# compiled output # compiled output
/dist/ /dist
/tmp/ /tmp
# dependencies # dependencies
/bower_components/ /node_modules
/node_modules/ /bower_components
# misc # misc
/.env*
/.pnp*
/.sass-cache /.sass-cache
/connect.lock /connect.lock
/coverage/ /coverage/*
/libpeerconnection.log /libpeerconnection.log
/npm-debug.log* npm-debug.log*
/testem.log testem.log
/yarn-error.log
.tm_properties
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try
+32
View File
@@ -0,0 +1,32 @@
{
"predef": [
"document",
"window",
"-Promise"
],
"browser": true,
"boss": true,
"curly": true,
"debug": false,
"devel": true,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esversion": 6,
"unused": true
}
-10
View File
@@ -1,10 +0,0 @@
'use strict';
module.exports = {
extends: 'recommended',
rules: {
'simple-unless': false,
'no-nested-interactive': false
}
};
+12 -16
View File
@@ -1,29 +1,25 @@
--- ---
language: node_js language: node_js
node_js: node_js:
- "10" - "6"
sudo: false sudo: false
dist: xenial
notifications:
email:
on_success: never
on_failure: always
addons:
chrome: stable
cache: cache:
directories: directories:
- $HOME/.npm - $HOME/.npm
- $HOME/.cache # includes bowers cache
env: before_install:
global: - npm config set spin false
# See https://git.io/vdao3 for details. - npm install -g bower
- JOBS=1 - bower --version
- npm install phantomjs-prebuilt
- node_modules/phantomjs-prebuilt/bin/phantomjs --version
install:
- npm install
- bower install
script: script:
- npm run lint:hbs
- npm run lint:js
- npm test - npm test
+21 -66
View File
@@ -10,24 +10,22 @@ A short introduction of this app could easily go here.
You will need the following things properly installed on your computer. You will need the following things properly installed on your computer.
* [Git](https://git-scm.com/) * [Git](https://git-scm.com/)
* [Node.js](https://nodejs.org/) (with npm) * [Node.js](https://nodejs.org/) (with NPM)
* [Bower](https://bower.io/)
* [Ember CLI](https://ember-cli.com/) * [Ember CLI](https://ember-cli.com/)
* [Google Chrome](https://google.com/chrome/) * [PhantomJS](http://phantomjs.org/)
## Installation ## Installation
* `git clone git@github.com:67P/kredits-web.git` this repository * `git clone <repository-url>` this repository
* `cd kredits-web` * `cd kredits-web`
* `npm install` * `npm install`
* `bower install`
## Running / Development ## Running / Development
* `ember serve`
* `ember serve` - by default kredits-web connects to the Ethreum Kovan network
* Visit your app at [http://localhost:4200](http://localhost:4200). * Visit your app at [http://localhost:4200](http://localhost:4200).
* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
See [working with locally deployed contracts](https://github.com/67P/kredits-web#working-with-locally-deployed-contracts) for details on how to develop with locally deployed contracts.
### Code Generators ### Code Generators
@@ -38,12 +36,6 @@ Make use of the many generators for code, try `ember help generate` for more det
* `ember test` * `ember test`
* `ember test --server` * `ember test --server`
### Linting
* `npm run lint:hbs`
* `npm run lint:js`
* `npm run lint:js -- --fix`
### Building ### Building
* `ember build` (development) * `ember build` (development)
@@ -51,72 +43,35 @@ Make use of the many generators for code, try `ember help generate` for more det
### Deploying ### Deploying
_(You need collaborator permissions on the 5apps Deploy project.)_ Specify what it takes to deploy your app.
`npm run deploy`
## Working with locally deployed contracts ## Working with locally deployed contracts
The smart contracts and their JavaScript wrapper library are developed in the For development you might want to run your own development chain with your own development contracts.
[kredits-contracts](https://github.com/67P/kredits-contracts) repo/package. The kredits-contracts helper scripts are available in kredits-web. See [kredits-contracts](https://github.com/67P/kredits-contracts) for more information.
You can run `kredits-web` on your machine, against a local, simulated Ethereum Start your parity node with a KreditsChain:
network, provided e.g. by [ganache](http://truffleframework.com/ganache/).
npm run kredits-parity
[kredits-contracts](https://github.com/67P/kredits-contracts) holds all the tools Deploy the contracts to your local chain:
to start and set up such a simulated network, as well as to deploy smart
contracts to it.
These are the basic steps to get up and running: npm run kredits-deploy
#### 1. IPFS Run kredits-web with your localy deployed contracts:
Run a local IPFS deamon. KREDITS_CONTRACT_ADDR=`kredits-inspect -c Kredits -r -w address` \
TOKEN_CONTRACT_ADDR=`kredits-inspect -c Token -r -w address` \
npm start
* Make sure CORS headers are configured. See [IPFS](#ipfs) for more info. You need to have `./node_modules/.bin` in your path for this to work. Otherwise simply copy the contract addresses and set the environment variables manually.
* `ipfs daemon`
#### 2. kredits-contracts
Get your local Ethereum development node running. (See [kredits-contracts README](https://github.com/67P/kredits-contracts)
for details.
* Clone [kredits-contracts](https://github.com/67P/kredits-contracts)
* `npm install`
* `npm run devchain` - runs a local development chain
* `npm run bootstrap` - bootstrap runs deploys all the contracts
* `npm link` - make the `kredits-contracts` module linkable as `kredits-contracts` on your machine (currently broken :( )
#### 3. kredits-web
With IPFS and Ethereum/ganache running, you can now start this Ember app.
* `npm link kredits-contracts` - link the local `kredits-contracts` package
* `npm run start:local` - WEB3_PROVIDER_URL=http://localhost:7545 must be set for local settings
If you restart the devchain your network ID changes. So kredits-contracts does not find the correct DAO address.
If that is the case the DAO address must be provided as `KREDITS_DAO_ADDRESS` environment variable.
* `npm run dao:address` in the kredits-contracts repo to find your address
* `KREDITS_DAO_ADDRESS=<0xYOURADDRESS> npm run start:local`
#### 4. Metamask network
Switch the network in Metamask to "Custom RPC" with the RPC URL `http://localhost:7545`.
#### IPFS
Install IPFS with your favorite package manager and run
ipfs init (on initial installation)
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["localhost:4200"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
ipfs config Addresses.Gateway /ip4/127.0.0.1/tcp/8080
## Further Reading / Useful Links ## Further Reading / Useful Links
* [ember.js](https://emberjs.com/) * [ember.js](http://emberjs.com/)
* [ember-cli](https://ember-cli.com/) * [ember-cli](https://ember-cli.com/)
* Development Browser Extensions * Development Browser Extensions
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
+6 -2
View File
@@ -1,9 +1,13 @@
import Application from '@ember/application'; import Ember from 'ember';
import Resolver from './resolver'; import Resolver from './resolver';
import loadInitializers from 'ember-load-initializers'; import loadInitializers from 'ember-load-initializers';
import config from './config/environment'; import config from './config/environment';
const App = Application.extend({ let App;
Ember.MODEL_FACTORY_INJECTIONS = true;
App = Ember.Application.extend({
modulePrefix: config.modulePrefix, modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix, podModulePrefix: config.podModulePrefix,
Resolver Resolver
@@ -1,75 +0,0 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed';
import moment from 'moment';
export default Component.extend({
attributes: null,
contributors: Object.freeze([]),
isValidContributor: notEmpty('contributorId'),
isValidKind: notEmpty('kind'),
isValidAmount: computed('amount', function() {
return parseInt(this.amount, 10) > 0;
}),
isValidDescription: notEmpty('description'),
isValidUrl: notEmpty('url'),
isValid: and('isValidContributor',
'isValidKind',
'isValidAmount',
'isValidDescription'),
init () {
this._super(...arguments);
this.set('defaultDate', moment().startOf('hour').toDate());
// Default attributes used by reset
this.set('attributes', {
contributorId: null,
kind: null,
date: this.defaultDate,
amount: null,
description: null,
url: null,
});
this.reset();
},
reset () {
this.setProperties(this.attributes);
},
actions: {
submit () {
if (!this.isValid) {
alert('Invalid data. Please review and try again.');
return;
}
const attributes = this.getProperties(Object.keys(this.attributes));
let dateInput = (attributes.date instanceof Array) ?
attributes.date[0] : attributes.date;
const [ date, time ] = dateInput.toISOString().split('T');
[ attributes.date, attributes.time ] = [ date, time ];
this.set('inProgress', true);
this.save(attributes)
.then((/*contribution*/) => {
this.reset();
}, err => {
console.warn(err);
window.alert('Something went wrong. Check the browser console for details.');
})
.finally(() => this.set('inProgress', false));
}
}
});
@@ -1,72 +0,0 @@
<form {{action "submit" on="submit"}}>
<label>
<p class="label">Contributor:</p>
<p>
<select required onchange={{action (mut contributorId) value="target.value"}}>
<option value="" selected disabled hidden></option>
{{#each contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}}
</select>
</p>
</label>
<label>
<p class="label">Kind:</p>
<p>
<select required onchange={{action (mut kind) value="target.value"}}>
<option value="" selected disabled hidden></option>
<option value="community" selected={{eq kind "community"}}>Community</option>
<option value="design" selected={{eq kind "design"}}>Design</option>
<option value="dev" selected={{eq kind "dev"}}>Development</option>
<option value="docs" selected={{eq kind "docs"}}>Documentation</option>
<option value="ops" selected={{eq kind "ops"}}>IT Operations</option>
<option value="special" selected={{eq kind "special"}}>Special</option>
</select>
</p>
</label>
<label>
<p class="label">Date:</p>
<p>
{{ember-flatpickr
date=date
defaultDate=defaultDate
maxDate=defaultDate
enableTime=true
time_24hr=true
onChange=(action (mut date))
}}
</p>
</label>
<label>
<p class="label">Amount:</p>
<p>
{{input type="text"
placeholder="500"
value=amount
class=(if isValidAmount "valid" "")}}
</p>
</label>
<label>
<p class="label">Description:</p>
<p>
{{input type="text"
value=description
class=(if isValidDescription "valid" "")}}
</p>
</label>
<label>
<p class="label">URL (optional):</p>
<p>
{{input type="text"
value=url
class=(if isValidUrl "valid" "")}}
</p>
</label>
<p class="actions">
{{input type="submit"
disabled=inProgress
value=(if inProgress "Processing" "Save")}}
</p>
</form>
+55 -57
View File
@@ -1,81 +1,79 @@
import Component from '@ember/component'; import Ember from 'ember';
import { computed } from '@ember/object'; import Contributor from 'kredits-web/models/contributor';
import { and, notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service'; const {
import { isPresent } from '@ember/utils'; Component,
import { isAddress } from 'web3-utils'; isPresent,
inject: {
service
},
computed
} = Ember;
export default Component.extend({ export default Component.extend({
kredits: service(), kredits: service(),
attributes: null, newContributor: null,
inProgress: false,
isValidAccount: computed('account', function() { isValidAddress: function() {
return isAddress(this.account); return this.get('kredits.web3')
}), .isAddress(this.get('newContributor.address'));
}.property('kredits.web3', 'newContributor.address'),
isValidName: notEmpty('name'), isValidName: function() {
isValidURL: notEmpty('url'), return isPresent(this.get('newContributor.name'));
isValidGithubUID: notEmpty('github_uid'), }.property('newContributor.name'),
isValidGithubUsername: notEmpty('github_username'),
isValidGiteaUsername: notEmpty('gitea_username'),
isValidWikiUsername: notEmpty('wiki_username'),
isValid: and( isValidURL: function() {
'isValidAccount', 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', 'isValidName',
'isValidGithubUID' 'isValidGithubUID'
), ),
inProgress: false,
init () {
this._super(...arguments);
this.setDefaultAttributes();
this.reset();
},
setDefaultAttributes () {
if (isPresent(this.attributes)) { return; }
this.set('attributes', {
account: null,
name: null,
kind: 'person',
url: null,
github_username: null,
github_uid: null,
gitea_username: null,
wiki_username: null
});
},
reset: function() { reset: function() {
this.setProperties(this.attributes); this.setProperties({
newContributor: Contributor.create({ kind: 'person' }),
inProgress: false
});
}, },
actions: { actions: {
submit () { save() {
if (!this.isValid) { if (!this.get('contractInteractionEnabled')) {
alert('Invalid data. Please review and try again.'); alert('Only core team members can add new contributors. Please ask someone to set you up.');
return; return;
} }
const attributes = Object.keys(this.attributes); if (this.get('isValid')) {
const contributor = this.getProperties(attributes); this.set('inProgress', true);
this.set('inProgress', true); this.get('kredits').addContributor(this.get('newContributor')).then(contributor => {
this.reset();
this.save(contributor).then(() => { this.get('contributors').pushObject(contributor);
this.reset(); window.scroll(0,0);
}).catch(err => { });
console.warn(err); } else {
window.alert('Something went wrong. Please check the browser console.'); alert('Invalid data. Please review and try again.');
}).finally(() => { }
this.set('inProgress', false);
});
} }
} }
+41 -32
View File
@@ -1,50 +1,59 @@
<form {{action "submit" on="submit"}}> <form {{action "save" on="submit"}}>
<p> <p>
<label for="c-account">Ethereum account</label> {{input type="checkbox" name="is-core" id="is-core" checked=newContributor.isCore}}
{{input name="account" id="c-account" <label for="is-core" class="checkbox">
type="text" value=account Core team member (can add contributors)
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" </label>
class=(if isValidAccount "valid" "")}}
</p> </p>
<p> <p>
<label for="c-kind">Kind</label> {{input name="address"
<select required onchange={{action (mut kind) value="target.value"}} id="c-kind"> type="text"
<option value="person" selected={{eq kind "person"}}>Person</option> placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
<option value="organization" selected={{eq kind "organization"}}>Organization</option> value=newContributor.address
class=(if isValidAddress 'valid' '')}}
</p>
<p>
<select required onchange={{action (mut newContributor.kind) value="target.value"}}>
<option value="person" selected={{eq newContributor.kind "person"}}>Person</option>
<option value="organization" selected={{eq newContributor.kind "organization"}}>Organization</option>
</select> </select>
</p> </p>
<p> <p>
<label for="c-name">Name</label> {{input name="name"
{{input name="name" type="text" value=name placeholder="Zero Cool" type="text"
class=(if isValidName "valid" "") id="c-name"}} placeholder="Name"
value=newContributor.name
class=(if isValidName 'valid' '')}}
</p> </p>
<p> <p>
<label for="c-url">URL</label> {{input name="url"
{{input name="url" type="text" value=url placeholder="http://zerocool.bit" type="text"
class=(if isValidURL "valid" "") id="c-url"}} placeholder="URL"
value=newContributor.url
class=(if isValidURL 'valid' '')}}
</p> </p>
<p> <p>
<label for="c-github-uid">GitHub UID</label> {{input name="github_uid"
{{input name="github_uid" type="text" value=github_uid placeholder="2342" type="text"
class=(if isValidGithubUID "valid" "") id="c-github-uid"}} placeholder="GitHub UID (123)"
value=newContributor.github_uid
class=(if isValidGithubUID 'valid' '')}}
</p> </p>
<p> <p>
<label for="c-github-username">GitHub username</label> {{input name="github_username"
{{input name="github_username" type="text" value=github_username placeholder="zerocool" type="text"
class=(if isValidGithubUsername "valid" "") id="c-github-username"}} placeholder="GitHub username"
value=newContributor.github_username
class=(if isValidGithubUsername 'valid' '')}}
</p> </p>
<p> <p>
<label for="c-gitea-username">Gitea username</label> {{input name="wiki_username"
{{input name="gitea_username" type="text" value=gitea_username placeholder="zerocool" type="text"
class=(if isValidGiteaUsername "valid" "") id="c-gitea-username"}} placeholder="Wiki Username"
</p> value=newContributor.wiki_username
<p> class=(if isValidWikiUsername 'valid' '')}}
<label for="c-wiki-username">Wiki username</label>
{{input name="wiki_username" type="text" value=wiki_username placeholder="ZeroCool"
class=(if isValidWikiUsername "valid" "") id="c-wiki-username"}}
</p> </p>
<p class="actions"> <p class="actions">
{{input type="submit" disabled=inProgress {{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
value=(if inProgress "Processing" "Save")}}
</p> </p>
</form> </form>
+47 -45
View File
@@ -1,63 +1,65 @@
import Component from '@ember/component'; import Ember from 'ember';
import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed'; const {
Component,
isPresent,
inject: {
service
},
computed
} = Ember;
export default Component.extend({ export default Component.extend({
attributes: null, kredits: service(),
contributors: Object.freeze([]),
isValidContributor: notEmpty('contributorId'), proposal: null,
isValidAmount: computed('amount', function() { contributors: null,
return parseInt(this.amount, 10) > 0; inProgress: false,
isValidRecipient: computed('proposal.recipientAddress', function() {
return this.get('kredits.web3').isAddress(this.get('proposal.recipientAddress'));
}), }),
isValidDescription: notEmpty('description'),
isValidUrl: notEmpty('url'),
isValid: and('isValidContributor',
'isValidAmount',
'isValidDescription'),
init () { isValidAmount: computed('proposal.amount', function() {
this._super(...arguments); return parseInt(this.get('proposal.amount'), 10) > 0;
}),
// Default attributes used by reset isValidUrl: computed('proposal.url', function() {
this.set('attributes', { return isPresent(this.get('proposal.url'));
contributorId: null, }),
kind: 'community',
amount: null,
description: null,
url: null,
});
},
didInsertElement() { isValidDescription: computed('proposal.description', function() {
this._super(...arguments); return isPresent(this.get('proposal.description'));
this.reset(); }),
},
reset: function() { isValid: computed.and('isValidRecipient',
this.setProperties(this.attributes); 'isValidAmount',
}, 'isValidDescription'),
actions: { actions: {
submit() { save() {
if (!this.isValid) { if (! this.get('isValid')) {
alert('Invalid data. Please review and try again.'); alert('Invalid data. Please review and try again.');
return; return false;
} }
this.set('inProgress', true);
let proposal = this.get('proposal');
let attributes = Object.keys(this.attributes); // Set the recipient's IPFS profile hash so it can be used in the
let proposal = this.getProperties(attributes); // contribution object (which is to be stored in IPFS as well)
let saved = this.save(proposal); let contributor = this.get('contributors').findBy('address', proposal.get('recipientAddress'));
proposal.set('recipientProfile', contributor.get('ipfsHash'));
// The promise handles inProgress this.get('kredits').addProposal(proposal)
this.set('inProgress', saved); .then(() => {
this.attrs.onSave();
saved.then(() => { }).catch((error) => {
this.reset(); Ember.Logger.error('[add-proposal] error creating the proposal', error);
window.scroll(0,0); alert('Something went wrong.');
window.alert('Proposal added.'); }).finally(() => {
}); this.set('inProgress', false);
});
} }
} }
+17 -19
View File
@@ -1,44 +1,42 @@
<form {{action "submit" on="submit"}}> <form {{action "save" on="submit"}}>
<p> <p>
<select required onchange={{action (mut contributorId) value="target.value"}}> <select required onchange={{action (mut proposal.recipientAddress) value="target.value"}}>
<option value="" selected disabled hidden>Contributor</option> <option value="" selected disabled hidden>Contributor</option>
{{#each contributors as |contributor|}} {{#each contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.github_username}}</option> <option value={{contributor.address}} selected={{eq proposal.recipientAddress contributor.address}}>{{contributor.github_username}}</option>
{{/each}} {{/each}}
</select> </select>
</p> </p>
<p> <p>
<select required onchange={{action (mut kind) value="target.value"}}> <select required onchange={{action (mut proposal.kind) value="target.value"}}>
<option value="community" selected={{eq kind "community"}}>Community</option> <option value="community" selected={{eq proposal.kind "community"}}>Community</option>
<option value="design" selected={{eq kind "design"}}>Design</option> <option value="design" selected={{eq proposal.kind "design"}}>Design</option>
<option value="dev" selected={{eq kind "dev"}}>Development</option> <option value="dev" selected={{eq proposal.kind "dev"}}>Development</option>
<option value="docs" selected={{eq kind "docs"}}>Documentation</option> <option value="docs" selected={{eq proposal.kind "docs"}}>Documentation</option>
<option value="ops" selected={{eq kind "ops"}}>IT Operations</option> <option value="ops" selected={{eq proposal.kind "ops"}}>IT Operations</option>
</select> </select>
</p> </p>
<p> <p>
{{input type="text" {{input type="text"
placeholder="100" placeholder="100"
value=amount value=proposal.amount
class=(if isValidAmount "valid" "")}} class=(if isValidAmount 'valid' '')}}
</p> </p>
<p> <p>
{{input type="text" {{input type="text"
placeholder="Description" placeholder="Description"
value=description value=proposal.description
class=(if isValidDescription "valid" "")}} class=(if isValidDescription 'valid' '')}}
</p> </p>
<p> <p>
{{input type="text" {{input type="text"
placeholder="URL (optional)" placeholder="URL (optional)"
value=url value=proposal.url
class=(if isValidUrl "valid" "")}} class=(if isValidUrl 'valid' '')}}
</p> </p>
<p class="actions"> <p class="actions">
{{input type="submit" {{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
disabled=(is-pending inProgress) {{#link-to 'index'}}Back{{/link-to}}
value=(if (is-pending inProgress) "Processing" "Save")}}
{{#link-to "index"}}Back{{/link-to}}
</p> </p>
</form> </form>
@@ -1,64 +0,0 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
let categoryColors = {
community: "#fb6868",
design: "#fbe468",
dev: "#e068fb",
docs: "#97fb68",
ops: "#8f68fb",
}
export default Component.extend({
contributions: null,
chartOptions: Object.freeze({
legend: {
display: false
}
}),
chartData: computed('contributions', function() {
let kredits = this.contributions
.filterBy('vetoed', false)
.map(c => {
return { kind: c.kind, amount: c.amount }
})
.reduce(function (kinds, c) {
if (c.kind in kinds) {
kinds[c.kind] = kinds[c.kind] + c.amount
} else {
kinds[c.kind] = c.amount;
}
return kinds;
}, {});
return {
datasets: [{
data: [
kredits['community'],
kredits['design'],
kredits['dev'],
kredits['ops'],
kredits['docs'],
],
borderColor: [
categoryColors.community,
categoryColors.design,
categoryColors.dev,
categoryColors.ops,
categoryColors.docs,
],
borderWidth: 1
}],
labels: [
'Community',
'Design',
'Development',
'Operations & Infrastructure',
'Documentation'
],
}
})
});
@@ -1,7 +0,0 @@
<div class="chart">
{{ember-chart type="doughnut"
data=chartData
options=chartOptions
width=200
height=200}}
</div>
+10 -66
View File
@@ -1,72 +1,16 @@
import Component from '@ember/component'; import Ember from 'ember';
import { computed } from '@ember/object';
import { sort } from '@ember/object/computed';
import { isPresent } from '@ember/utils';
import { inject as service } from '@ember/service';
export default Component.extend({ export default Ember.Component.extend({
router: service(), tagName: 'ul',
classNames: ['contribution-list'],
tagName: 'div', contributionsWithContributors: function() {
classNames: ['contributions'], return this.get('contributions').map((c) => {
c.set('contributor', this.get('contributors')
selectedContribution: null, .findBy('address', c.get('recipientAddress')));
return c;
showQuickFilter: false,
hideSmallContributions: false,
contributorId: null,
contributionKind: null,
kredits: service(),
contributorsSorting: Object.freeze(['name:asc']),
contributors: sort('kredits.contributors', 'contributorsSorting'),
contributorsActive: computed('contributors.[]', 'contributions', function() {
let activeIds = this.contributions.mapBy('contributorId')
.map(id => id.toString())
.uniq();
return this.contributors.filter(c => {
return activeIds.includes(c.id.toString());
}); });
}), }.property('contributions.@each')
contributionKinds: computed('contributions.[]', function() {
return this.contributions.mapBy('kind').uniq();
}),
contributionsFiltered: computed('contributions.[]', 'hideSmallContributions', 'contributorId', 'contributionKind', function() {
return this.contributions.filter(c => {
let included = true;
if (this.hideSmallContributions &&
c.amount <= 500) { included = false; }
if (isPresent(this.contributorId) &&
c.contributorId.toString() !== this.contributorId.toString()) { included = false; }
if (isPresent(this.contributionKind) &&
c.kind !== this.contributionKind) { included = false; }
return included;
});
}),
actions: {
veto (contributionId) {
if (this.contractInteractionEnabled) {
this.vetoContribution(contributionId);
} else {
window.alert('Only members can veto contributions. Please ask someone to set you up.');
}
},
openContributionDetails(contribution) {
this.router.transitionTo('dashboard.contributions.show', contribution);
}
}
}); });
+7 -54
View File
@@ -1,54 +1,7 @@
{{#if showQuickFilter}} {{#each contributionsWithContributors as |contribution|}}
<div class="quick-filter"> <li title="({{contribution.kind}}) {{contribution.description}}">
<p> <span class="category {{contribution.kind}}">♥</span>
<label class="filter-contributor"> <span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
Contributor: for <span class="recipient">{{contribution.contributor.name}}</span>
<select onchange={{action (mut contributorId) value="target.value"}}> </li>
<option value="" selected>all</option> {{/each}}
{{#each contributorsActive as |contributor|}}
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}}
</select>
</label>
<label class="filter-contribution-kind">
Kind:
<select onchange={{action (mut contributionKind) value="target.value"}}>
<option value="" selected>all</option>
{{#each contributionKinds as |kind|}}
<option value={{kind}} selected={{eq contributionKind kind}}>{{capitalize-string kind}}</option>
{{/each}}
</select>
</label>
<label class="filter-contribution-size">
{{input type="checkbox" checked=hideSmallContributions}}
Hide small contributions
</label>
</p>
</div>
{{/if}}
<ul class="contribution-list">
{{#each contributionsFiltered as |contribution|}}
<li role="button" {{action "openContributionDetails" contribution}}
data-contribution-id={{contribution.id}}
class="{{contribution-status contribution}}{{if contribution.vetoed " vetoed"}}{{if (eq contribution.id selectedContributionId) " selected"}}">
<p class="meta">
<span class="recipient">{{user-avatar contributor=contribution.contributor}}</span>
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
<span class="title">{{contribution.description}}</span>
</p>
<p class="kredits-amount">
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
</p>
{{#unless contribution.vetoed}}
{{#unless (is-confirmed-contribution contribution)}}
<p class="voting">
<button {{action "veto" contribution.id}} class="small danger">veto</button>
</p>
{{/unless}}
{{/unless}}
</li>
{{/each}}
</ul>
+10 -9
View File
@@ -1,19 +1,20 @@
import Component from '@ember/component'; import Ember from 'ember';
import { inject as service } from '@ember/service';
export default Component.extend({ export default Ember.Component.extend({
router: service(),
tagName: 'table', tagName: 'table',
classNames: 'contributor-list', classNames: 'contributor-list',
selectedContributor: null,
selectedContributorId: null,
actions: { actions: {
openContributorDetails(contributor) { toggleContributorInfo(contributor) {
this.router.transitionTo('dashboard.contributors.show', contributor); if (contributor.get('showMetadata')) {
contributor.set('showMetadata', false);
} else {
this.get('contributors').setEach('showMetadata', false);
contributor.set('showMetadata', true);
}
} }
} }
+15 -11
View File
@@ -1,20 +1,24 @@
<tbody> <tbody>
{{#each contributorList as |c|}} {{#each contributors as |contributor|}}
<tr role="button" {{action "openContributorDetails" c.contributor}} <tr class="{{if contributor.isCurrentUser 'current-user'}}" {{action "toggleContributorInfo" contributor}}>
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id selectedContributorId) "selected"}}">
<td class="person"> <td class="person">
{{user-avatar contributor=c.contributor}} {{c.contributor.name}} <img class="avatar" src={{contributor.avatarURL}}>
{{contributor.name}}
</td> </td>
<td class="kredits"> <td class="kredits">
<span class="amount"> <span class="amount">{{contributor.kredits}}</span>
{{#if showUnconfirmedKredits}}
{{c.amountTotal}}
{{else}}
{{c.amountConfirmed}}
{{/if}}
</span>
<span class="symbol">₭S</span> <span class="symbol">₭S</span>
</td> </td>
</tr> </tr>
<tr class="metadata {{if contributor.isCurrentUser 'current-user'}} {{if contributor.showMetadata 'visible'}}">
<td colspan="2">
<ul>
<li><a href="https://testnet.etherscan.io/address/{{contributor.address}}">Inspect Ethereum transactions</a></li>
{{#if contributor.ipfsHash}}
<li><a href="https://ipfs.io/ipfs/{{contributor.ipfsHash}}">Inspect IPFS profile</a></li>
{{/if}}
</ul>
</td>
</tr>
{{/each}} {{/each}}
</tbody> </tbody>
@@ -1,15 +0,0 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
tagName: "",
account: null,
iconComponentName: computed('account.site', function() {
if (this.account.site.match(/github|gitea|wiki/)) {
return 'icon-account-' + this.account.site.replace(/\./g, '-');
} else {
return 'icon-web-globe';
}
})
});
@@ -1,4 +0,0 @@
<a href={{account.url}} target="_blank" rel="noopener" title="{{account.username}} on {{account.site}}">
<span class="site">{{account.site}}</span>
{{component iconComponentName}}
</a>
@@ -1,5 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: ""
});
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.0 KiB

@@ -1,5 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: ""
});
@@ -1,8 +0,0 @@
<svg id="svg825" class="icon-github" width="260" height="260" enable-background="new 0 0 212.4575 186.23" version="1.1" viewBox="0 0 260 260" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<g transform="translate(23.766 36.885)">
<path d="m151.79 86.234c-7.078 0.1229-14.154 0.5605-21.232 0.7069-12.318 0.248-24.64 0.5664-36.961 0.512-11.176-0.0472-22.358-0.9728-33.518-0.7052-7.3164 0.1776-14.373 2.2948-20.131 7.584-9.9472 9.1368-12.522 21-11.314 33.807 1.92 20.357 9.2716 31.449 29.115 37.064 15.793 4.4704 31.963 5.2088 48.199 4.9744 5.254 0 10.51 0.1367 15.758-0.0292 12.723-0.4064 25.264-2.2072 37.358-6.3596 10.846-3.7225 18.799-10.691 22.572-22.166 2.6736-8.1288 3.7148-16.388 2.5544-24.853-1.7772-12.959-13.275-30.86-32.4-30.535zm-81.576 59.996c-8.836 0-16-8.9551-16-20 0-11.045 7.164-20 16-20s16 8.9553 16 20c0 11.045-7.164 20-16 20zm76 0c-8.836 0-16-8.9551-16-20 0-11.045 7.164-20 16-20s16 8.9553 16 20c0 11.045-7.164 20-16 20z" fill="none"/>
<path class="fg" d="m200.25 59.188c-2.5076-3.4493-4.3632-6.3888-3.8748-11.44 0.6796-7.0664-0.3536-14.396-1.3204-21.518-1.2188-8.9843-3.5236-17.74-7.1776-26.23-9.0332 1.9356-17.981 3.49-25.963 8.0976-8.4648 4.8908-16.695 10.215-25.08 15.25-0.9532 0.5704-2.3124 0.8848-3.3848 0.7032-17.967-3.1368-35.92-3.0196-53.898-0.086-1.3632 0.2244-3.0724-0.2345-4.2948-0.9608-4.4436-2.6604-8.6564-5.7344-13.123-8.3516-11.656-6.844-23.66-12.822-37.494-14.59-0.3928 0.8597-0.7188 1.4785-0.9612 2.1253-4.164 11.22-6.6424 22.844-7.4276 34.82-0.2872 4.3516-0.6092 8.8048 0 13.072 0.3456 2.4296-4e-3 3.8788-1.3924 5.6132-4.8516 6.0488-8.6368 12.717-11.102 20.219-4.6972 14.295-4.4004 28.861-2.2756 43.475 1.7268 11.974 4.7344 23.537 11.324 33.873 13.186 20.682 32.892 29.774 55.572 31.705 19.308 1.6464 38.8 1.1992 58.213 1.2364 13.855 0.0251 27.57-1.4924 40.715-6.6389 20.375-7.9727 33.859-22.604 40.275-44.166 3.7344-12.545 5.2324-25.396 4.8144-38.498-0.4412-13.81-4.0504-26.566-12.144-37.711zm-18.609 82.435c-3.7736 11.475-11.727 18.444-22.572 22.166-12.094 4.1524-24.635 5.9532-37.358 6.3596-5.248 0.1659-10.504 0.0292-15.758 0.0292-16.236 0.2344-32.406-0.504-48.199-4.9744-19.844-5.6152-27.195-16.707-29.115-37.064-1.2072-12.807 1.3672-24.67 11.314-33.807 5.7576-5.2892 12.814-7.4064 20.131-7.584 11.16-0.2676 22.342 0.658 33.518 0.7052 12.32 0.0544 24.642-0.264 36.961-0.512 7.0784-0.1464 14.154-0.584 21.232-0.7069 19.125-0.3243 30.623 17.576 32.4 30.535 1.1604 8.4647 0.1192 16.724-2.5544 24.853z"/>
<ellipse class="fg" cx="70.214" cy="126.23" rx="16" ry="20"/>
<ellipse class="fg" cx="146.21" cy="126.23" rx="16" ry="20"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

@@ -1,5 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: ""
});
@@ -1,8 +0,0 @@
<svg class="icon-wiki" width="120" height="120" version="1.1" viewBox="0 0 120 120" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<g transform="translate(18.1,3)">
<polygon class="fg" transform="translate(-6.2,9)" points="6.2 5 45.7 20.5 45.7 91 6.2 75.5"/>
<polygon class="fg" transform="translate(-6.2,9)" points="90 5 50.5 20.5 50.5 91 90 75.5"/>
<polygon class="fg" transform="translate(-6.2,9)" points="87 88 87 81.8 60.4 91"/>
<polygon class="fg" transform="translate(-6.2,9)" points="9.2 88 9.2 81.8 34.9 91"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 703 B

@@ -1,5 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: ""
});
@@ -1,3 +0,0 @@
<svg class="icon-web-globe" width="60" height="60" enable-background="new 0 0 64 64" version="1.1" viewBox="0 0 60 60" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<path class="fg" d="m48.1 14.3c-0.4-0.5-0.9-1-1.4-1.5-4.3-4.2-10.2-6.8-16.7-6.8s-12.4 2.6-16.7 6.8c-0.5 0.5-1 1-1.4 1.5-3.7 4.2-5.9 9.7-5.9 15.7s2.2 11.5 5.8 15.7c0.4 0.5 0.9 1 1.4 1.5 4.3 4.2 10.3 6.8 16.8 6.8s12.5-2.6 16.8-6.9c0.5-0.5 0.9-0.9 1.4-1.5 3.6-4.1 5.8-9.6 5.8-15.6s-2.2-11.5-5.9-15.7zm3.9 14.7h-8.1c-0.1-3-1-7.3-2.4-10.5 1.8-0.8 3.5-1.8 5.1-3 3.1 3.6 5.1 8.5 5.4 13.5zm-21-20.1c3 2.3 5.8 5.2 7.6 8.5-2.4 0.9-4.6 1.3-7.6 1.4zm-2 9.9c-3-0.1-5.2-0.6-7.6-1.4 1.8-3.3 4.6-6.3 7.6-8.5zm0 2v8.2h-10.8c0.1-3 0.9-6.8 2.3-9.8 2.7 1 5.5 1.5 8.5 1.6zm0 10.2v8c-3 0.1-5.9 0.7-8.6 1.7-1.4-3-2.1-5.7-2.3-9.7zm0 10v10.1c-3-2.3-5.8-5.3-7.7-8.6 2.4-0.9 4.7-1.4 7.7-1.5zm2 0c3 0.1 5.3 0.6 7.7 1.5-1.9 3.4-4.7 6.3-7.7 8.6zm0-2v-8h10.8c-0.1 4-0.9 6.7-2.3 9.7-2.6-1-5.5-1.5-8.5-1.7zm0-10v-8.2c3-0.1 5.8-0.7 8.5-1.6 1.4 3 2.2 6.8 2.3 9.8zm9.5-12.3c-1.7-3.2-4.1-6-6.9-8.3 4.4 0.6 8.4 2.7 11.5 5.6-1.4 1.1-3 1.9-4.6 2.7zm-14.1-8.4c-2.9 2.3-5.2 5.2-6.9 8.3-1.6-0.7-3.2-1.6-4.6-2.6 3.1-2.9 7.1-5 11.5-5.7zm-12.9 7.2c1.6 1.2 3.3 2.2 5.1 3-1.5 3.3-2.3 7.5-2.4 10.5h-8.2c0.3-5 2.3-9.9 5.5-13.5zm-5.5 15.5h8.1c0.1 4 1 7.2 2.4 10.5-1.8 0.8-3.5 1.8-5.1 3-3.2-3.7-5.1-8.5-5.4-13.5zm11.4 12.2c1.7 3.2 4.1 6.1 7 8.4-4.5-0.7-8.5-2.8-11.6-5.8 1.5-1 3-1.8 4.6-2.6zm14.2 8.5c2.9-2.4 5.3-5.2 7-8.4 1.6 0.7 3.2 1.6 4.6 2.6-3.1 3-7.2 5-11.6 5.8zm13-7.3c-1.6-1.2-3.3-2.2-5.1-3 1.4-3.2 2.3-6.5 2.4-10.5h8.1c-0.3 5.1-2.2 9.9-5.4 13.5z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

+2 -2
View File
@@ -1,6 +1,6 @@
import Component from '@ember/component'; import Ember from 'ember';
export default Component.extend({ export default Ember.Component.extend({
classNames: ['loading-spinner'] classNames: ['loading-spinner']
+2 -2
View File
@@ -2,7 +2,7 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" x="0px" y="0px" viewBox="0 0 132 100" version="1.1" x="0px" y="0px" viewBox="0 0 132 100"
enable-background="new 0 0 100 100" xml:space="preserve"> enable-background="new 0 0 100 100" xml:space="preserve">
<path id="path-comet" <path id="path-comet"
d="M79.062,24.173L79.062,24.173l-0.021-0.011c-0.068-0.03-0.137-0.062-0.204-0.093L42.392,7.337 d="M79.062,24.173L79.062,24.173l-0.021-0.011c-0.068-0.03-0.137-0.062-0.204-0.093L42.392,7.337
c0,0,3.065,14.104,4.458,18.019l0,0c0,0-41.754-12.481-46.85-13.67c1.67,6.52,30.607,62.492,30.607,62.492 c0,0,3.065,14.104,4.458,18.019l0,0c0,0-41.754-12.481-46.85-13.67c1.67,6.52,30.607,62.492,30.607,62.492
c5.848,11.873,19.394,18.485,33.522,18.485c19.811,0,35.87-16.059,35.87-35.869C100,42.313,91.418,29.837,79.062,24.173z"> c5.848,11.873,19.394,18.485,33.522,18.485c19.811,0,35.87-16.059,35.87-35.869C100,42.313,91.418,29.837,79.062,24.173z">
@@ -10,5 +10,5 @@
</svg> </svg>
</p> </p>
<p> <p>
Loading data... Loading data from Ethereum...
</p> </p>
+4 -4
View File
@@ -1,6 +1,6 @@
import Component from '@ember/component'; import Ember from 'ember';
export default Component.extend({ export default Ember.Component.extend({
tagName: 'ul', tagName: 'ul',
classNames: ['proposal-list'], classNames: ['proposal-list'],
@@ -8,8 +8,8 @@ export default Component.extend({
actions: { actions: {
confirm(proposalId) { confirm(proposalId) {
if (this.contractInteractionEnabled) { if (this.get('contractInteractionEnabled')) {
this.confirmProposal(proposalId); this.sendAction('confirmAction', proposalId);
} else { } else {
window.alert('Only members can vote on proposals. Please ask someone to set you up.'); window.alert('Only members can vote on proposals. Please ask someone to set you up.');
} }
+7 -17
View File
@@ -1,20 +1,10 @@
{{#each proposals as |proposal|}} {{#each proposals as |proposal|}}
<li data-proposal-id={{proposal.id}} class={{if proposal.isExecuted "confirmed" "unconfirmed"}}> <li data-proposal-id={{proposal.id}} title="({{proposal.kind}}) {{proposal.description}}">
<p class="meta"> <span class="category {{proposal.kind}}">♥</span>
<span class="category {{proposal.kind}}">♥ ({{proposal.kind}})</span> <span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
<span class="recipient">{{proposal.contributor.name}}:</span> for <span class="recipient">{{proposal.recipientName}}</span>
</p> <span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
<p class="kredits-amount">
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span> {{#unless proposal.executed}}<button {{action "confirm" proposal.id}}>+1</button>{{/unless}}
</p>
<p class="description">
<span class="description">{{proposal.description}}</span>
</p>
<p class="voting">
{{#unless proposal.isExecuted}}
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
<button {{action "confirm" proposal.id}}>+1</button>
{{/unless}}
</p>
</li> </li>
{{/each}} {{/each}}
@@ -1,19 +0,0 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
export default Component.extend({
tagName: '',
kredits: service(),
router: service(),
actions: {
signup() {
this.router.transitionTo('signup');
}
}
});
@@ -1,11 +0,0 @@
<section id="user-account">
{{#if (and kredits.hasAccounts kredits.currentUser)}}
{{kredits.currentUser.name}}
{{#if kredits.currentUserIsCore}}
<span class="core-flag">(core)</span>
{{/if}}
{{else}}
Anonymous
<button {{action "signup"}} class="small green">Sign up</button>
{{/if}}
</section>
-30
View File
@@ -1,30 +0,0 @@
import Component from '@ember/component';
import { alias } from '@ember/object/computed';
import { computed } from '@ember/object';
const SIZES = {
'small': '128', // pixels
'medium': '256',
'large': '512'
}
export default Component.extend({
contributor: null,
tagName: 'img',
classNames: ['avatar'],
classNameBindings: ['size'],
attributeBindings: ['src', 'title'],
size: 'small',
src: alias('avatarURL'),
title: alias('contributor.name'),
avatarURL: computed('contributor.github_uid', 'size', function() {
const github_uid = this.contributor.github_uid;
if (github_uid) {
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=${SIZES[this.size]}`;
}
})
});
+2 -4
View File
@@ -1,6 +1,4 @@
import Controller from '@ember/controller'; import Ember from 'ember';
import { inject as service } from '@ember/service';
export default Controller.extend({ export default Ember.Controller.extend({
kredits: service(),
}); });
-30
View File
@@ -1,30 +0,0 @@
import Controller from '@ember/controller';
import { alias, filterBy, sort } from '@ember/object/computed';
import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
contributors: alias('kredits.contributors'),
minedContributors: filterBy('contributors', 'id'),
contributorsSorting: Object.freeze(['name:asc']),
sortedContributors: sort('minedContributors', 'contributorsSorting'),
actions: {
save (contribution) {
const contributor = this.contributors.findBy('id', contribution.contributorId);
contribution.contributorIpfsHash = contributor.ipfsHash;
return this.kredits.addContribution(contribution)
.then(contribution => {
this.transitionToRoute('index');
return contribution;
});
}
}
});
-18
View File
@@ -1,18 +0,0 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
actions: {
save (attributes) {
return this.kredits
.updateContributor(this.model.id, attributes)
.then(() => this.transitionToRoute('index'))
}
}
});
-18
View File
@@ -1,18 +0,0 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
actions: {
save (contributor) {
return this.kredits
.addContributor(contributor)
.then(() => this.transitionToRoute('index'))
}
}
});
-59
View File
@@ -1,59 +0,0 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { alias, not, sort } from '@ember/object/computed';
import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
showDetailsPane: false,
selectedContributorId: null,
selectedContributionId: null,
currentBlock: alias('kredits.currentBlock'),
contributions: alias('kredits.contributions'),
contributionsConfirmed: alias('kredits.contributionsConfirmed'),
contributionsUnconfirmed: alias('kredits.contributionsUnconfirmed'),
contributionsSorting: Object.freeze(['date:desc', 'time:desc', 'id:desc']),
contributionsUnconfirmedSorted: sort('contributionsUnconfirmed', 'contributionsSorting'),
contributionsConfirmedSorted: sort('contributionsConfirmed', 'contributionsSorting'),
kreditsByContributor: alias('kredits.kreditsByContributor'),
kreditsToplistSorting: computed('showUnconfirmedKredits', function(){
return this.showUnconfirmedKredits ? ['amountTotal:desc'] : ['amountConfirmed:desc'];
}),
kreditsToplist: sort('kreditsByContributor', 'kreditsToplistSorting'),
showUnconfirmedKredits: true,
hideUnconfirmedKredits: not('showUnconfirmedKredits'),
showQuickFilterUnconfirmed: false,
showQuickFilterConfirmed: false,
actions: {
vetoContribution (contributionId) {
this.kredits.veto(contributionId).then(transaction => {
console.debug('[controllers:index] Veto submitted to Ethereum blockhain: '+transaction.hash);
});
},
confirmProposal (proposalId) {
this.kredits.vote(proposalId).then(transaction => {
console.debug('[controllers:index] Vote submitted to Ethereum blockhain: '+transaction.hash);
});
},
toggleQuickFilterUnconfirmed () {
this.toggleProperty('showQuickFilterUnconfirmed');
},
toggleQuickFilterConfirmed () {
this.toggleProperty('showQuickFilterConfirmed');
}
}
});
@@ -1,12 +0,0 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
export default Controller.extend({
roleName: computed('model.isCore', 'totalKreditsEarned', function() {
if (this.model.isCore) return 'Core Contributor';
if (this.model.totalKreditsEarned <= 5000) return 'Newcomer';
return 'Contributor';
})
});
+148
View File
@@ -0,0 +1,148 @@
import Ember from 'ember';
import Proposal from 'kredits-web/models/proposal';
const {
computed,
inject: {
service
}
} = Ember;
export default Ember.Controller.extend({
kredits: service(),
contractInteractionEnabled: computed.alias('kredits.web3Provided'),
findContributorByAddress(address) {
return this.get('model.contributors')
.findBy('address', address);
},
proposalsOpen: function() {
let proposals = this.get('model.proposals')
.filterBy('executed', false)
.map(p => {
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name);
return p;
});
return proposals;
}.property('model.proposals.[]', 'model.proposals.@each.executed', 'model.contributors.[]'),
proposalsClosed: function() {
let proposals = this.get('model.proposals')
.filterBy('executed', true)
.map(p => {
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name);
return p;
});
return proposals;
}.property('model.proposals.[]', 'model.proposals.@each.executed', 'model.contributors.[]'),
proposalsSorting: ['id:desc'],
proposalsClosedSorted: Ember.computed.sort('proposalsClosed', 'proposalsSorting'),
proposalsOpenSorted: Ember.computed.sort('proposalsOpen', 'proposalsSorting'),
contributorsWithKredits: function() {
return this.get('model.contributors').filter(c => {
return c.get('kredits') !== 0;
});
}.property('model.contributors.@each.kredits'),
contributorsSorting: ['kredits:desc'],
contributorsSorted: Ember.computed.sort('contributorsWithKredits', 'contributorsSorting'),
watchContractEvents: function() {
let events = this.get('kredits.kreditsContract')
.allEvents(/* [additionalFilterObject], */);
events.watch((error, data) => {
Ember.Logger.debug('[index] Received contract event', data);
switch (data.event) {
case 'ProposalCreated':
this._handleProposalCreated(data);
break;
case 'ProposalExecuted':
this._handleProposalExecuted(data);
break;
case 'ProposalVoted':
this._handleProposalVoted(data);
break;
case 'Transfer':
this._handleTransfer(data);
break;
}
});
}.on('init'),
_handleProposalCreated(data) {
if (Ember.isPresent(this.get('model.proposals')
.findBy('id', data.args.id.toNumber()))) {
Ember.Logger.debug('[index] proposal exists, not adding from event');
return false;
}
let proposal = Proposal.create({
id: data.args.id.toNumber(),
creatorAddress: data.args.creator,
recipientAddress: data.args.recipient,
recipientName: null,
votesCount: 0,
votesNeeded: 2,
amount: data.args.amount.toNumber(),
executed: false,
url: data.args.url,
ipfsHash: data.args.ipfsHash
});
this.get('model.proposals').pushObject(proposal);
},
_handleProposalExecuted(data) {
if (this.get('model.proposals')
.findBy('id', data.args.id.toNumber())
.get('executed')) {
Ember.Logger.debug('[index] proposal already executed, not adding from event');
return false;
}
this.get('model.proposals')
.findBy('id', data.args.id.toNumber())
.setProperties({
'executed': true,
'votesCount': 2 // TODO use real count
});
this.get('model.contributors')
.findBy('address', data.args.recipient)
.incrementProperty('kredits', data.args.amount.toNumber());
},
_handleProposalVoted(data) {
this.get('model.proposals')
.findBy('id', data.args.id.toNumber())
.incrementProperty('votesCount', 1);
},
_handleTransfer(data) {
this.get('model.contributors')
.findBy('address', data.args.from)
.incrementProperty('kredits', - data.args.value.toNumber());
this.get('model.contributors')
.findBy('address', data.args.to)
.incrementProperty('kredits', data.args.value.toNumber());
},
actions: {
confirmProposal(proposalId) {
this.get('kredits').vote(proposalId).then(transactionId => {
window.confirm('Vote submitted to Ethereum blockhain: '+transactionId);
});
}
}
});
+26 -21
View File
@@ -1,24 +1,29 @@
import Controller from '@ember/controller'; import Ember from 'ember';
import { alias, filterBy } from '@ember/object/computed'; import QueryParams from 'ember-parachute';
import { inject as service } from '@ember/service';
export default Controller.extend({ export const queryParams = new QueryParams({
kredits: service(), recipient: {
defaultValue: ''
contributors: alias('kredits.contributors'), },
minedContributors: filterBy('contributors', 'id'), amount: {
defaultValue: ''
actions: { },
save(proposal) { url: {
// contributorIpfsHash is needed for the proposal ipfs data. I'm not happy to do this here but I think to load all the contributors in addProposal again is a bit too much. I hope we can refactor it later. defaultValue: ''
let contributor = this.contributors.findBy('id', proposal.contributorId); },
proposal.contributorIpfsHash = contributor.get('ipfsHash'); ipfsHash: {
defaultValue: ''
return this.kredits.addProposal(proposal)
.then((proposal) => {
this.transitionToRoute('index');
return proposal;
});
}
} }
}); });
export default Ember.Controller.extend(queryParams.Mixin, {
contributors: null,
actions: {
onSave() {
this.transitionToRoute('index');
}
}
});
-49
View File
@@ -1,49 +0,0 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import { alias, not } from '@ember/object/computed';
import { isAddress } from 'web3-utils';
import { inject as service } from '@ember/service';
import config from 'kredits-web/config/environment';
export default Controller.extend({
kredits: service(),
ethAddress: null,
githubAccessToken: alias('kredits.githubAccessToken'),
isValidEthAccount: computed('ethAddress', function() {
return isAddress(this.ethAddress);
}),
signupButtonDisabled: not('isValidEthAccount'),
actions: {
completeSignup () {
const payload = {
accessToken: this.githubAccessToken,
account: this.ethAddress
}
fetch(config.githubSignupUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
console.log('Created contributor:', data);
this.setProperties({
githubAccessToken: null,
ethAddress: null
});
this.transitionToRoute('signup.complete');
});
}
}
});
-14
View File
@@ -1,14 +0,0 @@
import Controller from '@ember/controller';
import config from 'kredits-web/config/environment';
export default Controller.extend({
actions: {
connectGithub () {
window.location = config.githubConnectUrl;
}
}
});
-7
View File
@@ -1,7 +0,0 @@
import Helper from '@ember/component/helper';
export default Helper.extend({
compute([string]) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
});
-20
View File
@@ -1,20 +0,0 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
export default Helper.extend({
kredits: service(),
currentBlock: alias('kredits.currentBlock'),
compute([contribution]) {
if (contribution.vetoed) {
return 'vetoed';
} else if (contribution.confirmedAt > this.currentBlock) {
return 'unconfirmed';
} else {
return 'confirmed'
}
}
});
-15
View File
@@ -1,15 +0,0 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
export default Helper.extend({
kredits: service(),
currentBlock: alias('kredits.currentBlock'),
compute([contribution]) {
return !contribution.vetoed &&
(contribution.confirmedAt <= this.currentBlock);
}
});
-16
View File
@@ -1,16 +0,0 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import { isPresent } from '@ember/utils';
export default Helper.extend({
kredits: service(),
currentUser: alias('kredits.currentUser'),
compute([contributor]) {
return isPresent(contributor) && isPresent(this.currentUser) &&
contributor.account === this.currentUser.account;
}
});
+2 -2
View File
@@ -9,8 +9,8 @@
{{content-for "head"}} {{content-for "head"}}
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css"> <link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/kredits-web.css"> <link rel="stylesheet" href="{{rootURL}}assets/kredits-web.css">
{{content-for "head-footer"}} {{content-for "head-footer"}}
+20
View File
@@ -0,0 +1,20 @@
/**
* This pauses the app's boot process until the window load event is fired.
* It allows the user provided Web3 instance (e.g. Metamask, Mist Browser)
* to be inserted before we try to use it.
*/
export default {
name: 'defer-loading',
initialize: function(application) {
// Load event already fired, so web3 should be loaded if available
if (window.windowLoadComplete) { return true; }
// Pause app loading
application.deferReadiness();
window.addEventListener('load', function() {
// Continue app loading
application.advanceReadiness();
});
}
};
+41 -22
View File
@@ -1,34 +1,53 @@
import EmberObject, { computed } from '@ember/object'; import Ember from 'ember';
import bignumber from 'kredits-web/utils/cps/bignumber';
export default EmberObject.extend({ export default Ember.Object.extend({
// Contract // blockNumber: null,
id: null, // blockHash: null,
// transactionHash: null,
recipientAddress: null,
contributorId: null, contributorId: null,
amount: null,
confirmedAt: bignumber('confirmedAtBlock', 'toNumber'),
vetoed: null,
ipfsHash: null, ipfsHash: null,
amount: null,
creatorAccount: null, contributor: null,
contribution: null,
// IPFS
kind: null, kind: null,
description: null, description: null,
details: null,
url: null, url: null,
date: null, details: null,
time: null,
ipfsData: '',
init () { /**
this._super(...arguments); * Loads the contribution details from IPFS and sets local instance
this.set('details', {}); * properties from it
*
* @method
* @public
*/
loadDetails(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;
}, },
iso8601Date: computed('date', 'time', function() {
return this.time ? `${this.date}T${this.time}` : this.date;
})
}); });
+110 -15
View File
@@ -1,24 +1,119 @@
import EmberObject from '@ember/object'; import Ember from 'ember';
import bignumber from 'kredits-web/utils/cps/bignumber';
import kreditsValue from 'kredits-web/utils/cps/kredits';
export default EmberObject.extend({ const {
// Contract isPresent,
id: bignumber('idRaw', 'toString'), } = Ember;
account: null,
balance: kreditsValue('balanceRaw'),
totalKreditsEarned: bignumber('totalKreditsEarnedRaw', 'toNumber'),
contributionsCount: bignumber('contributionsCountRaw', 'toNumber'),
isCore: false,
ipfsHash: null,
// IPFS export default Ember.Object.extend({
kind: null,
address: null,
name: null, name: null,
kind: null,
url: null, url: null,
github_username: null, github_username: null,
github_uid: null, github_uid: null,
wiki_username: null, wiki_username: null,
ipfsData: '' ipfsHash: null,
kredits: null,
isCore: false,
isCurrentUser: false,
avatarURL: function() {
return `https\:\/\/avatars2.githubusercontent.com/u/${this.get('github_uid')}?v=3&s=128`;
}.property('github_uid'),
/**
* 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,
});
}
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());
}
}); });
+92 -22
View File
@@ -1,32 +1,102 @@
import EmberObject from '@ember/object'; import Ember from 'ember';
import { alias } from '@ember/object/computed';
import bignumber from 'kredits-web/utils/cps/bignumber';
export default EmberObject.extend({ const {
isPresent,
isEmpty,
} = Ember;
// Contract export default Ember.Object.extend({
id: bignumber('idRaw', 'toString'),
creatorAccount: null, id: null,
contributorId: bignumber('contributorIdRaw', 'toString'), creatorAddress: null,
amount: bignumber('amountRaw', 'toNumber'), recipientAddress: null,
votesCount: bignumber('votesCountRaw', 'toNumber'), recipientName: null,
votesNeeded: bignumber('votesNeededRaw', 'toNumber'), recipientProfile: null,
votesCount: null,
votesNeeded: null,
amount: null,
executed: null, executed: null,
ipfsHash: null, contribution: null,
// Shortcuts
isExecuted: alias('executed'),
// IPFS
kind: null, kind: null,
description: null, description: null,
details: null,
url: null, url: null,
ipfsData: '', details: null,
ipfsHash: null,
init () { /**
this._super(...arguments); * Loads the contribution details from IPFS and sets local instance
this.set('details', {}); * 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());
} }
}); });
+3 -23
View File
@@ -1,36 +1,16 @@
import EmberRouter from '@ember/routing/router'; import Ember from 'ember';
import config from './config/environment'; import config from './config/environment';
const Router = EmberRouter.extend({ const Router = Ember.Router.extend({
location: config.locationType, location: config.locationType,
rootURL: config.rootURL rootURL: config.rootURL
}); });
Router.map(function() { Router.map(function() {
this.route('dashboard', function() { this.route('spinner');
this.route('contributors', function() {
this.route('show', { path: ':id' });
});
this.route('contributions', function() {
this.route('show', { path: ':id' });
});
});
this.route('proposals', function() { this.route('proposals', function() {
this.route('new'); this.route('new');
}); });
this.route('contributions', function() {
this.route('new');
});
this.route('contributors', function() {
this.route('new');
this.route('edit', { path: ':id/edit' });
});
this.route('signup', function() {
this.route('github');
this.route('eth-account');
this.route('complete');
});
}); });
export default Router; export default Router;
+2 -29
View File
@@ -1,31 +1,4 @@
import { inject as service } from '@ember/service'; import Ember from 'ember';
import Route from '@ember/routing/route';
export default Route.extend({ export default Ember.Route.extend({
kredits: service(),
beforeModel(transition) {
const kredits = this.kredits;
return kredits.setup().then(() => {
kredits.get('kredits').preflightChecks().catch((error) => {
console.error('Kredits preflight check failed!');
console.error(error);
});
if (kredits.get('accountNeedsUnlock')) {
if (confirm('It looks like you have an Ethereum wallet available. Please unlock your account.')) {
transition.retry();
}
}
}).catch((error) => {
console.log('Error initializing Kredits', error);
});
},
afterModel() {
return this.kredits.loadInitialData()
.then(() => {
this.kredits.addContractEventHandlers();
});
}
}); });
-29
View File
@@ -1,29 +0,0 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import { alias } from '@ember/object/computed';
export default Route.extend({
kredits: service(),
contributors: alias('kredits.contributors'),
model(params) {
return this.kredits.contributors.findBy('id', params.id);
},
setupController (controller, model) {
this._super(controller, model);
controller.set('attributes', {
account: model.account,
name: model.name,
kind: model.kind,
url: model.url,
github_username: model.github_username,
github_uid: model.github_uid,
gitea_username: model.gitea_username,
wiki_username: model.wiki_username
});
}
});
@@ -1,32 +0,0 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import { alias } from '@ember/object/computed';
export default Route.extend({
kredits: service(),
contributions: alias('kredits.contributions'),
model (params) {
return this.contributions.findBy('id', parseInt(params.id));
},
setupController (controller, model) {
this._super(controller, model);
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: true,
selectedContributionId: model.id
});
},
deactivate () {
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: false,
selectedContributionId: null
});
}
});
-32
View File
@@ -1,32 +0,0 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import { alias } from '@ember/object/computed';
export default Route.extend({
kredits: service(),
contributors: alias('kredits.contributors'),
model (params) {
return this.contributors.findBy('id', params.id);
},
setupController (controller, model) {
this._super(controller, model);
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: true,
selectedContributorId: model.id
});
},
deactivate () {
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: false,
selectedContributorId: null
});
}
});
+29 -4
View File
@@ -1,9 +1,34 @@
import Route from '@ember/routing/route'; import Ember from 'ember';
import Contributor from 'kredits-web/models/contributor';
export default Route.extend({ export default Ember.Route.extend({
redirect () { kredits: Ember.inject.service(),
this.transitionTo('dashboard');
beforeModel(transition) {
const kredits = this.get('kredits');
if (kredits.get('web3') && kredits.get('web3Provided')) {
kredits.get('web3').eth.getAccounts((error, accounts) => {
if (error || accounts.length === 0) {
if (confirm('It looks like you have an Ethereum wallet available. Please unlock your account.')) {
transition.retry();
}
}
});
}
},
model() {
let kredits = this.get('kredits');
return Ember.RSVP.hash({
contributors: kredits.getContributors(),
totalSupply: kredits.getValueFromContract('tokenContract', 'totalSupply'),
proposals: kredits.getProposals(),
contributions: kredits.getContributions(),
newContributor: Contributor.create({ kind: 'person' })
});
} }
}); });
+37
View File
@@ -0,0 +1,37 @@
import Ember from 'ember';
import Proposal from 'kredits-web/models/proposal';
const {
Route,
RSVP,
inject: {
service
}
} = Ember;
export default Route.extend({
kredits: service(),
model(params) {
const proposal = Proposal.create({
recipientAddress: params.recipient,
amount: params.amount,
url: params.url,
kind: params.kind || 'dev',
ipfsHash: params.ipfsHash
});
return RSVP.hash({
proposal,
contributors: this.get('kredits').getContributors()
});
},
setupController(controller, model) {
this._super(controller, model.proposal);
controller.set('contributors', model.contributors);
}
});
-4
View File
@@ -1,4 +0,0 @@
import Route from '@ember/routing/route';
export default Route.extend({
});
-4
View File
@@ -1,4 +0,0 @@
import Route from '@ember/routing/route';
export default Route.extend({
});
-17
View File
@@ -1,17 +0,0 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { isEmpty } from '@ember/utils';
export default Route.extend({
kredits: service(),
redirect () {
this._super(...arguments);
if (isEmpty(this.kredits.githubAccessToken)) {
this.transitionTo('signup.index');
}
}
});
-29
View File
@@ -1,29 +0,0 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { isEmpty } from '@ember/utils';
export default Route.extend({
kredits: service(),
redirect () {
this._super(...arguments);
let accessToken;
try {
accessToken = window.location.hash.match(/access_token=(.+)/)[1];
} catch (error) { /* ignore */ }
if (isEmpty(accessToken) || accessToken === 'undefined') {
console.error('No GitHub access token found.');
this.transitionTo('signup');
return;
}
this.kredits.set('githubAccessToken', accessToken);
this.transitionTo('signup.eth-account');
}
});
+4
View File
@@ -0,0 +1,4 @@
import Ember from 'ember';
export default Ember.Route.extend({
});
+35
View File
@@ -0,0 +1,35 @@
import Ember from 'ember';
import ipfsAPI from 'npm:ipfs-api';
import config from 'kredits-web/config/environment';
export default Ember.Service.extend({
ipfsInstance: null,
ipfs: function() {
if (this.get('ipfsInstance')) {
return this.get('ipfsInstance');
}
let ipfs = ipfsAPI(config.ipfs);
this.set('ipfsInstance', ipfs);
return ipfs;
}.property('ipfsInstance'),
storeFile(content) {
let ipfs = this.get('ipfs');
return ipfs.add(new ipfs.Buffer(content)).then(res => {
Ember.Logger.debug('[ipfs] stored content in IPFS', content, res[0].hash);
return res[0].hash;
});
},
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;
});
}
});
+236 -353
View File
@@ -1,401 +1,284 @@
import ethers from 'ethers'; import Ember from 'ember';
import Kredits from 'kredits-contracts'; import Web3 from 'npm:web3';
import RSVP from 'rsvp';
import Service from '@ember/service';
import EmberObject from '@ember/object';
import { computed } from '@ember/object';
import { alias, notEmpty } from '@ember/object/computed';
import { isEmpty } from '@ember/utils';
import groupBy from 'kredits-web/utils/group-by';
import formatKredits from 'kredits-web/utils/format-kredits';
import config from 'kredits-web/config/environment'; import config from 'kredits-web/config/environment';
import Contributor from 'kredits-web/models/contributor' import Contributor from 'kredits-web/models/contributor';
import Proposal from 'kredits-web/models/proposal' import Proposal from 'kredits-web/models/proposal';
import Contribution from 'kredits-web/models/contribution' import Contribution from 'kredits-web/models/contribution';
import kreditsContracts from 'npm:kredits-contracts';
import uuid from 'npm:uuid';
const {
Service,
isPresent,
inject: {
service
}
} = Ember;
export default Service.extend({ export default Service.extend({
currentBlock: null, ipfs: service(),
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
currentUser: null,
contributors: null,
contributions: null,
proposals: null,
githubAccessToken: null,
currentUserIsContributor: notEmpty('currentUser'), web3Instance: null,
currentUserIsCore: alias('currentUser.isCore'), web3Provided: false, // Web3 provided (using Mist Browser, Metamask et al.)
hasAccounts: notEmpty('currentUserAccounts'),
accountNeedsUnlock: computed('currentUserAccounts', function() { web3: function() {
return this.currentUserAccounts && isEmpty(this.currentUserAccounts); if (this.get('web3Instance')) {
}), return this.get('web3Instance');
}
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() { let web3Instance;
return this.contributions.filter(contribution => {
return contribution.confirmedAt > this.currentBlock;
});
}),
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() { if (typeof window.web3 !== 'undefined') {
return this.contributions Ember.Logger.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask');
.filterBy('vetoed', false) web3Instance = window.web3;
.filter(contribution => { this.set('web3Provided', true);
return contribution.confirmedAt <= this.currentBlock; } else {
}); Ember.Logger.debug('[kredits] Creating new instance from npm module class');
}), let providerUrl = localStorage.getItem('config:web3ProviderUrl') || config.web3ProviderUrl;
let provider = new Web3.providers.HttpProvider(providerUrl);
web3Instance = new Web3(provider);
}
kreditsByContributor: computed('contributionsUnconfirmed.@each.vetoed', 'contributors.[]', function() { this.set('web3Instance', web3Instance);
const contributionsUnconfirmed = this.contributionsUnconfirmed.filterBy('vetoed', false); window.web3 = web3Instance;
const contributionsGrouped = groupBy(contributionsUnconfirmed, 'contributorId');
const contributorsWithUnconfirmed = contributionsGrouped.map(c => c.value.toString());
const contributorsWithOnlyConfirmed = this.contributors.reject(c => contributorsWithUnconfirmed.includes(c.id))
const kreditsByContributor = contributionsGrouped.map(c => { return web3Instance;
const amountUnconfirmed = c.items.mapBy('amount').reduce((a, b) => a + b); }.property('web3Instance'),
const contributor = this.contributors.findBy('id', c.value.toString());
return EmberObject.create({ currentUserAccounts: function() {
contributor: contributor, return (this.get('web3Provided') && this.get('web3').eth.accounts) || [];
amountUnconfirmed: amountUnconfirmed, }.property('web3Provided', 'web3'),
amountConfirmed: contributor.totalKreditsEarned,
amountTotal: contributor.totalKreditsEarned + amountUnconfirmed
})
});
contributorsWithOnlyConfirmed.forEach(c => { initializeKreditsContract() {
kreditsByContributor.push(EmberObject.create({ let contract = null;
contributor: c,
amountUnconfirmed: 0,
amountConfirmed: c.totalKreditsEarned,
amountTotal: c.totalKreditsEarned
}));
})
return kreditsByContributor; if (isPresent(config.contractMetadata)) {
}), if (localStorage.getItem('config:networkId')) {
config.contractMetadata['networkId'] = localStorage.getItem('config:networkId');
}
contract = kreditsContracts(this.get('web3'), config.contractMetadata)['Kredits'];
} else {
contract = kreditsContracts(this.get('web3'))['Kredits'];
}
init () { return contract;
this._super(...arguments);
this.set('contributors', []);
this.set('proposals', []);
this.set('contributions', []);
}, },
// This is called in the application route's beforeModel(). So it is kreditsContract: function() {
// initialized before everything else, and we can rely on the ethProvider and if (this.get('kreditsContractInstance')) {
// the potential currentUserAccounts to be available return this.get('kreditsContractInstance');
getEthProvider () { }
let ethProvider;
return new RSVP.Promise(async (resolve) => { let contract = this.initializeKreditsContract();
function instantiateWithoutAccount () {
console.debug('[kredits] Creating new instance from npm module class');
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
resolve({
ethProvider: ethProvider,
ethSigner: null
});
}
async function instantiateWithAccount (web3Provider, context) { this.set('kreditsContractInstance', contract);
console.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask'); // window.Kredits = contract;
ethProvider = new ethers.providers.Web3Provider(web3Provider); return contract;
// const network = await ethProvider.getNetwork(); }.property('kreditsContractInstance', 'web3'),
ethProvider.listAccounts().then(accounts => {
context.set('currentUserAccounts', accounts); tokenContract: function() {
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner(); if (this.get('tokenContractInstance')) {
resolve({ return this.get('tokenContractInstance');
ethProvider, }
ethSigner
let contract = kreditsContracts(this.get('web3'), config.contractMetadata)['Token'];
this.set('tokenContractInstance', contract);
window.Token = contract;
return contract;
}.property('tokenContractInstance', 'web3'),
getValueFromContract(contract, contractMethod, ...args) {
Ember.Logger.debug('[kredits] read from contract', contract);
return new Ember.RSVP.Promise((resolve, reject) => {
this.get(contract)[contractMethod](...args, (err, data) => {
if (err) { reject(err); return; }
resolve(data);
});
});
},
getContributorData(i) {
let promise = new Ember.RSVP.Promise((resolve, reject) => {
this.getValueFromContract('kreditsContract', 'contributorAddresses', i).then(address => {
this.getValueFromContract('kreditsContract', 'contributors', address).then(person => {
this.getValueFromContract('tokenContract', 'balanceOf', address).then(balance => {
Ember.Logger.debug('[kredits] person', address, person);
let contributor = Contributor.create({
address: address,
ipfsHash: person[2],
kredits: balance.toNumber(),
isCurrentUser: this.get('currentUserAccounts').includes(address)
});
contributor.loadProfile(this.get('ipfs')).then(
() => resolve(contributor),
err => reject(err)
);
}); });
}); });
}).catch(err => reject(err));
});
return promise;
},
getContributors() {
return this.getValueFromContract('kreditsContract', 'contributorsCount').then(contributorsCount => {
let contributors = [];
for(var i = 0; i < contributorsCount.toNumber(); i++) {
contributors.push(this.getContributorData(i));
} }
if (window.ethereum) { return Ember.RSVP.all(contributors);
try {
// Request account access if needed
await window.ethereum.enable();
// Acccounts now exposed
instantiateWithAccount(window.ethereum, this);
} catch (error) {
instantiateWithoutAccount();
}
}
// Legacy dapp browsers...
else if (window.web3) {
instantiateWithAccount(window.web3.currentProvider, this);
}
// Non-dapp browsers...
else {
instantiateWithoutAccount();
}
}); });
}, },
setup () { getProposalData(i) {
return this.getEthProvider().then((providerAndSigner) => { let promise = new Ember.RSVP.Promise((resolve, reject) => {
let kredits = new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, { this.getValueFromContract('kreditsContract', 'proposals', i).then(p => {
addresses: { Kernel: config.kreditsKernelAddress }, let proposal = Proposal.create({
apm: config.kreditsApmDomain, id : i,
ipfsConfig: config.ipfs creatorAddress : p[0],
}); recipientAddress : p[1],
return kredits votesCount : p[2].toNumber(),
.init() votesNeeded : p[3].toNumber(),
.then(async (kredits) => { amount : p[4].toNumber(),
this.set('kredits', kredits); executed : p[5],
this.set('currentBlock', await kredits.provider.getBlockNumber()); url : p[6],
ipfsHash : p[7]
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
this.getCurrentUser.then((contributorData) => {
this.set('currentUser', contributorData);
});
}
return kredits;
}); });
});
},
totalSupply: computed(function() { if (proposal.get('ipfsHash')) {
return this.kredits.Token.functions.totalSupply().then(total => { proposal.loadContribution(this.get('ipfs')).then(
return formatKredits(total); () => resolve(proposal),
}) err => reject(err)
}), );
totalKreditsEarned: computed(function() {
return this.kredits.Contribution.functions.totalKreditsEarned(true)
.then(total => total.toNumber());
}),
loadInitialData () {
return this.getContributors()
.then(contributors => this.contributors.pushObjects(contributors))
.then(() => this.getContributions())
.then(contributions => this.contributions.pushObjects(contributions))
},
addContributor (attributes) {
if (attributes.github_uid) {
const uidInt = parseInt(attributes.github_uid);
attributes.github_uid = uidInt;
}
console.debug('[kredits] add contributor', attributes);
return this.kredits.Contributor.add(attributes, { gasLimit: 350000 })
.then(data => {
console.debug('[kredits] add contributor response', data);
});
},
updateContributor (id, attributes) {
if (attributes.github_uid) {
const uidInt = parseInt(attributes.github_uid);
attributes.github_uid = uidInt;
}
console.debug('[kredits] update contributor', attributes);
return this.kredits.Contributor.updateProfile(id, attributes, { gasLimit: 350000 })
.then(data => {
console.debug('[kredits] updateProfile response', data);
});
},
getContributors () {
return this.kredits.Contributor.all()
.then((contributors) => {
return contributors.map((contributor) => {
return Contributor.create(contributor);
});
});
},
addContribution (attributes) {
console.debug('[kredits] add contribution', attributes);
return this.kredits.Contribution.addContribution(attributes, { gasLimit: 300000 })
.then(data => {
console.debug('[kredits] add contribution response', data);
attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
const contribution = Contribution.create(attributes);
// TODO receive from wrapper
contribution.set('confirmedAtBlock', data.blockNumber + 40320);
this.contributions.pushObject(contribution);
return contribution;
});
},
addProposal (attributes) {
console.debug('[kredits] add proposal', attributes);
return this.kredits.Proposal.addProposal(attributes)
.then((data) => {
console.debug('[kredits] add proposal response', data);
attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
return Proposal.create(attributes);
});
},
getProposals () {
return this.kredits.Proposal.all()
.then((proposals) => {
return proposals.map((proposal) => {
proposal.contributor = this.contributors.findBy('id', proposal.contributorId.toString());
return Proposal.create(proposal);
});
});
},
getContributions () {
return this.kredits.Contribution.all({page: {size: 200}})
.then(contributions => {
return contributions.map(contribution => {
contribution.contributor = this.contributors.findBy('id', contribution.contributorId.toString());
return Contribution.create(contribution);
});
});
},
vote (proposalId) {
console.debug('[kredits] vote for', proposalId);
return this.kredits.Proposal.functions.vote(proposalId)
.then(data => {
console.debug('[kredits] vote response', data);
return data;
});
},
veto (contributionId) {
console.debug('[kredits] veto against', contributionId);
return this.kredits.Contribution.functions.veto(contributionId, { gasLimit: 300000 })
.then(data => {
console.debug('[kredits] veto response', data);
return data;
});
},
getCurrentUser: computed('kredits.provider', function() {
if (isEmpty(this.currentUserAccounts)) {
return RSVP.resolve();
}
return this.kredits.Contributor
.functions.getContributorIdByAddress(this.get('currentUserAccounts.firstObject'))
.then((id) => {
// check if the user is a contributor or not
if (id === 0) {
return RSVP.resolve();
} else { } else {
return this.kredits.Contributor.getById(id); Ember.Logger.warn('[kredits] proposal from blockchain is missing IPFS hash', proposal);
resolve(proposal);
} }
}).catch(err => reject(err));
});
return promise;
},
getProposals() {
return this.getValueFromContract('kreditsContract', 'proposalsCount').then(proposalsCount => {
let proposals = [];
for(var i = 0; i < proposalsCount.toNumber(); i++) {
proposals.push(this.getProposalData(i));
}
return Ember.RSVP.all(proposals);
});
},
vote(proposalId) {
Ember.Logger.debug('[kredits] vote for', proposalId);
return new Ember.RSVP.Promise((resolve, reject) => {
this.get('kreditsContract').vote(proposalId, (err, data) => {
if (err) { reject(err); return; }
Ember.Logger.debug('[kredits] vote response', data);
resolve(data);
}); });
}), });
findProposalById(proposalId) {
return this.proposals.findBy('id', proposalId.toString());
}, },
// Contract events addContributor(contributor) {
addContractEventHandlers () { Ember.Logger.debug('[kredits] add contributor', contributor);
this.kredits.Contributor
.on('ContributorProfileUpdated', this.handleContributorChange.bind(this))
.on('ContributorAccountUpdated', this.handleContributorChange.bind(this))
.on('ContributorAdded', this.handleContributorChange.bind(this))
this.kredits.Contribution contributor.setProperties({
.on('ContributionVetoed', this.handleContributionVetoed.bind(this)) kredits: 0,
isCurrentUser: this.get('currentUserAccounts').includes(contributor.address)
});
this.kredits.Proposal let id = uuid.v4();
.on('ProposalCreated', this.handleProposalCreated.bind(this))
.on('ProposalVoted', this.handleProposalVoted.bind(this))
.on('ProposalExecuted', this.handleProposalExecuted.bind(this));
this.kredits.Token return new Ember.RSVP.Promise((resolve, reject) => {
.on('Transfer', this.handleTransfer.bind(this)); this.get('ipfs').storeFile(contributor.serialize()).then(ipfsHash => {
}, contributor.set('ipfsHash', ipfsHash);
this.get('kreditsContract').addContributor(contributor.address, contributor.name, contributor.ipfsHash, contributor.isCore, id, (err, data) => {
async handleContributorChange (contributorId, ...args) { if (err) { reject(err); return; }
console.debug('[kredits] Contributor add/update event received for ID', contributorId); Ember.Logger.debug('[kredits] add contributor response', data);
console.debug('[kredits] Event data:', args); resolve(contributor);
const contributorData = await this.kredits.Contributor.getById(contributorId); });
const newContributor = Contributor.create(contributorData);
const oldContributor = this.contributors.findBy('id', contributorId.toString());
if (oldContributor) {
console.debug('[kredits] old contributor', oldContributor);
this.contributors.removeObject(oldContributor);
}
console.debug('[kredits] new contributor', newContributor);
this.contributors.pushObject(newContributor);
},
handleContributionVetoed (contributionId) {
console.debug('[kredits] ContributionVetoed event received for ', contributionId);
const contribution = this.contributions.findBy('id', contributionId);
console.debug('[kredits] contribution', contribution);
if (contribution) {
contribution.set('vetoed', true);
}
},
handleProposalCreated (proposalId) {
let proposal = this.findProposalById(proposalId);
if (proposal) {
console.debug('[events] proposal exists, not adding from event');
return;
}
this.kredits.Proposal.getById(proposalId)
.then((proposal) => {
proposal.contributor = this.contributors.findBy('id', proposal.contributorId.toString());
this.proposals.pushObject(Proposal.create(proposal));
}); });
});
}, },
// TODO: We may want to reload that proposal to show the voter as voted addProposal(proposal) {
handleProposalVoted (proposalId, voterId, totalVotes) { return new Ember.RSVP.Promise((resolve, reject) => {
let proposal = this.findProposalById(proposalId); const {
recipientAddress,
amount,
url
} = proposal.getProperties('recipientAddress', 'amount', 'url');
if (proposal) { this.get('ipfs').storeFile(proposal.serializeContribution()).then(ipfsHash => {
proposal.set('votesCount', totalVotes); this.get('kreditsContract').addProposal(recipientAddress, amount, url, ipfsHash, (err, data) => {
} if (err) { reject(err); return; }
Ember.Logger.debug('[kredits] add proposal response', data);
resolve();
});
});
});
}, },
handleProposalExecuted (proposalId, contributorId, amount) { getTokenMintedEvents(fromBlock=0, toBlock='latest') {
let proposal = this.findProposalById(proposalId); return new Ember.RSVP.Promise((resolve, reject) => {
const minted = this.get('tokenContract')
.Minted({}, {fromBlock: fromBlock, toBlock: toBlock});
if (proposal.get('isExecuted')) { minted.get((err, res) => {
console.debug('[events] proposal already executed, not adding from event'); if (err) {
return; Ember.Logger.error('[kredits] Error reading Token#Minted events', res);
} reject(err);
return;
proposal.set('executed', true); }
Ember.Logger.warn('[kredits] Token#Minted events', res);
this.contributors resolve(res);
.findBy('id', contributorId.toString()) });
.incrementProperty('balance', amount); });
}, },
handleTransfer (from, to, value) { getContributions() {
value = value.toNumber(); // this.getTokenMintedEvents().then(events => {
let contributions = [];
this.contributors config.fixtures.mintedEvents.forEach(mintedEvent => {
.findBy('address', from) contributions.push(this.getContributionData(mintedEvent));
.decrementProperty('balance', value); });
this.contributors return Ember.RSVP.all(contributions);
.findBy('address', to) // });
.incrementProperty('balance', value);
}, },
getContributionData(mintedEvent) {
return new Ember.RSVP.Promise((resolve, reject) => {
let contribution = Contribution.create({
recipientAddress: mintedEvent.args.recipientAddress,
contributorId: mintedEvent.args.contributorId,
amount: mintedEvent.args.amount,
ipfsHash: mintedEvent.args.reference
});
if (contribution.get('ipfsHash')) {
contribution.loadDetails(this.get('ipfs')).then(
() => resolve(contribution),
err => reject(err)
);
} else {
Ember.Logger.warn('[kredits] contribution from Minted event is missing IPFS hash', contribution);
resolve(contribution);
}
});
},
// logKreditsContract: function() {
// Ember.Logger.debug('[kredits] kreditsContract', this.get('kreditsContract'));
// }.on('init')
}); });
-73
View File
@@ -1,73 +0,0 @@
button, input[type=submit], .button {
display: inline-block;
padding: 0.6rem 2rem;
background-color: rgba(22, 21, 40, 0.6);
border: 1px solid rgba(22, 21, 40, 1);
border-radius: 3px;
color: $primary-color;
font-weight: 500;
text-decoration: none;
text-transform: uppercase;
cursor: pointer;
letter-spacing: 0.1em;
&+button, &+input[type=submit], &+.button {
margin-left: 0.5rem;
}
&:hover {
background-color: rgba(22, 21, 40, 0.8);
}
&:active, &.active {
border-color: $primary-color;
}
&[disabled] {
color: rgba(255,255,255,0.5);
}
&.small {
font-size: 0.86rem;
padding: 0.2rem 0.8rem;
}
&.danger {
color: $red;
background-color: rgba(40, 21, 21, 0.6);
border-color: rgba(40, 21, 21, 1);
&:hover {
background-color: rgba(40, 21, 21, 0.8);
}
&:active, &.active {
border-color: $red;
}
}
&.green {
color: $green;
background-color: rgba(21, 40, 21, 0.6);
border-color: rgba(21, 40, 21, 1);
&:hover {
background-color: rgba(21, 40, 21, 0.8);
}
&:active, &.active {
border-color: $green;
}
}
&.icon {
svg {
width: 2rem;
height: 2rem;
vertical-align: middle;
margin-right: 1rem;
.fg {
fill: $primary-color;
}
}
}
}
+1 -8
View File
@@ -5,11 +5,4 @@ $green: #97fb68;
$yellow: #fbe468; $yellow: #fbe468;
$red: #fb6868; $red: #fb6868;
$primary-color: $blue; $primaryColor: $blue;
$body-text-color: #fff;
#topbar {
background-color: rgba(255, 0, 255, .2);
}
+18 -107
View File
@@ -1,120 +1,31 @@
main { section {
padding: 1rem 2rem; @include outer-container;
margin-bottom: 8rem;
@include media-max(small) {
padding: 1rem;
}
&#dashboard {
width: 100%;
display: grid;
grid-row-gap: 2rem;
grid-template-areas:
"stats"
"contributions";
}
&.center-column {
display: flex;
flex-direction: column;
align-items: center;
section {
width: 600px;
max-width: 100%;
header {
text-align: center;
}
}
}
section {
.content {
a {
font-size: inherit;
}
&.text-lg {
p {
font-size: 1.2rem;
margin-bottom: 1em;
line-height: 1.5em;
}
}
&.text-center {
text-align: center;
}
p {
&.mg-bottom-md {
margin-bottom: 2rem;
}
&.actions {
text-align: center;
padding-top: 2rem;
}
}
}
}
}
@media (min-width: 550px) {
main {
&#dashboard {
grid-column-gap: 4rem;
grid-row-gap: 2rem;
grid-template-columns: 2fr 4fr;
grid-template-areas:
"stats contributions";
&.with-details {
grid-column-gap: 3rem;
grid-template-columns: 2fr 4fr 2fr;
grid-template-areas:
"stats contributions details";
}
}
}
}
main section {
margin-bottom: 5rem;
&:first-of-type { &:first-of-type {
margin-top: 2rem; margin-top: 6rem;
} }
@include media-max(small) { @include media($mobile) {
margin-bottom: 5rem; margin-bottom: 5rem;
&:first-of-type {
margin-top: 2rem;
}
} }
header { header {
margin-bottom: 3rem; @include span-columns(12);
padding-bottom: 3rem;
text-align: center;
// background-color: purple;
&.with-nav { @include media($mobile) {
display: grid; @include span-columns(10);
grid-template-columns: auto 5rem; @include shift(1);
grid-template-areas: padding-bottom: 2rem;
"title" "actions";
} }
h2 { div.content {
display: inline-block; @include span-columns(12);
// padding-left: 1.2rem;
}
nav {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
@include media-max(small) {
margin-bottom: 2rem;
} }
} }
} }
-33
View File
@@ -1,33 +0,0 @@
$breakpoints-max: (
small: 600px,
medium: 960px,
large: 1280px
);
$breakpoints-min: (
small: 601px,
medium: 961px,
large: 1281px
);
@mixin media-max($screen-size) {
@if map-has-key($breakpoints-max, $screen-size) {
@media (max-width: map-get($breakpoints-max, $screen-size)) {
@content;
}
} @else {
// Debugging
@warn "'#{$screen-size}' has not been declared as a breakpoint."
}
}
@mixin media-min($screen-size) {
@if map-has-key($breakpoints-min, $screen-size) {
@media (min-width: map-get($breakpoints-min, $screen-size)) {
@content;
}
} @else {
// Debugging
@warn "'#{$screen-size}' has not been declared as a breakpoint."
}
}
+38 -48
View File
@@ -10,23 +10,23 @@
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
} }
@import "mediaqueries"; @import "bourbon";
@import "settings/neat";
@import "neat";
@import "settings/breakpoints";
@import "layout"; @import "layout";
@import "colors"; @import "colors";
$font-family-sans: 'Open Sans', sans-serif; $font-family-sans: 'Open Sans', sans-serif;
html, body {
font-family: $font-family-sans;
font-weight: 300;
font-size: 12px;
color: $body-text-color;
}
body { body {
background-image: linear-gradient(to bottom, rgba(22, 21, 40, .4), rgba(0, 0, 0, .75)), url('/img/bg.jpg'); background-image: url('/img/bg.jpg');
background-repeat: none; background-repeat: none;
background-attachment: fixed; background-attachment: fixed;
font-family: $font-family-sans;
font-size: 14px;
font-weight: 300;
color: #fff;
} }
@media screen and (min-width: 900px) { @media screen and (min-width: 900px) {
@@ -41,38 +41,28 @@ h1, h2, h3, h4, h5, input, button {
font-weight: 300; font-weight: 300;
} }
a {
color: $primary-color;
}
section { section {
h2 { h2 {
font-size: 1.4rem; font-size: 2.8rem;
color: $primary-color; color: $primaryColor;
@include media($mobile) {
font-size: 2rem;
}
} }
&#people { &#contributors {
.content { .content {
p.stats { p.stats {
margin-bottom: 1rem; padding-top: 3rem;
font-size: 1rem; font-size: 1rem;
color: white; color: white;
text-align: center; text-align: center;
span.number { span.number {
font-weight: 600; font-weight: 600;
} }
} @include media($mobile) {
} padding-top: 2rem;
} }
&#contributions-by-type {
.chart {
width: 50%;
margin-left: auto;
margin-right: auto;
@include media-max(small) {
width: 75%;
} }
} }
} }
@@ -81,37 +71,37 @@ section {
.actions { .actions {
padding-top: 3rem; padding-top: 3rem;
font-size: 1rem; font-size: 1rem;
color: $primary-color; color: $primaryColor;
text-align: center; text-align: center;
@include media-max(small) { @include media($mobile) {
padding-top: 2rem; padding-top: 2rem;
} }
a { a {
color: $primary-color; color: $primaryColor;
} }
} }
} }
} }
@media (min-width: 550px) { button, input[type=submit] {
section { display: inline-block;
h2 { border: 1px solid rgba(22, 21, 40, 1);
font-size: 2rem; 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 "buttons";
@import "forms";
@import "components/contribution-list";
@import "components/contribution-details";
@import "components/contributor-list";
@import "components/contributor-profile";
@import "components/external-account-link";
@import "components/loading-spinner"; @import "components/loading-spinner";
@import "components/contributor-list";
@import "components/add-contributor";
@import "components/proposal-list"; @import "components/proposal-list";
@import "components/topbar"; @import "components/contribution-list";
@import "components/topbar-account-panel";
@import "components/user-avatar";
@@ -1,37 +1,20 @@
section#add-contributor, section#add-contributor, section#add-proposal {
section#add-contribution,
section#add-proposal,
section#signup {
form { form {
p { p {
margin-bottom: 1.5rem; margin-bottom: 1rem;
&.mg-bottom-md {
margin-bottom: 2rem;
}
&.label {
margin-bottom: .5rem;
}
&.actions { &.actions {
padding-top: 1.5rem; padding-top: 1rem;
text-align: center; text-align: center;
a { a {
color: $primary-color; color: $primaryColor;
margin-left: 1rem; margin-left: 1rem;
} }
} }
} }
label {
display: block;
margin-bottom: 0.5rem;
opacity: 0.7;
}
input[type=text], select { input[type=text], select {
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
@@ -40,22 +23,17 @@ section#signup {
background-color: rgba(22, 21, 40, 0.3); background-color: rgba(22, 21, 40, 0.3);
color: #fff; color: #fff;
font-size: 1.2rem; font-size: 1.2rem;
transition: border-color 0.1s linear;
&:focus, &.valid { &:focus, &.valid {
background-color: rgba(22, 21, 40, 0.6); background-color: rgba(22, 21, 40, 0.6);
} }
&:focus { @include placeholder {
border-color: $blue;
}
&::placeholder {
color: rgba(238, 238, 238, 0.5); color: rgba(238, 238, 238, 0.5);
} }
} }
select { select {
-moz-appearance: none;
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none;
appearance: none; appearance: none;
border-radius: 0; border-radius: 0;
background-color: rgba(22, 21, 40, 0.6); background-color: rgba(22, 21, 40, 0.6);
@@ -1,57 +0,0 @@
section#contribution-details {
header {
nav {
.amount {
font-size: 1.5rem;
}
.symbol {
padding-left: 0.2rem;
}
}
}
.content {
width: 100%;
margin: 0 0 1.5rem;
padding: 2rem 2rem;
border-top: 1px solid rgba(255,255,255,0.2);
border-bottom: 1px solid rgba(255,255,255,0.2);
background-color: rgba(255,255,255,0.1);
h3 {
font-size: 1.5rem;
margin-bottom: 2rem;
}
p {
margin-bottom: 2rem;
span {
}
&.who-what-when {
font-size: 1.2rem;
margin-bottom: 1.2rem;
}
&:last-of-type {
margin-bottom: 0;
}
}
a {
color: $primary-color;
text-decoration: none;
&:not(.button):hover {
text-decoration: underline;
}
}
}
.actions {
text-align: center;
}
}
+6 -118
View File
@@ -1,89 +1,19 @@
main section {
@include media-max(small) {
&#contributions-unconfirmed, &#contributions-confirmed {
margin-top: 0;
}
}
}
.quick-filter {
font-size: 1.2rem;
margin-bottom: 2rem;
p {
padding: 0.2rem 0 0;
}
label {
&+ label {
margin-left: 3.6rem;
}
}
}
ul.contribution-list { ul.contribution-list {
clear: both; clear: both;
width: 100%; width: 100%;
list-style: none; list-style: none;
li { li {
display: grid; // display: block;
grid-template-columns: auto 5rem 5rem; padding: 0 1.2rem;
grid-row-gap: 0.5rem; line-height: 4.2rem;
padding: 0.8rem 1.2rem;
background-color: rgba(255,255,255,0.1); background-color: rgba(255,255,255,0.1);
font-size: 1.2rem; font-size: 1.4rem;
border-bottom: 1px solid rgba(255,255,255,0.2); border-bottom: 1px solid rgba(255,255,255,0.2);
cursor: pointer;
&:first-of-type { &:first-of-type {
border-top: 1px solid rgba(255,255,255,0.2); border-top: 1px solid rgba(255,255,255,0.2);
} }
&.confirmed {
grid-template-columns: auto 5rem;
}
&.vetoed {
grid-template-columns: auto 5rem;
text-decoration: line-through;
opacity: 0.6;
}
&.selected {
background-color: rgba(255,255,255,0.2);
}
p {
align-self: center;
margin: 0;
line-height: 2rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.kredits-amount, &.voting {
text-align: right;
}
}
span {
&.title {
a {
text-decoration: none;
color: $body-text-color;
}
a:hover, a:active {
color: $primary-color;
}
}
}
.description {
line-height: 1.4em;
font-size: 1rem;
}
.category { .category {
color: $blue; color: $blue;
padding-right: 0.2rem; padding-right: 0.2rem;
@@ -93,59 +23,17 @@ ul.contribution-list {
&.docs { color: $green; } &.docs { color: $green; }
&.ops { color: $purple; } &.ops { color: $purple; }
} }
.amount { .amount {
font-weight: 500; font-weight: 500;
} }
.symbol { .symbol {
font-size: 0.8rem; font-size: 1rem;
font-weight: 500;
padding-left: 0.2rem; padding-left: 0.2rem;
} }
.recipient { .recipient {
font-weight: 500; font-weight: 500;
} }
.votes {
font-size: 1rem;
color: $primary-color;
margin-right: 0.5rem;
}
} }
} }
@media (min-width: 550px) {
ul.proposal-list {
li {
grid-template-columns: auto 10rem;
grid-row-gap: 0.5rem;
p {
&.kredits-amount, &.voting {
text-align: right;
}
}
&.unconfirmed {
p {
&.kredits-amount, &.voting {
text-align: right;
}
&.description {
grid-column-start: span 1;
}
&.voting {
grid-column-start: span 1;
}
}
}
.description {
}
}
}
}
+46 -15
View File
@@ -1,48 +1,79 @@
table.contributor-list { table.contributor-list {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 1.5rem;
tr { tr {
background-color: rgba(255,255,255,0.1);
border-bottom: 1px solid rgba(255,255,255,0.2); border-bottom: 1px solid rgba(255,255,255,0.2);
cursor: pointer;
&:first-of-type { &:first-of-type {
border-top: 1px solid rgba(255,255,255,0.2); border-top: 1px solid rgba(255,255,255,0.2);
} }
background-color: rgba(255,255,255,0.1);
&.selected { &.current-user {
background-color: rgba(255,255,255,0.2); background-color: rgba(255,255,255,0.2);
} }
&.current-user { &.metadata {
font-weight: 500; height: 0;
transition: all 0.2s ease-out;
td {
padding: 0 1.2rem;
}
a {
color: $primaryColor;
&:hover, &:active {
color: #fff;
}
}
ul {
list-style: none;
display: block;
overflow: hidden;
height: 0;
transition: all 0.2s ease-out;
li {
display: inline;
&+li {
margin-left: 1rem;
}
}
}
&.visible {
height: auto;
ul {
height: auto;
}
}
} }
td { td {
padding: 0 1.2rem; padding: 0 1.2rem;
line-height: 3.6rem; line-height: 4.2rem;
&.person { &.person {
text-align: left; text-align: left;
font-size: 1.2rem; font-size: 1.4rem;
img.avatar { img.avatar {
width: 2rem;
height: 2rem;
vertical-align: middle;
margin-right: 0.2rem; margin-right: 0.2rem;
border-radius: 1rem;
} }
} }
&.kredits { &.kredits {
text-align: right; text-align: right;
.amount { .amount {
font-size: 1.2rem; font-size: 1.4rem;
font-weight: 500;
} }
.symbol { .symbol {
font-size: 0.8rem; font-size: 1rem;
padding-left: 0.2rem; padding-left: 0.2rem;
} }
} }
@@ -1,52 +0,0 @@
section#contributor-profile {
text-align: center;
header {
z-index: 2;
width: 100%;
text-align: center;
img {
margin: 0 auto;
border: 3px solid rgba(255,255,255,0.2);
}
}
.content {
z-index: 1;
width: 100%;
margin: -7.2rem 0 1.5rem;
padding: 6rem 1.2rem 2rem;
border-top: 1px solid rgba(255,255,255,0.2);
border-bottom: 1px solid rgba(255,255,255,0.2);
background-color: rgba(255,255,255,0.1);
h2 {
text-align: center;
margin-bottom: 0.6rem;
}
p.role {
margin-bottom: 2rem;
}
ul.external-accounts {
list-style: none;
li {
display: inline-block;
&+li {
margin-left: 0.6rem;
}
}
}
}
.actions {
.button {
margin-bottom: 0.6rem;
}
}
}
@@ -1,34 +0,0 @@
.external-accounts a {
display: inline-block;
width: 4rem;
height: 4rem;
padding: 0.8rem;
background-color: rgba(0, 0, 0, 0.3);
color: $body-text-color;
border-radius: 2rem;
span.site {
display: none;
}
svg {
display: block;
position: relative;
width: 100%;
height: 100%;
.fg {
fill: $body-text-color;
}
}
&:hover, &:active {
background-color: rgba(0, 0, 0, 0.5);
svg {
.fg {
fill: $primary-color;
}
}
}
}
+3 -3
View File
@@ -11,9 +11,9 @@
margin-top: 12rem; margin-top: 12rem;
text-align: center; text-align: center;
font-size: 1.4rem; font-size: 1.4rem;
color: $primary-color; color: $primaryColor;
@include media-max(small) { @include media($mobile) {
margin-top: 6rem; margin-top: 6rem;
} }
@@ -23,7 +23,7 @@
margin-bottom: 2rem; margin-bottom: 2rem;
#path-comet { #path-comet {
fill: $primary-color; fill: $primaryColor;
opacity: 0.1; opacity: 0.1;
animation-name: pulse; animation-name: pulse;
+14 -73
View File
@@ -4,44 +4,16 @@ ul.proposal-list {
list-style: none; list-style: none;
li { li {
display: grid; // display: block;
grid-template-columns: auto 5rem; padding: 0 1.2rem;
grid-row-gap: 0.5rem; line-height: 4.2rem;
padding: 1rem 1.2rem;
background-color: rgba(255,255,255,0.1); background-color: rgba(255,255,255,0.1);
font-size: 1.2rem; font-size: 1.4rem;
border-bottom: 1px solid rgba(255,255,255,0.2); border-bottom: 1px solid rgba(255,255,255,0.2);
&:first-of-type { &:first-of-type {
border-top: 1px solid rgba(255,255,255,0.2); border-top: 1px solid rgba(255,255,255,0.2);
} }
p {
align-self: center;
margin: 0;
line-height: 2rem;
&.kredits-amount, &.voting {
text-align: right;
}
&.description {
grid-column-start: span 2;
}
&.voting {
grid-column-start: span 2;
}
}
span {
}
.description {
line-height: 1.4em;
font-size: 1rem;
}
.category { .category {
color: $blue; color: $blue;
padding-right: 0.2rem; padding-right: 0.2rem;
@@ -51,61 +23,30 @@ ul.proposal-list {
&.docs { color: $green; } &.docs { color: $green; }
&.ops { color: $purple; } &.ops { color: $purple; }
} }
.amount { .amount {
font-weight: 500; font-weight: 500;
} }
.symbol { .symbol {
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
padding-left: 0.2rem; padding-left: 0.2rem;
} }
.recipient { .recipient {
font-weight: 500; font-weight: 500;
} }
.votes { .votes {
font-size: 1rem; font-size: 1rem;
color: $primary-color; color: $primaryColor;
margin-right: 0.5rem; padding-left: 0.5rem;
}
button {
float: right;
margin-top: 1.1rem;
height: 2rem;
line-height: 2rem;
padding: 0 0.6rem;
} }
} }
} }
@media (min-width: 550px) {
ul.proposal-list {
li {
grid-template-columns: auto 10rem;
grid-row-gap: 0.5rem;
p {
&.kredits-amount, &.voting {
text-align: right;
}
}
&.unconfirmed {
p {
&.kredits-amount, &.voting {
text-align: right;
}
&.description {
grid-column-start: span 1;
}
&.voting {
grid-column-start: span 1;
}
}
}
.description {
}
}
}
}
@@ -1,7 +0,0 @@
header#topbar section#user-account {
button {
margin-left: 1.2rem;
}
}
-27
View File
@@ -1,27 +0,0 @@
#topbar {
padding: 0 1rem;
height: 3rem;
line-height: 3rem;
background-color: rgba(0,0,0,.3);
@include media-min(medium) {
padding: 0 2rem;
height: 4rem;
line-height: 4rem;
}
h1 {
display: inline-block;
text-transform: uppercase;
a {
color: inherit;
text-decoration: none;
}
}
section#user-account {
display: inline-block;
float: right;
}
}
-21
View File
@@ -1,21 +0,0 @@
img.avatar {
width: 2rem;
height: 2rem;
vertical-align: middle;
margin-right: 0.2rem;
border-radius: 1rem;
&.medium {
margin: 0;
width: 10rem;
height: 10rem;
border-radius: 5rem;
}
&.large {
margin: 0;
width: 256px;
height: 256px;
border-radius: 128px;
}
}
+1
View File
@@ -0,0 +1 @@
$mobile: new-breakpoint(max-width 600px);
+5
View File
@@ -0,0 +1,5 @@
// $visual-grid: true;
// $visual-grid-color: red;
// $visual-grid-index: front;
// $visual-grid-opacity: 0.5;
$max-width: em(500, 12);
+3 -6
View File
@@ -1,6 +1,3 @@
<header id="topbar"> <main>
<h1>{{link-to "Kosmos Kredits" "dashboard"}}</h1> {{outlet}}
{{topbar-account-panel}} </main>
</header>
{{outlet}}
-13
View File
@@ -1,13 +0,0 @@
<main class="center-column">
<section id="add-contribution">
<header>
<h2>Submit a contribution</h2>
</header>
<div class="content">
{{add-contribution contributors=sortedContributors save=(action "save")}}
</div>
</section>
</main>
-13
View File
@@ -1,13 +0,0 @@
<main class="center-column">
<section id="add-contributor">
<header>
<h2>Update contributor profile</h2>
</header>
<div class="content">
{{add-contributor attributes=attributes save=(action "save")}}
</div>
</section>
</main>
-13
View File
@@ -1,13 +0,0 @@
<main class="center-column">
<section id="add-contributor">
<header>
<h2>Add contributor profile</h2>
</header>
<div class="content">
{{add-contributor save=(action "save")}}
</div>
</section>
</main>
-82
View File
@@ -1,82 +0,0 @@
<main id="dashboard" class={{if showDetailsPane "with-details"}}>
<div id="stats">
<section id="people">
<header class="with-nav">
<h2>Contributors</h2>
{{#if kredits.hasAccounts}}
<nav>
{{link-to "add" "contributors.new" title="Add contributor profile" class="button small green"}}
</nav>
{{/if}}
</header>
<div class="content">
{{contributor-list contributorList=kreditsToplist
showUnconfirmedKredits=showUnconfirmedKredits
selectedContributorId=selectedContributorId}}
<p class="stats">
<span class="number">{{await kredits.totalKreditsEarned}}</span> kredits confirmed and issued to
<span class="number">{{contributorsWithKredits.length}}</span> contributors
</p>
<p class="stats">
{{input type="checkbox" id="hide-unnconfirmed-kredits" checked=showUnconfirmedKredits}}
<label for="hide-unnconfirmed-kredits">Show unconfirmed kredits in toplist</label>
</p>
</div>
</section>
<section id="contributions-by-type">
<header>
<h2>Contributions by type</h2>
</header>
<div class="content">
{{chart-contributions-by-type contributions=contributions}}
</div>
</section>
</div>
<div id="contributions">
{{#if contributionsUnconfirmed}}
<section id="contributions-unconfirmed">
<header class="with-nav">
<h2>Latest Contributions</h2>
{{#if kredits.hasAccounts}}
<nav>
<button {{action "toggleQuickFilterUnconfirmed"}} class="small {{if showQuickFilterUnconfirmed "active"}}">filter</button>
{{link-to "add" "contributions.new" title="Submit a contribution" class="button small green"}}
</nav>
{{/if}}
</header>
<div class="content">
{{!-- TODO: We need a better naming for kredits.hasAccounts --}}
{{contribution-list contributions=contributionsUnconfirmedSorted
vetoContribution=(action "vetoContribution")
contractInteractionEnabled=kredits.hasAccounts
selectedContributionId=selectedContributionId
showQuickFilter=showQuickFilterUnconfirmed}}
</div>
</section>
{{/if}}
<section id="contributions-confirmed">
<header class="with-nav">
<h2>Confirmed Contributions</h2>
<nav>
<button {{action "toggleQuickFilterConfirmed"}} class="small {{if showQuickFilterConfirmed "active"}}">filter</button>
</nav>
</header>
<div class="content">
{{contribution-list contributions=contributionsConfirmedSorted
vetoContribution=(action "vetoContribution")
selectedContributionId=selectedContributionId
showQuickFilter=showQuickFilterConfirmed}}
</div>
</section>
</div>
<div id="details">
{{outlet}}
</div>
</main>
@@ -1,47 +0,0 @@
<section id="contribution-details">
<header class="with-nav">
<h2>Contribution #{{model.id}}</h2>
<nav>
<span class="amount">{{model.amount}}</span>&nbsp;
<span class="symbol">₭S</span>
</nav>
</header>
<div class="content">
<p class="who-what-when">
<span class="contributor">
{{user-avatar contributor=model.contributor}}
{{link-to model.contributor.name "dashboard.contributors.show" model.contributor}}
</span>
contributed
<span class="date" title={{model.iso8601Date}}>{{moment-from model.iso8601Date}}</span>:
</p>
<h3>{{model.description}}</h3>
<p>
Kind: {{model.kind}}
<br>Status: {{contribution-status model}}
</p>
{{#if model.url}}
<p>
<a href={{model.url}}
title={{model.description}}
class="button"
target="_blank"
rel="noopener">
Open URL
</a>
</p>
{{/if}}
</div>
<div class="actions">
<p>
{{#if model.ipfsHash}}
<a href="https://ipfs.io/ipfs/{{model.ipfsHash}}"
class="button small" target="_blank" rel="noopener">
Inspect IPFS data
</a>
{{/if}}
</p>
</div>
</section>
@@ -1,36 +0,0 @@
<section id="contributor-profile">
<header>
{{user-avatar contributor=model size="medium"}}
</header>
<div class="content">
<h2>{{model.name}}</h2>
<p class="role">{{roleName}}</p>
<ul class="external-accounts">
{{#each model.accounts as |account|}}
<li>
{{external-account-link account=account}}
</li>
{{/each}}
{{#if model.url}}
<li>
<a href={{model.url}} target="_blank" rel="noopener" title="Website">
<span class="site">Web</span>
{{icon-web-globe}}
</a>
</li>
{{/if}}
</ul>
</div>
<div class="actions">
<p>
<a href="https://rinkeby.etherscan.io/address/{{model.account}}" class="button small" target="_blank" rel="noopener">Inspect Ethereum transactions</a>
{{#if model.ipfsHash}}
<a href="https://ipfs.io/ipfs/{{model.ipfsHash}}" class="button small" target="_blank" rel="noopener">Inspect IPFS profile</a>
{{/if}}
{{link-to "Edit profile" "contributors.edit" model class="button small"}}
</p>
</div>
</section>
+55
View File
@@ -0,0 +1,55 @@
<section id="contributors">
<header>
<h2>Kosmos Contributors</h2>
</header>
<div class="content">
{{contributor-list contributors=contributorsSorted}}
<p class="stats">
<span class="number">{{model.totalSupply}}</span> kredits issued and distributed among
<span class="number">{{contributorsWithKredits.length}}</span> contributors.
</p>
</div>
</section>
{{#if proposalsOpen}}
<section id="proposals-open">
<header>
<h2>Open Proposals</h2>
</header>
<div class="content">
{{proposal-list proposals=proposalsOpenSorted
confirmAction="confirmProposal"
contractInteractionEnabled=contractInteractionEnabled}}
{{#if contractInteractionEnabled}}
<p class="actions">
{{#link-to 'proposals.new'}}Create new proposal{{/link-to}}
</p>
{{/if}}
</div>
</section>
{{/if}}
<section id="contributions">
<header>
<h2>Contributions</h2>
</header>
<div class="content">
{{contribution-list contributions=model.contributions
contributors=model.contributors}}
</div>
</section>
<section id="add-contributor">
<header>
<h2>Add Contributor</h2>
</header>
<div class="content">
{{add-contributor contributors=model.contributors
newContributor=model.newContributor
contractInteractionEnabled=contractInteractionEnabled}}
</div>
</section>

Some files were not shown because too many files have changed in this diff Show More