Compare commits

..

1 Commits

Author SHA1 Message Date
basti 574342ae7f 1.1.1 2019-06-27 15:09:12 +02:00
148 changed files with 27601 additions and 36900 deletions
+1
View File
@@ -4,6 +4,7 @@
root = true root = true
[*] [*]
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
+6 -22
View File
@@ -1,14 +1,8 @@
'use strict';
module.exports = { module.exports = {
root: true, root: true,
parser: 'babel-eslint',
parserOptions: { parserOptions: {
ecmaVersion: 2018, ecmaVersion: 2017,
sourceType: 'module', sourceType: 'module'
ecmaFeatures: {
legacyDecorators: true
}
}, },
globals: { globals: {
console: true console: true
@@ -25,10 +19,7 @@ module.exports = {
}, },
rules: { rules: {
'ember/avoid-leaking-state-in-ember-objects': 'warn', 'ember/avoid-leaking-state-in-ember-objects': 'warn',
'no-console': 'off', 'no-console': 'off'
'ember/no-jquery': 'error',
'ember/require-computed-property-dependencies': 'warn',
'ember/no-observers': 'warn'
}, },
overrides: [ overrides: [
// node files // node files
@@ -44,20 +35,13 @@ module.exports = {
'server/**/*.js' 'server/**/*.js'
], ],
parserOptions: { parserOptions: {
sourceType: 'script' sourceType: 'script',
ecmaVersion: 2015
}, },
env: { env: {
browser: false, browser: false,
node: true 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
+2 -86
View File
@@ -1,93 +1,9 @@
'use strict'; 'use strict';
module.exports = { module.exports = {
extends: 'octane', extends: 'recommended',
rules: { rules: {
'simple-unless': false, 'simple-unless': false
'no-nested-interactive': false
},
ignore: [
'kredits-web/templates/components/**',
'app/templates/components/**'
],
pending: [
{
"moduleId": "app/templates/dashboard",
"only": [
"no-action"
]
},
{
"moduleId": "app/components/add-contribution/template",
"only": [
"no-action",
"no-curly-component-invocation"
]
},
{
"moduleId": "app/components/add-contributor/template",
"only": [
"no-action",
"no-curly-component-invocation"
]
},
{
"moduleId": "app/components/contribution-list/template",
"only": [
"no-action",
"no-curly-component-invocation"
]
},
{
"moduleId": "app/components/contributor-list/template",
"only": [
"no-action"
]
},
{
"moduleId": "app/components/topbar-account-panel/template",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributions/new",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributions/resubmit",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributors/edit",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributors/new",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/signup/eth-account",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/signup/index",
"only": [
"no-action"
]
} }
]
}; };
+8 -8
View File
@@ -6,22 +6,22 @@ node_js:
sudo: false sudo: false
dist: xenial dist: xenial
notifications:
email:
on_success: never
on_failure: always
addons: addons:
chrome: stable chrome: stable
cache:
directories:
- $HOME/.npm
env: env:
global: global:
# See https://git.io/vdao3 for details. # See https://git.io/vdao3 for details.
- JOBS=1 - JOBS=1
branches: before_install:
only: - npm config set spin false
- master
script: script:
- npm run lint:hbs
- npm run lint:js
- npm test - npm test
+5 -11
View File
@@ -71,10 +71,10 @@ These are the basic steps to get up and running:
#### 1. IPFS #### 1. IPFS
Run a local IPFS deamon. Run a local IPFS deamon in offline mode.
* Make sure CORS headers are configured. See [IPFS](#ipfs) for more info. * Make sure CORS headers are configured. See [IPFS](#ipfs) for more info.
* `ipfs daemon` * `ipfs daemon --offline`
#### 2. kredits-contracts #### 2. kredits-contracts
@@ -94,15 +94,10 @@ With IPFS and Ethereum/ganache running, you can now start this Ember app.
* `npm link kredits-contracts` - link the local `kredits-contracts` package * `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 * `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 you restart the devchain your network ID changes. So kredits-contracts does not find the correct DAO Kernel address.
If that is the case the DAO address must be provided as `KREDITS_DAO_ADDRESS` environment variable. If that is the case the DAO address must be provided as `KREDITS_KERNEL_ADDRESS` environment variable.
* `npm run dao:address` in the kredits-contracts repo to find your address * `KREDITS_KERNEL_ADDRESS=<0xYOURADDRESS> npm run start:local` - find your address with the `npm run dao:address` command in the kredits-contracts repo.
* `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 #### IPFS
@@ -112,7 +107,6 @@ Install IPFS with your favorite package manager and run
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["localhost:4200"]' 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-Methods '["PUT", "GET", "POST"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]' 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
+8 -6
View File
@@ -1,12 +1,14 @@
import Application from '@ember/application'; import Application from '@ember/application';
import Resolver from 'ember-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';
export default class App extends Application { const App = Application.extend({
modulePrefix = config.modulePrefix; modulePrefix: config.modulePrefix,
podModulePrefix = config.podModulePrefix; podModulePrefix: config.podModulePrefix,
Resolver = Resolver; Resolver
} });
loadInitializers(App, config.modulePrefix); loadInitializers(App, config.modulePrefix);
export default App;
+9 -17
View File
@@ -1,8 +1,6 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed'; import { and, notEmpty } from '@ember/object/computed';
import { assign } from '@ember/polyfills';
import moment from 'moment';
export default Component.extend({ export default Component.extend({
@@ -23,19 +21,18 @@ export default Component.extend({
init () { init () {
this._super(...arguments); this._super(...arguments);
this.set('defaultDate', moment().startOf('hour').toDate()); this.set('defaultDate', new Date());
this.set('defaultAttr', {
// Default attributes used by reset
this.set('attributes', {
contributorId: null, contributorId: null,
kind: null, kind: null,
date: this.defaultDate, date: [new Date()],
amount: null, amount: null,
description: null, description: null,
url: null, url: null,
details: null
}); });
this.set('attributes', assign({}, this.defaultAttr, this.attributes));
this.reset(); this.reset();
}, },
@@ -45,21 +42,15 @@ export default Component.extend({
actions: { actions: {
submit (evt) { submit () {
evt.preventDefault();
if (!this.isValid) { if (!this.isValid) {
alert('Invalid data. Please review and try again.'); alert('Invalid data. Please review and try again.');
return; return;
} }
const attributes = this.getProperties(Object.keys(this.attributes)); const attributes = this.getProperties(Object.keys(this.attributes));
const [ date/* , time */ ] = attributes.date[0].toISOString().split('T');
let dateInput = (attributes.date instanceof Array) ? attributes.date = date;
attributes.date[0] : attributes.date;
const [ date, time ] = dateInput.toISOString().split('T');
[ attributes.date, attributes.time ] = [ date, time ];
this.set('inProgress', true); this.set('inProgress', true);
@@ -71,6 +62,7 @@ export default Component.extend({
window.alert('Something went wrong. Check the browser console for details.'); window.alert('Something went wrong. Check the browser console for details.');
}) })
.finally(() => this.set('inProgress', false)); .finally(() => this.set('inProgress', false));
} }
} }
+32 -60
View File
@@ -1,86 +1,58 @@
<form onsubmit={{action "submit"}}> <form {{action "submit" on="submit"}}>
<label>
<p class="label">Contributor:</p>
<p> <p>
<select required onchange={{action (mut this.contributorId) value="target.value"}}> <select required onchange={{action (mut contributorId) value="target.value"}}>
<option value="" selected disabled hidden></option> <option value="" selected disabled hidden>Contributor</option>
{{#each @contributors as |contributor|}} {{#each contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option> <option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}} {{/each}}
</select> </select>
</p> </p>
</label>
<label>
<p class="label">Kind:</p>
<p> <p>
<select required onchange={{action (mut this.kind) value="target.value"}}> <select required onchange={{action (mut kind) value="target.value"}}>
<option value="" selected disabled hidden></option> <option value="" selected disabled hidden>Kind</option>
<option value="bureaucracy" selected={{eq this.kind "bureaucracy"}}>Bureaucracy</option> <option value="community" selected={{eq kind "community"}}>Community</option>
<option value="community" selected={{eq this.kind "community"}}>Community</option> <option value="design" selected={{eq kind "design"}}>Design</option>
<option value="design" selected={{eq this.kind "design"}}>Design</option> <option value="dev" selected={{eq kind "dev"}}>Development</option>
<option value="dev" selected={{eq this.kind "dev"}}>Development</option> <option value="docs" selected={{eq kind "docs"}}>Documentation</option>
<option value="docs" selected={{eq this.kind "docs"}}>Documentation</option> <option value="ops" selected={{eq kind "ops"}}>IT Operations</option>
<option value="ops" selected={{eq this.kind "ops"}}>IT Operations</option> <option value="special" selected={{eq kind "special"}}>Special</option>
<option value="outreach" selected={{eq this.kind "outreach"}}>Outreach</option>
<option value="qa" selected={{eq this.kind "qa"}}>Quality Assurance</option>
<option value="special" selected={{eq this.kind "special"}}>Special</option>
</select> </select>
</p> </p>
</label>
<label>
<p class="label">Date:</p>
<p> <p>
{{ember-flatpickr {{ember-flatpickr
date=this.date allowInput=false
defaultDate=this.defaultDate altFormat="F j, Y"
maxDate=this.defaultDate altInput=true
enableTime=true altInputClass="date-alt"
time_24hr=true date=date
onChange=(action (mut this.date)) dateFormat="Y-m-d"
defaultDate=defaultDate
maxDate=defaultDate
onChange=(action (mut date))
}} }}
</p> </p>
</label>
<label>
<p class="label">Amount:</p>
<p> <p>
{{input type="text" {{input type="text"
placeholder="500" placeholder="500"
value=this.amount value=amount
class=(if this.isValidAmount "valid" "")}} class=(if isValidAmount "valid" "")}}
</p> </p>
</label>
<label>
<p class="label">Description:</p>
<p> <p>
{{input type="text" {{input type="text"
value=this.description placeholder="Description"
class=(if this.isValidDescription "valid" "")}} value=description
class=(if isValidDescription "valid" "")}}
</p> </p>
</label>
<label>
<p class="label">URL (optional):</p>
<p> <p>
{{input type="text" {{input type="text"
value=this.url placeholder="URL (optional)"
class=(if this.isValidUrl "valid" "")}} value=url
class=(if isValidUrl "valid" "")}}
</p> </p>
</label>
{{#if this.details}}
<label>
<p class="label">Details:</p>
<p>
<pre>
{{this.details}}
</pre>
</p>
</label>
{{/if}}
<p class="actions"> <p class="actions">
{{input type="submit" {{input type="submit"
disabled=this.inProgress disabled=inProgress
value=(if this.inProgress "Processing" "Save")}} value=(if inProgress "Processing" "Save")}}
</p> </p>
</form> </form>
+5 -20
View File
@@ -1,29 +1,22 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed'; import { and, notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { isPresent } from '@ember/utils'; import { isPresent } from '@ember/utils';
import { isAddress } from 'web3-utils';
export default Component.extend({ export default Component.extend({
router: service(),
kredits: service(), kredits: service(),
attributes: null, attributes: null,
isValidAccount: computed('account', function() { // TODO: add proper address validation
return isAddress(this.account); isValidAccount: notEmpty('account'),
}),
isValidName: notEmpty('name'), isValidName: notEmpty('name'),
isValidURL: notEmpty('url'), isValidURL: notEmpty('url'),
isValidGithubUID: notEmpty('github_uid'), isValidGithubUID: notEmpty('github_uid'),
isValidGithubUsername: notEmpty('github_username'), isValidGithubUsername: notEmpty('github_username'),
isValidGiteaUsername: notEmpty('gitea_username'), isValidGiteaUsername: notEmpty('gitea_username'),
isValidWikiUsername: notEmpty('wiki_username'), isValidWikiUsername: notEmpty('wiki_username'),
isValidZoomDisplayName: notEmpty('zoom_display_name'),
isValid: and( isValid: and(
'isValidAccount', 'isValidAccount',
'isValidName', 'isValidName',
@@ -49,8 +42,7 @@ export default Component.extend({
github_username: null, github_username: null,
github_uid: null, github_uid: null,
gitea_username: null, gitea_username: null,
wiki_username: null, wiki_username: null
zoom_display_name: null
}); });
}, },
@@ -60,15 +52,9 @@ export default Component.extend({
actions: { actions: {
submit (evt) { submit () {
evt.preventDefault();
if (!this.kredits.currentUserIsCore) {
window.alert('Only core team members can edit profiles. Please ask someone to set you up.');
return;
}
if (!this.isValid) { if (!this.isValid) {
window.alert('Invalid data. Please review and try again.'); alert('Invalid data. Please review and try again.');
return; return;
} }
@@ -84,7 +70,6 @@ export default Component.extend({
window.alert('Something went wrong. Please check the browser console.'); window.alert('Something went wrong. Please check the browser console.');
}).finally(() => { }).finally(() => {
this.set('inProgress', false); this.set('inProgress', false);
this.router.transitionTo('dashboard');
}); });
} }
+21 -27
View File
@@ -1,56 +1,50 @@
<form onsubmit={{action "submit"}}> <form {{action "submit" on="submit"}}>
<p> <p>
<label for="c-account">Ethereum account</label> <label for="c-account">Ethereum account</label>
<Input @type="text" {{input name="account" id="c-account"
@value={{this.account}} type="text" value=account
name="account" id="c-account"
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
class={{if this.isValidAccount "valid" ""}} /> class=(if isValidAccount "valid" "")}}
</p> </p>
<p> <p>
<label for="c-kind">Kind</label> <label for="c-kind">Kind</label>
<select required onchange={{action (mut this.kind) value="target.value"}} id="c-kind"> <select required onchange={{action (mut kind) value="target.value"}} id="c-kind">
<option value="person" selected={{eq this.kind "person"}}>Person</option> <option value="person" selected={{eq kind "person"}}>Person</option>
<option value="organization" selected={{eq this.kind "organization"}}>Organization</option> <option value="organization" selected={{eq kind "organization"}}>Organization</option>
</select> </select>
</p> </p>
<p> <p>
<label for="c-name">Name</label> <label for="c-name">Name</label>
{{input name="name" type="text" value=this.name placeholder="Zero Cool" {{input name="name" type="text" value=name placeholder="Zero Cool"
class=(if this.isValidName "valid" "") id="c-name"}} class=(if isValidName "valid" "") id="c-name"}}
</p> </p>
<p> <p>
<label for="c-url">URL</label> <label for="c-url">URL</label>
{{input name="url" type="text" value=this.url placeholder="http://zerocool.bit" {{input name="url" type="text" value=url placeholder="http://zerocool.bit"
class=(if this.isValidURL "valid" "") id="c-url"}} class=(if isValidURL "valid" "") id="c-url"}}
</p> </p>
<p> <p>
<label for="c-github-uid">GitHub UID</label> <label for="c-github-uid">GitHub UID</label>
{{input name="github_uid" type="text" value=this.github_uid placeholder="2342" {{input name="github_uid" type="text" value=github_uid placeholder="2342"
class=(if this.isValidGithubUID "valid" "") id="c-github-uid"}} class=(if isValidGithubUID "valid" "") id="c-github-uid"}}
</p> </p>
<p> <p>
<label for="c-github-username">GitHub username</label> <label for="c-github-username">GitHub username</label>
{{input name="github_username" type="text" value=this.github_username placeholder="zerocool" {{input name="github_username" type="text" value=github_username placeholder="zerocool"
class=(if this.isValidGithubUsername "valid" "") id="c-github-username"}} class=(if isValidGithubUsername "valid" "") id="c-github-username"}}
</p> </p>
<p> <p>
<label for="c-gitea-username">Gitea username</label> <label for="c-gitea-username">Gitea username</label>
{{input name="gitea_username" type="text" value=this.gitea_username placeholder="zerocool" {{input name="gitea_username" type="text" value=gitea_username placeholder="zerocool"
class=(if this.isValidGiteaUsername "valid" "") id="c-gitea-username"}} class=(if isValidGiteaUsername "valid" "") id="c-gitea-username"}}
</p> </p>
<p> <p>
<label for="c-wiki-username">Wiki username</label> <label for="c-wiki-username">Wiki username</label>
{{input name="wiki_username" type="text" value=this.wiki_username placeholder="ZeroCool" {{input name="wiki_username" type="text" value=wiki_username placeholder="ZeroCool"
class=(if this.isValidWikiUsername "valid" "") id="c-wiki-username"}} class=(if isValidWikiUsername "valid" "") id="c-wiki-username"}}
</p>
<p>
<label for="c-zoom-display-name">Zoom display name</label>
{{input name="zoom_display_name" type="text" value=this.zoom_display_name placeholder="Zero Cool"
class=(if this.isValidZoomDisplayName "valid" "") id="c-zoom-display-name"}}
</p> </p>
<p class="actions"> <p class="actions">
{{input type="submit" disabled=this.inProgress {{input type="submit" disabled=inProgress
value=(if this.inProgress "Processing" "Save")}} value=(if inProgress "Processing" "Save")}}
</p> </p>
</form> </form>
+64
View File
@@ -0,0 +1,64 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed';
export default Component.extend({
attributes: null,
contributors: Object.freeze([]),
isValidContributor: notEmpty('contributorId'),
isValidAmount: computed('amount', function() {
return parseInt(this.amount, 10) > 0;
}),
isValidDescription: notEmpty('description'),
isValidUrl: notEmpty('url'),
isValid: and('isValidContributor',
'isValidAmount',
'isValidDescription'),
init () {
this._super(...arguments);
// Default attributes used by reset
this.set('attributes', {
contributorId: null,
kind: 'community',
amount: null,
description: null,
url: null,
});
},
didInsertElement() {
this._super(...arguments);
this.reset();
},
reset: function() {
this.setProperties(this.attributes);
},
actions: {
submit() {
if (!this.isValid) {
alert('Invalid data. Please review and try again.');
return;
}
let attributes = Object.keys(this.attributes);
let proposal = this.getProperties(attributes);
let saved = this.save(proposal);
// The promise handles inProgress
this.set('inProgress', saved);
saved.then(() => {
this.reset();
window.scroll(0,0);
window.alert('Proposal added.');
});
}
}
});
+44
View File
@@ -0,0 +1,44 @@
<form {{action "submit" on="submit"}}>
<p>
<select required onchange={{action (mut contributorId) value="target.value"}}>
<option value="" selected disabled hidden>Contributor</option>
{{#each contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.github_username}}</option>
{{/each}}
</select>
</p>
<p>
<select required onchange={{action (mut kind) value="target.value"}}>
<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>
</select>
</p>
<p>
{{input type="text"
placeholder="100"
value=amount
class=(if isValidAmount "valid" "")}}
</p>
<p>
{{input type="text"
placeholder="Description"
value=description
class=(if isValidDescription "valid" "")}}
</p>
<p>
{{input type="text"
placeholder="URL (optional)"
value=url
class=(if isValidUrl "valid" "")}}
</p>
<p class="actions">
{{input type="submit"
disabled=(is-pending inProgress)
value=(if (is-pending inProgress) "Processing" "Save")}}
{{#link-to "index"}}Back{{/link-to}}
</p>
</form>
@@ -1,6 +1,7 @@
<div class="chart"> <div class="chart">
<EmberChart @type="doughnut" {{ember-chart type="doughnut"
@data={{this.chartData}} data=chartData
@options={{this.chartOptions}} options=chartOptions
@width="200" @height="200" /> width=200
height=200}}
</div> </div>
+2 -52
View File
@@ -1,55 +1,9 @@
import Component from '@ember/component'; import Component from '@ember/component';
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 Component.extend({
router: service(), tagName: 'ul',
classNames: ['contribution-list'],
tagName: 'div',
classNames: ['contributions'],
selectedContribution: null,
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() {
const activeIds = new Set(this.contributions.mapBy('contributorId')
.map(id => id.toString()));
return this.contributors.filter(c => activeIds.has(c.id.toString()));
}),
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: { actions: {
@@ -59,10 +13,6 @@ export default Component.extend({
} else { } else {
window.alert('Only members can veto contributions. Please ask someone to set you up.'); window.alert('Only members can veto contributions. Please ask someone to set you up.');
} }
},
openContributionDetails(contribution) {
this.router.transitionTo('dashboard.contributions.show', contribution);
} }
} }
+11 -42
View File
@@ -1,43 +1,15 @@
{{#if @showQuickFilter}} {{#each contributions as |contribution|}}
<div class="quick-filter"> <li data-contribution-id={{contribution.id}} class="{{contribution-status contribution}} {{if contribution.vetoed "vetoed"}}">
<p>
<label class="filter-contributor">
Contributor:
<select onchange={{action (mut this.contributorId) value="target.value"}}>
<option value="" selected>all</option>
{{#each this.contributorsActive as |contributor|}}
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}}
</select>
</label>
<label class="filter-contribution-kind">
Kind:
<select onchange={{action (mut this.contributionKind) value="target.value"}}>
<option value="" selected>all</option>
{{#each this.contributionKinds as |kind|}}
<option value={{kind}} selected={{eq this.contributionKind kind}}>{{capitalize-string kind}}</option>
{{/each}}
</select>
</label>
<label class="filter-contribution-size">
<Input @type="checkbox" @checked={{this.hideSmallContributions}} />
Hide small contributions
</label>
</p>
</div>
{{/if}}
<ul class="contribution-list {{if @loading 'loading'}}">
{{#each this.contributionsFiltered as |contribution|}}
<li role="button" data-contribution-id={{contribution.id}}
{{action "openContributionDetails" contribution}}
class="{{contribution-status contribution}}{{if (eq contribution.id @selectedContributionId) " selected"}}">
<p class="meta"> <p class="meta">
<span class="recipient"><UserAvatar @contributor={{contribution.contributor}} /></span> <span class="recipient">{{user-avatar contributor=contribution.contributor}}</span>
<span class="category {{contribution.kind}}">({{contribution.kind}})</span> <span class="category {{contribution.kind}}">({{contribution.kind}})</span>
<span class="title">{{contribution.description}}</span> <span class="title">
{{#if contribution.url}}
<a href={{contribution.url}} target="_blank" rel="noopener">{{contribution.description}}</a>
{{else}}
{{contribution.description}}
{{/if}}
</span>
</p> </p>
<p class="kredits-amount"> <p class="kredits-amount">
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span> <span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
@@ -45,12 +17,9 @@
{{#unless contribution.vetoed}} {{#unless contribution.vetoed}}
{{#unless (is-confirmed-contribution contribution)}} {{#unless (is-confirmed-contribution contribution)}}
<p class="voting"> <p class="voting">
{{input type="button" class="button small danger" value="veto" <button {{action "veto" contribution.id}} class="small danger">veto</button>
click=(action "veto" contribution.id)
disabled=contribution.hasPendingChanges}}
</p> </p>
{{/unless}} {{/unless}}
{{/unless}} {{/unless}}
</li> </li>
{{/each}} {{/each}}
</ul>
+11 -7
View File
@@ -1,18 +1,22 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { inject as service } from '@ember/service';
export default Component.extend({ export default Component.extend({
tagName: '',
router: service(), tagName: 'table',
classNames: 'contributor-list',
selectedContributorId: null, selectedContributor: null,
actions: { actions: {
openContributorDetails(contributor) { toggleContributorInfo(contributor) {
this.router.transitionTo('dashboard.contributors.show', contributor); if (contributor.showMetadata) {
contributor.set('showMetadata', false);
} else {
this.contributorList.setEach('showMetadata', false);
contributor.set('showMetadata', true);
}
} }
} }
}); });
+24 -10
View File
@@ -1,17 +1,12 @@
<table class="contributor-list {{if @loading 'loading'}}">
<thead>
</thead>
<tbody> <tbody>
{{#each @contributorList as |c|}} {{#each contributorList as |c|}}
<tr role="button" <tr role="button" class={{if (is-current-user c.contributor) "current-user"}} {{action "toggleContributorInfo" c}}>
onclick={{action "openContributorDetails" c.contributor}}
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id @selectedContributorId) "selected"}}">
<td class="person"> <td class="person">
<UserAvatar @contributor={{c.contributor}} /> {{c.contributor.name}} {{user-avatar contributor=c.contributor}} {{c.contributor.name}}
</td> </td>
<td class="kredits"> <td class="kredits">
<span class="amount"> <span class="amount">
{{#if @showUnconfirmedKredits}} {{#if showUnconfirmedKredits}}
{{c.amountTotal}} {{c.amountTotal}}
{{else}} {{else}}
{{c.amountConfirmed}} {{c.amountConfirmed}}
@@ -20,6 +15,25 @@
<span class="symbol">₭S</span> <span class="symbol">₭S</span>
</td> </td>
</tr> </tr>
<tr class="metadata {{if (is-current-user c.contributor) "current-user"}} {{if c.showMetadata "visible"}}">
<td colspan="2">
<ul>
<li>
<a href="https://rinkeby.etherscan.io/address/{{c.contributor.account}}" target="_blank" rel="noopener">Inspect Ethereum transactions</a>
</li>
{{#if c.contributor.ipfsHash}}
<li>
<a href="https://ipfs.io/ipfs/{{c.contributor.ipfsHash}}" target="_blank" rel="noopener">Inspect IPFS profile</a>
</li>
{{/if}}
<li>
{{link-to "Edit profile" "contributors.edit" c.contributor.id}}
</li>
</ul>
{{#if c.showMetadata}}
<pre>{{c.contributor.ipfsData}}</pre>
{{/if}}
</td>
</tr>
{{/each}} {{/each}}
</tbody> </tbody>
</table>
@@ -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|zoom/)) {
return 'icon-account-' + this.account.site.replace(/\./g, '-');
} else {
return 'icon-web-globe';
}
})
});
@@ -1,5 +0,0 @@
<a href={{this.account.url}} target="_blank" rel="noopener noreferrer"
title="{{this.account.username}} on {{this.account.site}}">
<span class="site">{{this.account.site}}</span>
{{component this.iconComponentName}}
</a>
+20
View File
@@ -0,0 +1,20 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'ul',
classNames: ['proposal-list'],
actions: {
confirm(proposalId) {
if (this.contractInteractionEnabled) {
this.confirmProposal(proposalId);
} else {
window.alert('Only members can vote on proposals. Please ask someone to set you up.');
}
}
}
});
+20
View File
@@ -0,0 +1,20 @@
{{#each proposals as |proposal|}}
<li data-proposal-id={{proposal.id}} class={{if proposal.isExecuted "confirmed" "unconfirmed"}}>
<p class="meta">
<span class="category {{proposal.kind}}">♥ ({{proposal.kind}})</span>
<span class="recipient">{{proposal.contributor.name}}:</span>
</p>
<p class="kredits-amount">
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
</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>
{{/each}}
@@ -1,46 +0,0 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { isPresent } from '@ember/utils';
export default Component.extend({
tagName: '',
kredits: service(),
router: service(),
setupInProgress: false,
userHasEthereumWallet: computed(function() {
return isPresent(window.ethereum);
}),
showConnectButton: computed('userHasEthereumWallet',
'kredits.hasAccounts', function() {
return this.userHasEthereumWallet &&
!this.kredits.hasAccounts;
}),
actions: {
signup() {
this.router.transitionTo('signup');
},
async connectAccount() {
try {
await window.ethereum.enable();
this.set('setupInProgress', true);
await this.kredits.setup();
this.set('setupInProgress', false);
this.router.transitionTo('dashboard');
} catch (error) {
this.set('setupInProgress', false);
console.log('Opening Ethereum wallet failed:', error);
}
}
}
});
@@ -1,18 +0,0 @@
<section id="user-account">
{{#if this.setupInProgress}}
Connecting account...
{{else}}
{{#if (and this.kredits.hasAccounts this.kredits.currentUser)}}
{{this.kredits.currentUser.name}}
{{#if this.kredits.currentUserIsCore}}
<span class="core-flag">(core)</span>
{{/if}}
{{else}}
<span class="anonymous">Anonymous</span>
<button onclick={{action "signup"}} class="small" type="button">Sign up</button>
{{#if this.showConnectButton}}
<button onclick={{action "connectAccount"}} class="small green" type="button">Connect account</button>
{{/if}}
{{/if}}
{{/if}}
</section>
+2 -24
View File
@@ -1,33 +1,11 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { alias } from '@ember/object/computed'; import { alias } from '@ember/object/computed';
import { computed } from '@ember/object';
const SIZES = {
'small': '128', // pixels
'medium': '256',
'large': '512'
}
export default Component.extend({ export default Component.extend({
contributor: null, contributor: null,
tagName: 'img', tagName: 'img',
classNames: ['avatar'], classNames: ['avatar'],
classNameBindings: ['size'],
attributeBindings: ['src', 'title'], attributeBindings: ['src', 'title'],
size: 'small', src: alias('contributor.avatarURL'),
title: alias('contributor.name')
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]}`;
} else {
// TODO use custom avatar
return '';
}
})
}); });
-2
View File
@@ -6,8 +6,6 @@ export default Controller.extend({
kredits: service(), kredits: service(),
queryParams: ['contributorId', 'kind', 'amount'],
contributors: alias('kredits.contributors'), contributors: alias('kredits.contributors'),
minedContributors: filterBy('contributors', 'id'), minedContributors: filterBy('contributors', 'id'),
@@ -1,7 +0,0 @@
import ContributionsNewController from 'kredits-web/controllers/contributions/new';
export default ContributionsNewController.extend({
attributes: null,
});
+3 -1
View File
@@ -8,7 +8,9 @@ export default Controller.extend({
actions: { actions: {
save (attributes) { save (attributes) {
return this.kredits.updateContributor(this.model.id, attributes); return this.kredits
.updateContributor(this.model.id, attributes)
.then(() => this.transitionToRoute('index'))
} }
} }
+3 -1
View File
@@ -8,7 +8,9 @@ export default Controller.extend({
actions: { actions: {
save (contributor) { save (contributor) {
return this.kredits.addContributor(contributor); return this.kredits
.addContributor(contributor)
.then(() => this.transitionToRoute('index'))
} }
} }
@@ -1,12 +0,0 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import config from 'kredits-web/config/environment';
export default Controller.extend({
ipfsGatewayUrl: computed(function() {
return config.ipfs.gatewayUrl;
})
});
@@ -1,17 +0,0 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import config from 'kredits-web/config/environment';
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';
}),
ipfsGatewayUrl: computed(function() {
return config.ipfs.gatewayUrl;
})
});
@@ -5,18 +5,13 @@ import { inject as service } from '@ember/service';
export default Controller.extend({ export default Controller.extend({
kredits: service(), kredits: service(),
showDetailsPane: false,
selectedContributorId: null,
selectedContributionId: null,
currentBlock: alias('kredits.currentBlock'), currentBlock: alias('kredits.currentBlock'),
contributions: alias('kredits.contributions'), contributions: alias('kredits.contributions'),
contributionsConfirmed: alias('kredits.contributionsConfirmed'), contributionsConfirmed: alias('kredits.contributionsConfirmed'),
contributionsUnconfirmed: alias('kredits.contributionsUnconfirmed'), contributionsUnconfirmed: alias('kredits.contributionsUnconfirmed'),
contributionsSorting: Object.freeze(['date:desc', 'time:desc', 'id:desc']), contributionsSorting: Object.freeze(['id:desc']),
contributionsUnconfirmedSorted: sort('contributionsUnconfirmed', 'contributionsSorting'), contributionsUnconfirmedSorted: sort('contributionsUnconfirmed', 'contributionsSorting'),
contributionsConfirmedSorted: sort('contributionsConfirmed', 'contributionsSorting'), contributionsConfirmedSorted: sort('contributionsConfirmed', 'contributionsSorting'),
@@ -30,13 +25,6 @@ export default Controller.extend({
showUnconfirmedKredits: true, showUnconfirmedKredits: true,
hideUnconfirmedKredits: not('showUnconfirmedKredits'), hideUnconfirmedKredits: not('showUnconfirmedKredits'),
showQuickFilterUnconfirmed: false,
showQuickFilterConfirmed: false,
showIntroText: computed('kredits.{hasAccounts,currentUser}', function(){
return (!this.kredits.hasAccounts || !this.kredits.currentUser);
}),
actions: { actions: {
vetoContribution (contributionId) { vetoContribution (contributionId) {
@@ -45,12 +33,10 @@ export default Controller.extend({
}); });
}, },
toggleQuickFilterUnconfirmed () { confirmProposal (proposalId) {
this.toggleProperty('showQuickFilterUnconfirmed'); this.kredits.vote(proposalId).then(transaction => {
}, console.debug('[controllers:index] Vote submitted to Ethereum blockhain: '+transaction.hash);
});
toggleQuickFilterConfirmed () {
this.toggleProperty('showQuickFilterConfirmed');
} }
} }
+24
View File
@@ -0,0 +1,24 @@
import Controller from '@ember/controller';
import { alias, filterBy } 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'),
actions: {
save(proposal) {
// 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.
let contributor = this.contributors.findBy('id', proposal.contributorId);
proposal.contributorIpfsHash = contributor.get('ipfsHash');
return this.kredits.addProposal(proposal)
.then((proposal) => {
this.transitionToRoute('index');
return proposal;
});
}
}
});
-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);
}
});
+3 -39
View File
@@ -1,7 +1,6 @@
import Helper from '@ember/component/helper'; import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed'; import { alias } from '@ember/object/computed';
import { once } from '@ember/runloop';
export default Helper.extend({ export default Helper.extend({
@@ -9,48 +8,13 @@ export default Helper.extend({
currentBlock: alias('kredits.currentBlock'), currentBlock: alias('kredits.currentBlock'),
compute([contribution]) { compute([contribution]) {
this.setupRecompute(contribution);
let status = [];
if (contribution.vetoed) { if (contribution.vetoed) {
status.push('vetoed'); return 'vetoed';
} else if (contribution.confirmedAt > this.currentBlock) { } else if (contribution.confirmedAt > this.currentBlock) {
status.push('unconfirmed'); return 'unconfirmed';
} else { } else {
status.push('confirmed'); return 'confirmed'
} }
if (contribution.hasPendingChanges) {
status.push('pending');
}
return status.join(' ');
},
destroy () {
if (this.teardown) this.teardown();
this._super(...arguments);
},
setupRecompute (contribution) {
if (this.teardown) this.teardown();
contribution.addObserver('vetoed' , this, this.triggerRecompute);
contribution.addObserver('confirmedAt' , this, this.triggerRecompute);
contribution.addObserver('currentBlock' , this, this.triggerRecompute);
contribution.addObserver('hasPendingChanges' , this, this.triggerRecompute);
this.teardown = () => {
contribution.removeObserver('vetoed', this, this.triggerRecompute);
contribution.removeObserver('confirmedAt', this, this.triggerRecompute);
contribution.removeObserver('currentBlock', this, this.triggerRecompute);
contribution.removeObserver('hasPendingChanges', this, this.triggerRecompute);
};
},
triggerRecompute () {
once(this, this.recompute);
} }
}); });
+6 -30
View File
@@ -1,6 +1,5 @@
import EmberObject, { computed } from '@ember/object'; import EmberObject from '@ember/object';
import { isEmpty, isPresent } from '@ember/utils'; import bignumber from 'kredits-web/utils/cps/bignumber';
import moment from 'moment';
export default EmberObject.extend({ export default EmberObject.extend({
@@ -8,45 +7,22 @@ export default EmberObject.extend({
id: null, id: null,
contributorId: null, contributorId: null,
amount: null, amount: null,
confirmedAt: null, confirmedAt: bignumber('confirmedAtBlock', 'toNumber'),
vetoed: null, vetoed: null,
ipfsHash: null, ipfsHash: null,
// contributor model instance creatorAccount: null,
contributor: null,
// TODO contributor who submitted the contribution
// submittedBy: null,
// IPFS // IPFS
kind: null, kind: null,
description: null, description: null,
details: null, details: null,
url: null, url: null,
date: null, ipfsData: '',
time: null,
pendingTx: null,
init () { init () {
this._super(...arguments); this._super(...arguments);
if (isEmpty(this.details)) this.set('details', {}); this.set('details', {});
},
iso8601Date: computed('date', 'time', function() {
return this.time ? `${this.date}T${this.time}` : this.date;
}),
jsDate: computed('iso8601Date', function() {
return moment(this.iso8601Date).toDate();
}),
hasPendingChanges: computed('pendingTx', function() {
return isPresent(this.pendingTx);
}),
serialize () {
return JSON.stringify(this);
} }
}); });
+13 -8
View File
@@ -1,12 +1,15 @@
import { computed } from '@ember/object';
import EmberObject from '@ember/object'; import EmberObject from '@ember/object';
import bignumber from 'kredits-web/utils/cps/bignumber';
import kreditsValue from 'kredits-web/utils/cps/kredits';
export default EmberObject.extend({ export default EmberObject.extend({
// Contract // Contract
id: null, id: bignumber('idRaw', 'toString'),
account: null, account: null,
balance: 0, balance: kreditsValue('balanceRaw'),
totalKreditsEarned: 0, totalKreditsEarned: bignumber('totalKreditsEarnedRaw', 'toNumber'),
contributionsCount: 0, contributionsCount: bignumber('contributionsCountRaw', 'toNumber'),
isCore: false, isCore: false,
ipfsHash: null, ipfsHash: null,
@@ -17,10 +20,12 @@ export default EmberObject.extend({
github_username: null, github_username: null,
github_uid: null, github_uid: null,
wiki_username: null, wiki_username: null,
zoom_display_name: null, ipfsData: '',
serialize () { avatarURL: computed('github_uid', function() {
return JSON.stringify(this); let github_uid = this.github_uid;
if (github_uid) {
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=128`;
} }
}),
}); });
+32
View File
@@ -0,0 +1,32 @@
import EmberObject from '@ember/object';
import { alias } from '@ember/object/computed';
import bignumber from 'kredits-web/utils/cps/bignumber';
export default EmberObject.extend({
// Contract
id: bignumber('idRaw', 'toString'),
creatorAccount: null,
contributorId: bignumber('contributorIdRaw', 'toString'),
amount: bignumber('amountRaw', 'toNumber'),
votesCount: bignumber('votesCountRaw', 'toNumber'),
votesNeeded: bignumber('votesNeededRaw', 'toNumber'),
executed: null,
ipfsHash: null,
// Shortcuts
isExecuted: alias('executed'),
// IPFS
kind: null,
description: null,
details: null,
url: null,
ipfsData: '',
init () {
this._super(...arguments);
this.set('details', {});
}
});
+3
View File
@@ -0,0 +1,3 @@
import Resolver from 'ember-resolver';
export default Resolver;
+9 -19
View File
@@ -1,32 +1,22 @@
import EmberRouter from '@ember/routing/router'; import EmberRouter from '@ember/routing/router';
import config from './config/environment'; import config from './config/environment';
export default class Router extends EmberRouter { const Router = EmberRouter.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('proposals', function() {
this.route('contributors', function() { this.route('new');
this.route('show', { path: ':id' });
});
this.route('contributions', function() {
this.route('show', { path: ':id' });
});
}); });
this.route('contributions', function() { this.route('contributions', function() {
this.route('new', { queryParams: ['contributorId', 'kind', 'amount'] }); this.route('new');
this.route('resubmit', { path: ':id/resubmit' });
}); });
this.route('contributors', function() { this.route('contributors', function() {
this.route('new'); this.route('new');
this.route('edit', { path: ':id/edit' }); this.route('edit', { path: ':id/edit' });
}); });
this.route('signup', function() {
this.route('github');
this.route('eth-account');
this.route('complete');
});
}); });
export default Router;
+6 -14
View File
@@ -1,11 +1,10 @@
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { schedule } from '@ember/runloop';
export default Route.extend({ export default Route.extend({
kredits: service(), kredits: service(),
beforeModel(/* transition */) { beforeModel(transition) {
const kredits = this.kredits; const kredits = this.kredits;
return kredits.setup().then(() => { return kredits.setup().then(() => {
@@ -13,6 +12,11 @@ export default Route.extend({
console.error('Kredits preflight check failed!'); console.error('Kredits preflight check failed!');
console.error(error); 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) => { }).catch((error) => {
console.log('Error initializing Kredits', error); console.log('Error initializing Kredits', error);
}); });
@@ -22,18 +26,6 @@ export default Route.extend({
return this.kredits.loadInitialData() return this.kredits.loadInitialData()
.then(() => { .then(() => {
this.kredits.addContractEventHandlers(); this.kredits.addContractEventHandlers();
})
.then(() => {
if (this.kredits.contributorsNeedSync) {
schedule('afterRender', this.kredits.syncContributors,
this.kredits.syncContributors.perform);
}
if (this.kredits.contributionsNeedSync) {
schedule('afterRender', this.kredits.syncContributions,
this.kredits.syncContributions.perform);
}
schedule('afterRender', this.kredits.fetchMissingContributions,
this.kredits.fetchMissingContributions.perform);
}); });
} }
}); });
-9
View File
@@ -1,9 +0,0 @@
import Route from '@ember/routing/route';
export default Route.extend({
model (params) {
return { params };
}
});
-25
View File
@@ -1,25 +0,0 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default Route.extend({
kredits: service(),
model(params) {
const contribution = this.kredits.contributions.findBy('id', parseInt(params.id));
contribution.contributorId = contribution.contributorId.toString();
return contribution;
},
setupController (controller, model) {
this._super(controller, model);
controller.set('attributes', model.getProperties([
'kind', 'amount', 'description', 'url', 'details'
]));
controller.set('attributes.contributorId', model.contributorId.toString());
controller.set('attributes.date', model.jsDate);
}
});
+1 -2
View File
@@ -22,8 +22,7 @@ export default Route.extend({
github_username: model.github_username, github_username: model.github_username,
github_uid: model.github_uid, github_uid: model.github_uid,
gitea_username: model.gitea_username, gitea_username: model.gitea_username,
wiki_username: model.wiki_username, wiki_username: model.wiki_username
zoom_display_name: model.zoom_display_name
}); });
} }
@@ -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
});
}
});
-9
View File
@@ -1,9 +0,0 @@
import Route from '@ember/routing/route';
export default Route.extend({
redirect () {
this.transitionTo('dashboard');
}
});
-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');
}
});
-27
View File
@@ -1,27 +0,0 @@
import Service from '@ember/service';
import * as localforage from 'localforage';
import config from 'kredits-web/config/environment';
function createStore(name) {
const networkName = config.web3RequiredNetwork || 'custom';
return localforage.createInstance({ name: `kredits:${networkName}:${name}` });
}
export default class BrowserCacheService extends Service {
constructor() {
super(...arguments);
this.stores = {
contributors: createStore('contributors'),
contributions: createStore('contributions')
}
}
get contributors() {
return this.stores.contributors;
}
get contributions() {
return this.stores.contributions;
}
}
+119 -215
View File
@@ -1,42 +1,37 @@
import ethers from 'ethers'; import ethers from 'npm:ethers';
import Kredits from 'kredits-contracts'; import Kredits from 'npm:kredits-contracts';
import RSVP from 'rsvp';
import Service from '@ember/service'; import Service from '@ember/service';
import EmberObject from '@ember/object'; import EmberObject from '@ember/object';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { alias, notEmpty } from '@ember/object/computed'; import { alias, notEmpty } from '@ember/object/computed';
import { isEmpty, isPresent } from '@ember/utils'; import { isEmpty } from '@ember/utils';
import { inject as service } from '@ember/service';
import { task, taskGroup } from 'ember-concurrency';
import groupBy from 'kredits-web/utils/group-by'; import groupBy from 'kredits-web/utils/group-by';
import processContributorData from 'kredits-web/utils/process-contributor-data';
import processContributionData from 'kredits-web/utils/process-contribution-data';
import formatKredits from 'kredits-web/utils/format-kredits'; 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 Contribution from 'kredits-web/models/contribution' import Contribution from 'kredits-web/models/contribution'
export default Service.extend({ export default Service.extend({
browserCache: service(),
currentBlock: null, currentBlock: null,
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded. currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
currentUser: null, currentUser: null,
contributors: null, contributors: null,
contributions: null, contributions: null,
githubAccessToken: null, proposals: null,
currentUserIsContributor: notEmpty('currentUser'), currentUserIsContributor: notEmpty('currentUser'),
currentUserIsCore: alias('currentUser.isCore'), currentUserIsCore: alias('currentUser.isCore'),
hasAccounts: notEmpty('currentUserAccounts'), hasAccounts: notEmpty('currentUserAccounts'),
// When data was loaded from cache, we need to fetch updates from the network accountNeedsUnlock: computed('currentUserAccounts', function() {
contributorsNeedSync: false, return this.currentUserAccounts && isEmpty(this.currentUserAccounts);
contributionsNeedSync: false, }),
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() { contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
return this.contributions.filter(contribution => { return this.contributions.filter(contribution => {
@@ -85,6 +80,7 @@ export default Service.extend({
init () { init () {
this._super(...arguments); this._super(...arguments);
this.set('contributors', []); this.set('contributors', []);
this.set('proposals', []);
this.set('contributions', []); this.set('contributions', []);
}, },
@@ -94,7 +90,7 @@ export default Service.extend({
getEthProvider () { getEthProvider () {
let ethProvider; let ethProvider;
return new Promise(resolve => { return new RSVP.Promise(async (resolve) => {
function instantiateWithoutAccount () { function instantiateWithoutAccount () {
console.debug('[kredits] Creating new instance from npm module class'); console.debug('[kredits] Creating new instance from npm module class');
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`); console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
@@ -108,14 +104,7 @@ export default Service.extend({
async function instantiateWithAccount (web3Provider, context) { async function instantiateWithAccount (web3Provider, context) {
console.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask'); console.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask');
ethProvider = new ethers.providers.Web3Provider(web3Provider); ethProvider = new ethers.providers.Web3Provider(web3Provider);
// const network = await ethProvider.getNetwork();
const network = await ethProvider.getNetwork();
if (isPresent(config.web3RequiredNetwork) &&
network.name !== config.web3RequiredNetwork) {
window.alert(`Please switch your Ethereum wallet to the "${config.web3RequiredNetwork}" network before connecting your account.`);
return instantiateWithoutAccount();
}
ethProvider.listAccounts().then(accounts => { ethProvider.listAccounts().then(accounts => {
context.set('currentUserAccounts', accounts); context.set('currentUserAccounts', accounts);
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner(); const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
@@ -127,9 +116,12 @@ export default Service.extend({
} }
if (window.ethereum) { if (window.ethereum) {
if (window.ethereum.isConnected()) { try {
// Request account access if needed
await window.ethereum.enable();
// Acccounts now exposed
instantiateWithAccount(window.ethereum, this); instantiateWithAccount(window.ethereum, this);
} else { } catch (error) {
instantiateWithoutAccount(); instantiateWithoutAccount();
} }
} }
@@ -179,24 +171,12 @@ export default Service.extend({
.then(total => total.toNumber()); .then(total => total.toNumber());
}), }),
async loadInitialData () {
const numCachedContributors = await this.browserCache.contributors.length();
if (numCachedContributors > 0) {
await this.loadContributorsFromCache();
this.set('contributorsNeedSync', true);
} else {
await this.fetchContributors();
}
const numCachedContributions = await this.browserCache.contributions.length(); loadInitialData () {
if (numCachedContributions > 0) { return this.getContributors()
await this.loadContributionsFromCache(); .then(contributors => this.contributors.pushObjects(contributors))
this.set('contributionsNeedSync', true); .then(() => this.getContributions())
} else { .then(contributions => this.contributions.pushObjects(contributions))
await this.fetchContributions({ page: { size: 30 } });
}
return Promise.resolve();
}, },
addContributor (attributes) { addContributor (attributes) {
@@ -227,48 +207,15 @@ export default Service.extend({
}); });
}, },
fetchContributors () { getContributors () {
console.debug(`[kredits] Fetching all contributors from the network`);
return this.kredits.Contributor.all() return this.kredits.Contributor.all()
.then(contributors => { .then((contributors) => {
return contributors.forEach(data => { return contributors.map((contributor) => {
this.loadContributorFromData(data); return Contributor.create(contributor);
return;
}); });
})
.then(() => {
return this.cacheLoadedContributors();
}); });
}, },
loadContributorFromData(data) {
const contributor = Contributor.create(processContributorData(data));
const loadedContributor = this.contributors.findBy('id', contributor.id);
if (loadedContributor) { this.contributors.removeObject(loadedContributor); }
this.contributors.pushObject(contributor);
return contributor;
},
async cacheLoadedContributors () {
for (const c of this.contributors) {
await this.browserCache.contributors.setItem(c.id, c.serialize());
}
console.debug(`[kredits] Cached ${this.contributors.length} contributors in browser storage`);
return Promise.resolve();
},
async loadContributorsFromCache () {
return this.browserCache.contributors.iterate((value/*, key , iterationNumber */) => {
this.contributors.pushObject(Contributor.create(JSON.parse(value)));
}).then((/* result */) => {
console.debug(`[kredits] Loaded ${this.contributors.length} contributors from cache`);
});
},
syncContributors: task(function * () {
yield this.fetchContributors();
}),
addContribution (attributes) { addContribution (attributes) {
console.debug('[kredits] add contribution', attributes); console.debug('[kredits] add contribution', attributes);
@@ -277,151 +224,84 @@ export default Service.extend({
console.debug('[kredits] add contribution response', data); console.debug('[kredits] add contribution response', data);
attributes.contributor = this.contributors.findBy('id', attributes.contributorId); attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
const contribution = Contribution.create(attributes); const contribution = Contribution.create(attributes);
contribution.set('pendingTx', data); // TODO receive from wrapper
contribution.set('confirmedAtBlock', data.blockNumber + 40320); contribution.set('confirmedAtBlock', data.blockNumber + 40320);
this.contributions.pushObject(contribution); this.contributions.pushObject(contribution);
return contribution; return contribution;
}); });
}, },
fetchContributions (options = { page: { size: 200 } }) { addProposal (attributes) {
console.debug(`[kredits] Fetching contributions from the network`); console.debug('[kredits] add proposal', attributes);
return this.kredits.Contribution.all(options)
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 => { .then(contributions => {
return contributions.map(data => { return contributions.map(contribution => {
const contribution = this.loadContributionFromData(data); contribution.contributor = this.contributors.findBy('id', contribution.contributorId.toString());
return contribution; return Contribution.create(contribution);
});
})
.then(contributions => {
const cacheWrites = contributions.map(c => {
return this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
});
return Promise.all(cacheWrites).then(() => {
console.debug(`[kredits] Cached ${contributions.length} contributions in browser storage`);
}); });
}); });
}, },
loadContributionFromData(data) { vote (proposalId) {
const contribution = Contribution.create(processContributionData(data)); console.debug('[kredits] vote for', proposalId);
contribution.set('contributor', this.contributors.findBy('id', data.contributorId.toString()));
const loadedContribution = this.contributions.findBy('id', contribution.id);
if (loadedContribution) { this.contributions.removeObject(loadedContribution); }
this.contributions.pushObject(contribution);
return contribution;
},
async cacheLoadedContributions () { return this.kredits.Proposal.functions.vote(proposalId)
for (const c of this.contributions) {
await this.browserCache.contributions.setItem(c.id, c.serialize());
}
console.debug(`[kredits] Cached ${this.contributions.length} contributions in browser storage`);
return Promise.resolve();
},
async loadContributionsFromCache () {
return this.browserCache.contributions.iterate((value/*, key , iterationNumber */) => {
this.contributions.pushObject(Contribution.create(JSON.parse(value)));
}).then((/* result */) => {
console.debug(`[kredits] Loaded ${this.contributions.length} contributions from cache`);
});
},
contributionTasks: taskGroup().enqueue(),
syncContributions: task(function * () {
yield this.fetchNewContributions.perform();
yield this.syncUnconfirmedContributions.perform();
}).group('contributionTasks'),
fetchNewContributions: task(function * () {
const count = yield this.kredits.Contribution.functions.contributionsCount();
const lastKnownContributionId = Math.max.apply(null, this.contributions.mapBy('id'));
const toFetch = count - lastKnownContributionId;
if (toFetch > 0) {
console.debug(`[kredits] Fetching ${toFetch} new contributions`);
for (let id = lastKnownContributionId; id <= count; id++) {
const data = yield this.kredits.Contribution.getById(id);
const c = this.loadContributionFromData(data);
yield this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
}
} else {
console.debug(`[kredits] No new contributions to fetch`);
}
}),
fetchMissingContributions: task(function * () {
const count = yield this.kredits.Contribution.functions.contributionsCount();
const allIds = [...Array(count+1).keys()];
allIds.shift(); // remove first item, which is 0
const loadedContributions = new Set(this.contributions.mapBy('id'));
const toFetch = allIds.filter(id => !loadedContributions.has(id));
if (toFetch.length === 0) {
console.debug(`[kredits] No contributions left to fetch`);
return;
}
console.debug(`[kredits] Fetching ${toFetch.length} past contributions`);
let countFetched = 0;
for (let id = count; id > 0; id--) {
if (loadedContributions.has(id)) {
continue;
} else {
const data = yield this.kredits.Contribution.getById(id);
const c = this.loadContributionFromData(data);
yield this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
countFetched++;
if (countFetched % 20 === 0) {
console.debug(`[kredits] Fetched ${countFetched} more contributions`);
}
}
}
console.debug(`[kredits] Cached ${countFetched} past contributions`);
}).group('contributionTasks'),
syncUnconfirmedContributions: task(function * () {
if (this.contributionsUnconfirmed.length > 0) {
console.debug(`[kredits] Syncing unconfirmed contributions`);
for (const c of this.contributionsUnconfirmed) {
const data = yield this.kredits.Contribution.getById(c.id);
const contribution = this.loadContributionFromData(data);
yield this.browserCache.contributions.setItem(c.id.toString(), contribution.serialize());
}
} else {
console.debug(`[kredits] No unconfirmed contributions to sync`);
}
}),
veto (contributionId) {
console.debug('[kredits] veto against', contributionId);
const contribution = this.contributions.findBy('id', contributionId);
return this.kredits.Contribution.functions.veto(contributionId, { gasLimit: 300000 })
.then(data => { .then(data => {
console.debug('[kredits] veto response', data); console.debug('[kredits] vote response', data);
contribution.set('pendingTx', data);
return data; return data;
}); });
}, },
getCurrentUser: computed('kredits.provider', 'currentUserAccounts.[]', function() { 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)) { if (isEmpty(this.currentUserAccounts)) {
return Promise.resolve(); return RSVP.resolve();
} }
return this.kredits.Contributor return this.kredits.Contributor
.functions.getContributorIdByAddress(this.currentUserAccounts.firstObject) .functions.getContributorIdByAddress(this.get('currentUserAccounts.firstObject'))
.then((id) => { .then((id) => {
// check if the user is a contributor or not // check if the user is a contributor or not
if (id === 0) { if (id === 0) {
return Promise.resolve(); return RSVP.resolve();
} else { } else {
return this.kredits.Contributor.getById(id); return this.kredits.Contributor.getById(id);
} }
}); });
}), }),
findProposalById(proposalId) {
return this.proposals.findBy('id', proposalId.toString());
},
// Contract events // Contract events
addContractEventHandlers () { addContractEventHandlers () {
this.kredits.Contributor this.kredits.Contributor
@@ -430,9 +310,13 @@ export default Service.extend({
.on('ContributorAdded', this.handleContributorChange.bind(this)) .on('ContributorAdded', this.handleContributorChange.bind(this))
this.kredits.Contribution this.kredits.Contribution
.on('ContributionAdded', this.handleContributionAdded.bind(this))
.on('ContributionVetoed', this.handleContributionVetoed.bind(this)) .on('ContributionVetoed', this.handleContributionVetoed.bind(this))
this.kredits.Proposal
.on('ProposalCreated', this.handleProposalCreated.bind(this))
.on('ProposalVoted', this.handleProposalVoted.bind(this))
.on('ProposalExecuted', this.handleProposalExecuted.bind(this));
this.kredits.Token this.kredits.Token
.on('Transfer', this.handleTransfer.bind(this)); .on('Transfer', this.handleTransfer.bind(this));
}, },
@@ -453,24 +337,6 @@ export default Service.extend({
this.contributors.pushObject(newContributor); this.contributors.pushObject(newContributor);
}, },
async handleContributionAdded (id, contributorId, amount) {
console.debug('[kredits] ContributionAdded event received', { id, contributorId, amount });
const pendingContribution = this.contributions.find(c => {
return (c.id === null) &&
(c.contributorId.toString() === contributorId.toString()) &&
(c.amount.toString() === amount.toString());
});
if (pendingContribution) {
const attributes = await this.kredits.Contribution.getById(id);
attributes.contributor = this.contributors.findBy('id', attributes.contributorId.toString());
const newContribution = Contribution.create(attributes);
this.contributions.addObject(newContribution);
this.contributions.removeObject(pendingContribution);
}
},
handleContributionVetoed (contributionId) { handleContributionVetoed (contributionId) {
console.debug('[kredits] ContributionVetoed event received for ', contributionId); console.debug('[kredits] ContributionVetoed event received for ', contributionId);
const contribution = this.contributions.findBy('id', contributionId); const contribution = this.contributions.findBy('id', contributionId);
@@ -478,10 +344,48 @@ export default Service.extend({
if (contribution) { if (contribution) {
contribution.set('vetoed', true); contribution.set('vetoed', true);
contribution.set('pendingTx', null);
} }
}, },
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
handleProposalVoted (proposalId, voterId, totalVotes) {
let proposal = this.findProposalById(proposalId);
if (proposal) {
proposal.set('votesCount', totalVotes);
}
},
handleProposalExecuted (proposalId, contributorId, amount) {
let proposal = this.findProposalById(proposalId);
if (proposal.get('isExecuted')) {
console.debug('[events] proposal already executed, not adding from event');
return;
}
proposal.set('executed', true);
this.contributors
.findBy('id', contributorId.toString())
.incrementProperty('balance', amount);
},
handleTransfer (from, to, value) { handleTransfer (from, to, value) {
value = value.toNumber(); value = value.toNumber();
+3 -35
View File
@@ -10,30 +10,17 @@ button, input[type=submit], .button {
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;
letter-spacing: 0.1em; letter-spacing: 0.1em;
-webkit-appearance: none;
&+button, &+input[type=submit], &+.button {
margin-left: 0.5rem;
}
&:hover { &:hover {
background-color: rgba(22, 21, 40, 0.8); background-color: rgba(22, 21, 40, 0.8);
} }
&:active, &.active {
border-color: $primary-color;
}
&[disabled] {
color: rgba(255,255,255,0.5);
}
&.small { &.small {
font-size: 0.86rem; font-size: 0.8rem;
padding: 0.2rem 0.8rem; padding: 0.2rem 0.8rem;
} }
&.danger:not(:disabled) { &.danger {
color: $red; color: $red;
background-color: rgba(40, 21, 21, 0.6); background-color: rgba(40, 21, 21, 0.6);
border-color: rgba(40, 21, 21, 1); border-color: rgba(40, 21, 21, 1);
@@ -41,12 +28,9 @@ button, input[type=submit], .button {
&:hover { &:hover {
background-color: rgba(40, 21, 21, 0.8); background-color: rgba(40, 21, 21, 0.8);
} }
&:active, &.active {
border-color: $red;
}
} }
&.green:not(:disabled) { &.green {
color: $green; color: $green;
background-color: rgba(21, 40, 21, 0.6); background-color: rgba(21, 40, 21, 0.6);
border-color: rgba(21, 40, 21, 1); border-color: rgba(21, 40, 21, 1);
@@ -54,21 +38,5 @@ button, input[type=submit], .button {
&:hover { &:hover {
background-color: rgba(21, 40, 21, 0.8); 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;
}
}
} }
} }
+5
View File
@@ -8,3 +8,8 @@ $red: #fb6868;
$primary-color: $blue; $primary-color: $blue;
$body-text-color: #fff; $body-text-color: #fff;
#topbar {
background-color: rgba(255, 0, 255, .2);
}
+1 -9
View File
@@ -1,20 +1,12 @@
section#add-contributor, section#add-contributor,
section#add-contribution, section#add-contribution,
section#signup { section#add-proposal {
form { form {
p { p {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
&.mg-bottom-md {
margin-bottom: 2rem;
}
&.label {
margin-bottom: .5rem;
}
&.actions { &.actions {
padding-top: 1.5rem; padding-top: 1.5rem;
text-align: center; text-align: center;
+2 -74
View File
@@ -5,7 +5,7 @@ main {
padding: 1rem; padding: 1rem;
} }
&#dashboard { &#index {
width: 100%; width: 100%;
display: grid; display: grid;
grid-row-gap: 2rem; grid-row-gap: 2rem;
@@ -28,88 +28,16 @@ main {
} }
} }
} }
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;
}
}
}
}
}
#intro {
padding: 2rem;
background-color: rgba(0,0,0,.2);
font-size: 1.6rem;
@include media-max(small) {
padding: 2rem 1rem;
font-size: 1.4rem;
}
h2 {
font-size: inherit;
margin-bottom: 1em;
}
p {
margin-bottom: 1em;
line-height: 1.5em;
font-size: 1.2rem;
&:last-child {
margin-bottom: 0;
}
}
a {
text-decoration: none;
&:hover, &:active {
text-decoration: underline;
}
}
} }
@media (min-width: 550px) { @media (min-width: 550px) {
main { main {
&#dashboard { &#index {
grid-column-gap: 4rem; grid-column-gap: 4rem;
grid-row-gap: 2rem; grid-row-gap: 2rem;
grid-template-columns: 2fr 4fr; grid-template-columns: 2fr 4fr;
grid-template-areas: grid-template-areas:
"stats contributions"; "stats contributions";
&.with-details {
grid-column-gap: 3rem;
grid-template-columns: 2fr 4fr 2fr;
grid-template-areas:
"stats contributions details";
}
} }
} }
} }
-17
View File
@@ -1,17 +0,0 @@
@mixin loading-border-top {
&::before {
display: block;
width: 100%;
height: 1px;
content: '';
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.2) 40%, #68d7fb 60%, rgba(255, 255, 255, 0.2));
background-size: 200% 200%;
animation: kitt 2.5s linear infinite;
}
}
@keyframes kitt {
0%{ background-position: 0% 0%}
50%{ background-position: 100% 0%}
100%{ background-position: 0% 0%}
}
+23 -19
View File
@@ -2,6 +2,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px;
} }
*:hover, *:active, *:focus { *:hover, *:active, *:focus {
@@ -16,17 +17,13 @@
$font-family-sans: 'Open Sans', sans-serif; $font-family-sans: 'Open Sans', sans-serif;
html, body { body {
font-family: $font-family-sans; font-family: $font-family-sans;
font-weight: 300; font-weight: 300;
font-size: 12px;
color: $body-text-color;
}
body {
background-image: linear-gradient(to bottom, rgba(22, 21, 40, .4), rgba(0, 0, 0, .75)), url('/img/bg.jpg'); background-image: linear-gradient(to bottom, rgba(22, 21, 40, .4), rgba(0, 0, 0, .75)), url('/img/bg.jpg');
background-repeat: none; background-repeat: none;
background-attachment: fixed; background-attachment: fixed;
color: $body-text-color;
} }
@media screen and (min-width: 900px) { @media screen and (min-width: 900px) {
@@ -41,10 +38,6 @@ 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: 1.4rem;
@@ -76,6 +69,22 @@ section {
} }
} }
} }
&#proposals-open, &#proposals-closed {
.actions {
padding-top: 3rem;
font-size: 1rem;
color: $primary-color;
text-align: center;
@include media-max(small) {
padding-top: 2rem;
}
a {
color: $primary-color;
}
}
}
} }
@media (min-width: 550px) { @media (min-width: 550px) {
@@ -88,14 +97,9 @@ section {
@import "buttons"; @import "buttons";
@import "forms"; @import "forms";
@import "sugar";
@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/topbar"; @import "components/topbar";
@import "components/topbar-account-panel"; @import "components/loading-spinner";
@import "components/contributor-list";
@import "components/proposal-list";
@import "components/contribution-list";
@import "components/user-avatar"; @import "components/user-avatar";
@@ -1,100 +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:not(.button) {
color: $primary-color;
text-decoration: none;
&:not(.button):hover {
text-decoration: underline;
}
}
}
.actions {
text-align: center;
}
&.vetoed {
.content {
h3 {
text-decoration: line-through;
}
}
}
.hint.vetoed {
overflow: auto;
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid rgba(255,255,255,0.2);
font-size: 1.2rem;
.icon {
display: inline-block;
width: 40px;
height: 40px;
float: left;
margin-right: 1rem;
margin-bottom: 1rem;
svg {
width: 100%;
height: auto;
}
}
}
}
// On small screens, hide intro text, contributor and contributions list when
// showing details
@include media-max(small) {
#dashboard.with-details {
#contributions, #stats {
display: none;
}
}
#intro.with-details {
display: none;
}
}
+35 -27
View File
@@ -6,21 +6,6 @@ main section {
} }
} }
.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%;
@@ -34,7 +19,6 @@ ul.contribution-list {
background-color: rgba(255,255,255,0.1); background-color: rgba(255,255,255,0.1);
font-size: 1.2rem; font-size: 1.2rem;
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);
@@ -50,17 +34,11 @@ ul.contribution-list {
opacity: 0.6; opacity: 0.6;
} }
&.selected {
background-color: rgba(255,255,255,0.2);
}
p { p {
align-self: center; align-self: center;
margin: 0; margin: 0;
font-size: inherit;
line-height: 2rem; line-height: 2rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.kredits-amount, &.voting { &.kredits-amount, &.voting {
text-align: right; text-align: right;
@@ -68,8 +46,11 @@ ul.contribution-list {
} }
span { span {
font-size: inherit;
&.title { &.title {
a { a {
font-size: inherit;
text-decoration: none; text-decoration: none;
color: $body-text-color; color: $body-text-color;
} }
@@ -114,12 +95,39 @@ ul.contribution-list {
} }
} }
&.loading { }
@include loading-border-top;
@media (min-width: 550px) {
ul.proposal-list {
li { li {
&:first-of-type { grid-template-columns: auto 10rem;
border-top: none; 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 {
font-size: inherit;
} }
} }
} }
+46 -15
View File
@@ -1,24 +1,59 @@
table.contributor-list { table.contributor-list {
position: relative;
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 1.5rem; 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;
visibility: hidden;
&:not(.visible) {
border-bottom: none;
}
td {
padding: 0 1.2rem;
}
a {
color: $primary-color;
&:hover, &:active {
color: #fff;
}
}
ul {
list-style: none;
display: block;
overflow: hidden;
height: 0;
li {
display: inline;
&+li {
margin-left: 1rem;
}
}
}
&.visible {
height: auto;
visibility: visible;
ul {
height: auto;
}
}
} }
td { td {
@@ -36,24 +71,20 @@ table.contributor-list {
&.kredits { &.kredits {
text-align: right; text-align: right;
.amount { .amount {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 500; font-weight: 500;
} }
.symbol { .symbol {
font-size: 0.8rem; font-size: 0.8rem;
padding-left: 0.2rem; padding-left: 0.2rem;
} }
} }
}
}
&.loading { pre {
@include loading-border-top; line-height: 1rem;
&::before { padding-bottom: 1rem;
position: absolute; }
} }
} }
} }
@@ -1,56 +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 {
margin-top: 2rem;
}
}
.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;
}
}
}
}
+114
View File
@@ -0,0 +1,114 @@
ul.proposal-list {
clear: both;
width: 100%;
list-style: none;
li {
display: grid;
grid-template-columns: auto 5rem;
grid-row-gap: 0.5rem;
padding: 1rem 1.2rem;
background-color: rgba(255,255,255,0.1);
font-size: 1.2rem;
border-bottom: 1px solid rgba(255,255,255,0.2);
&:first-of-type {
border-top: 1px solid rgba(255,255,255,0.2);
}
p {
align-self: center;
margin: 0;
font-size: inherit;
line-height: 2rem;
&.kredits-amount, &.voting {
text-align: right;
}
&.description {
grid-column-start: span 2;
}
&.voting {
grid-column-start: span 2;
}
}
span {
font-size: inherit;
}
.description {
line-height: 1.4em;
font-size: 1rem;
}
.category {
color: $blue;
padding-right: 0.2rem;
&.community { color: $red; }
&.dev { color: $pink; }
&.design { color: $yellow; }
&.docs { color: $green; }
&.ops { color: $purple; }
}
.amount {
font-weight: 500;
}
.symbol {
font-size: 1rem;
font-weight: 500;
padding-left: 0.2rem;
}
.recipient {
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 {
font-size: inherit;
}
}
}
}
@@ -1,17 +0,0 @@
header#topbar section#user-account {
button {
margin-left: 1.5rem;
}
button + button {
margin-left: 0.6rem;
}
@include media-max(small) {
.anonymous {
display: none;
}
}
}
-5
View File
@@ -13,11 +13,6 @@
h1 { h1 {
display: inline-block; display: inline-block;
text-transform: uppercase; text-transform: uppercase;
a {
color: inherit;
text-decoration: none;
}
} }
section#user-account { section#user-account {
-14
View File
@@ -4,18 +4,4 @@ img.avatar {
vertical-align: middle; vertical-align: middle;
margin-right: 0.2rem; margin-right: 0.2rem;
border-radius: 1rem; border-radius: 1rem;
&.medium {
margin: 0;
width: 10rem;
height: 10rem;
border-radius: 5rem;
}
&.large {
margin: 0;
width: 256px;
height: 256px;
border-radius: 128px;
}
} }
+1 -1
View File
@@ -1 +1 @@
<LoadingSpinner /> {{loading-spinner}}
+12 -2
View File
@@ -1,6 +1,16 @@
<header id="topbar"> <header id="topbar">
<h1><LinkTo @route="dashboard">Kosmos Kredits</LinkTo></h1> <h1>Kosmos Kredits</h1>
<TopbarAccountPanel />
<section id="user-account">
{{#if kredits.hasAccounts }}
{{#if kredits.currentUser}}
{{kredits.currentUser.name}}
{{#if kredits.currentUserIsCore}}(core){{/if}}
{{/if}}
{{else}}
Anonymous
{{/if}}
</section>
</header> </header>
{{outlet}} {{outlet}}
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.0 KiB

@@ -1,8 +0,0 @@
<svg 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,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,8 +0,0 @@
<svg width="256" height="256" version="1.1" viewBox="0 0 67.733 67.733" 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(0 -229.27)">
<g transform="matrix(.90278 0 0 .90278 6.5684 141.05)" fill-rule="evenodd">
<path class="fg" d="m32.67 117.1c2.2056 0.1405 3.0528 0.27041 4.1992 0.64479 3.9359 1.2854 6.7418 4.6736 7.3059 8.8223 0.06615 0.48578 0.13996 1.0253 0.16431 1.1986 0.29104 2.0743 0.07488 21.552-0.25003 22.55-0.59082 1.814-1.901 2.7993-4.0537 3.0488-1.3671 0.15849-30.564 0.0701-31.846-0.0963-5.2843-0.68659-8.8934-4.001-9.7398-8.9445-0.38418-2.2439-0.62362-20.748-0.29871-23.084 0.34422-2.4746 1.7264-3.797 4.3212-4.1338 0.98769-0.12832 28.191-0.13361 30.197-6e-3"/>
<path class="fg" d="m61.114 118.11c0.56621 0.4072 0.97816 1.0565 1.1885 1.874 0.19553 0.75909 0.24474 28.04 0.05371 29.747-0.37333 3.3361-2.6305 4.0137-5.5264 1.6584-7.9722-6.4841-9.021-7.5734-9.6795-10.052-0.26008-0.97895-0.33152-10.629-0.08811-11.876 0.38021-1.9457 1.2594-3.1946 3.7383-5.3099 7.4491-6.3564 7.5914-6.4569 9.0522-6.4008 0.70723 0.0272 0.8599 0.0709 1.2613 0.3593"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-15
View File
@@ -1,15 +0,0 @@
<svg width="60px" height="60px" version="1.1" viewBox="0 0 60 60" 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 fill="none" fill-rule="evenodd">
<g transform="translate(-479,-238)" stroke="#fbe468" stroke-linecap="round" stroke-linejoin="round" stroke-width="1">
<g transform="translate(482,244)">
<a transform="translate(0,2)">
<polygon id="Triangle-58" points="0.15321 44 27 0 53.847 44"/>
</a>
<rect x="25" y="13" width="4" height="21"/>
<a transform="translate(0,2)">
<circle cx="27" cy="38" r="2"/>
</a>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 750 B

@@ -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

+1 -3
View File
@@ -6,9 +6,7 @@
</header> </header>
<div class="content"> <div class="content">
<AddContribution @contributors={{this.sortedContributors}} {{add-contribution contributors=sortedContributors save=(action "save")}}
@attributes={{this.model.params}}
@save={{action "save"}} />
</div> </div>
</section> </section>
-15
View File
@@ -1,15 +0,0 @@
<main class="center-column">
<section id="add-contribution">
<header>
<h2>Re-submit contribution #{{this.model.id}}</h2>
</header>
<div class="content">
<AddContribution @attributes={{this.attributes}}
@contributors={{this.sortedContributors}}
@save={{action "save"}} />
</div>
</section>
</main>
+1 -1
View File
@@ -6,7 +6,7 @@
</header> </header>
<div class="content"> <div class="content">
<AddContributor @attributes={{this.attributes}} @save={{action "save"}} /> {{add-contributor attributes=attributes save=(action "save")}}
</div> </div>
</section> </section>
+1 -1
View File
@@ -6,7 +6,7 @@
</header> </header>
<div class="content"> <div class="content">
<AddContributor @save={{action "save"}} /> {{add-contributor save=(action "save")}}
</div> </div>
</section> </section>
-114
View File
@@ -1,114 +0,0 @@
{{#if this.showIntroText}}
<div id="intro" class={{if this.showDetailsPane "with-details"}}>
<h2>
Welcome to the contribution dashboard of the
<a href="https://kosmos.org" target="_blank" rel="noreferrer noopener">Kosmos</a> project!
</h2>
<p>
If you want to learn more about what the numbers mean and how this works,
check out the
<a href="https://wiki.kosmos.org/Kredits" target="_blank" rel="noreferrer noopener">Kredits documentation</a>.
If you want to start earning kredits for your contributions,
<LinkTo @route="signup">create a contributor profile</LinkTo>.
</p>
</div>
{{/if}}
<main id="dashboard" class={{if this.showDetailsPane "with-details"}}>
<div id="stats">
<section id="people">
<header class="with-nav">
<h2>Contributors</h2>
{{#if this.kredits.hasAccounts}}
<nav>
<LinkTo @route="contributors.new" @title="Add contributor profile" class="button small green">add</LinkTo>
</nav>
{{/if}}
</header>
<div class="content">
<ContributorList @contributorList={{this.kreditsToplist}}
@showUnconfirmedKredits={{this.showUnconfirmedKredits}}
@selectedContributorId={{this.selectedContributorId}}
@loading={{this.kredits.syncContributors.isRunning}} />
<p class="stats">
<span class="number">{{await this.kredits.totalKreditsEarned}}</span> kredits confirmed and issued to
<span class="number">{{this.contributorsWithKredits.length}}</span> contributors
</p>
<p class="stats">
<Input id="hide-unnconfirmed-kredits"
@type="checkbox"
@checked={{this.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">
<ChartContributionsByType @contributions={{this.contributions}} />
</div>
</section>
</div>
<div id="contributions">
{{#if this.contributionsUnconfirmed}}
<section id="contributions-unconfirmed">
<header class="with-nav">
<h2>
Latest Contributions
</h2>
<nav>
<button type="button"
onclick={{action "toggleQuickFilterUnconfirmed"}}
class="small {{if this.showQuickFilterUnconfirmed "active"}}">
filter
</button>
{{#if this.kredits.hasAccounts}}
<LinkTo @route="contributions.new"
@title="Submit a contribution"
class="button small green">add</LinkTo>
{{/if}}
</nav>
</header>
<div class="content">
{{!-- TODO: We need a better naming for kredits.hasAccounts --}}
<ContributionList @contributions={{this.contributionsUnconfirmedSorted}}
@vetoContribution={{action "vetoContribution"}}
@contractInteractionEnabled={{this.kredits.hasAccounts}}
@selectedContributionId={{this.selectedContributionId}}
@showQuickFilter={{this.showQuickFilterUnconfirmed}}
@loading={{this.kredits.syncContributions.isRunning}}/>
</div>
</section>
{{/if}}
<section id="contributions-confirmed">
<header class="with-nav">
<h2>Confirmed Contributions</h2>
<nav>
<button type="button"
onclick={{action "toggleQuickFilterConfirmed"}}
class="small {{if this.showQuickFilterConfirmed "active"}}">
filter
</button>
</nav>
</header>
<div class="content">
<ContributionList @contributions={{this.contributionsConfirmedSorted}}
@vetoContribution={{action "vetoContribution"}}
@selectedContributionId={{this.selectedContributionId}}
@showQuickFilter={{this.showQuickFilterConfirmed}} />
</div>
</section>
</div>
<div id="details">
{{outlet}}
</div>
</main>
@@ -1,62 +0,0 @@
<section id="contribution-details" class={{if this.model.vetoed "vetoed"}}>
<header class="with-nav">
<h2>Contribution #{{this.model.id}}</h2>
<nav>
<span class="amount">{{this.model.amount}}</span>&nbsp;
<span class="symbol">₭S</span>
</nav>
</header>
<div class="content">
<p class="who-what-when">
<span class="contributor">
<UserAvatar @contributor={{this.model.contributor}} />
<LinkTo @route="dashboard.contributors.show" @model={{this.model.contributor}}>{{this.model.contributor.name}}</LinkTo>
</span>
contributed
<span class="date" title={{this.model.iso8601Date}}>{{moment-from this.model.iso8601Date}}</span>:
</p>
<h3>{{this.model.description}}</h3>
<p>
Kind: {{this.model.kind}}
<br>Status: {{contribution-status this.model}}
</p>
{{#if this.model.url}}
<p>
<a href={{this.model.url}}
title={{this.model.description}}
class="button"
target="_blank"
rel="noopener noreferrer">
Open URL
</a>
</p>
{{/if}}
{{#if this.model.vetoed}}
<div class="hint vetoed">
<div class="icon">
<IconWarning />
</div>
<p>
This contribution has been vetoed, meaning no
kredits will be issued.
</p>
<p>
<LinkTo @route="contributions.resubmit" @model={{this.model}} class="button small green">Re-submit contribution …</LinkTo>
</p>
</div>
{{/if}}
</div>
<div class="actions">
<p>
<LinkTo @route="contributions.resubmit" @model={{this.model}} class="button small">Copy & edit as new</LinkTo>
{{#if this.model.ipfsHash}}
<a href="{{this.ipfsGatewayUrl}}/{{this.model.ipfsHash}}"
class="button small" target="_blank" rel="noopener noreferrer">
Inspect IPFS data
</a>
{{/if}}
</p>
</div>
</section>
@@ -1,41 +0,0 @@
<section id="contributor-profile">
<header>
<UserAvatar @contributor={{this.model}} @size="medium" />
</header>
<div class="content">
<h2>{{this.model.name}}</h2>
<p class="role">{{this.roleName}}</p>
<ul class="external-accounts">
{{#each this.model.accounts as |account|}}
<li>
<ExternalAccountLink @account={{account}} />
</li>
{{/each}}
{{#if this.model.url}}
<li>
<a href={{this.model.url}} target="_blank" rel="noopener noreferrer" title="Website">
<span class="site">Web</span>
<IconWebGlobe />
</a>
</li>
{{/if}}
</ul>
<div class="actions">
<p>
<LinkTo @route="contributions.new" @query={{hash contributorId=this.model.id}} class="button green">♥ Give kredits</LinkTo>
</p>
</div>
</div>
<div class="actions">
<p>
<a href="https://rinkeby.etherscan.io/address/{{this.model.account}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect Ethereum transactions</a>
{{#if this.model.ipfsHash}}
<a href="{{this.ipfsGatewayUrl}}/{{this.model.ipfsHash}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect IPFS profile</a>
{{/if}}
<LinkTo @route="contributors.edit" @model={{this.model}} class="button small">Edit profile</LinkTo>
</p>
</div>
</section>
+69
View File
@@ -0,0 +1,69 @@
<main id="index">
<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}}
<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>
{{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}}
</div>
</section>
{{/if}}
<section id="contributions-confirmed">
<header>
<h2>Confirmed Contributions</h2>
</header>
<div class="content">
{{contribution-list contributions=contributionsConfirmedSorted
vetoContribution=(action "vetoContribution")}}
</div>
</section>
</div>
</main>
+10
View File
@@ -0,0 +1,10 @@
<section id="add-proposal">
<header>
<h2>Add Proposal</h2>
</header>
<div class="content">
{{add-proposal contributors=minedContributors save=(action "save")}}
</div>
</section>
-5
View File
@@ -1,5 +0,0 @@
<main class="center-column">
<section id="signup">
{{outlet}}
</section>
</main>
-15
View File
@@ -1,15 +0,0 @@
<header>
<h2>Welcome aboard!</h2>
</header>
<div class="content text-lg text-center">
<p>
Congratulations. Your initial profile is now complete.
</p>
<p>
Why not say hi to your fellow contributors
<a href="https://wiki.kosmos.org/Main_Page#Community_.2F_Getting_in_touch_.2F_Getting_involved">in one of our chat rooms</a>?.
</p>
<p class="actions">
<LinkTo @route="dashboard" class="button small">Return to dashboard</LinkTo>
</p>
</div>
-24
View File
@@ -1,24 +0,0 @@
<header>
<h2>Complete your contributor profile</h2>
</header>
<div class="content text-lg">
<p class="mg-bottom-md">
Kredits allow you to take part in project governance, and to earn rewards for
your contributions. For both, you will need an Ethereum wallet/account.
</p>
<form>
<p>
<label>
Ethereum account:<br>
<Input @type="text" @value={{this.ethAddress}} placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" class={{if this.isValidEthAccount "valid" ""}} />
</label>
</p>
</form>
<p class="actions">
<button disabled={{this.signupButtonDisabled}}
onclick={{action "completeSignup"}}
type="button">
Complete my profile
</button>
</p>
</div>
-20
View File
@@ -1,20 +0,0 @@
<header>
<h2>Create your contributor profile</h2>
</header>
<div class="content text-lg">
<p>
You have already contributed to a Kosmos project, or you are interested
in contributing in the near future? Fantastic!
</p>
<p>
In order to earn kredits for your contributions, you will need a
registered contributor profile. The quickest way to register is by
connecting one of the following accounts:
</p>
<p class="actions">
<button onclick={{action "connectGithub"}} class="icon" type="button">
<IconAccountGithubCom />
Connect GitHub
</button>
</p>
</div>
+20
View File
@@ -0,0 +1,20 @@
import { computed } from '@ember/object';
import ethers from 'npm:ethers';
export default function(dependentKey, converterMethod) {
return computed(dependentKey, {
get () {
let value = this.get(dependentKey);
if (value && ethers.utils.BigNumber.isBigNumber(value)) {
return value[converterMethod]();
} else {
return value;
}
},
set (key, value) {
const bnValue = ethers.utils.bigNumberify(value);
this.set(dependentKey, bnValue);
return bnValue[converterMethod]();
}
});
}
+17
View File
@@ -0,0 +1,17 @@
import { computed } from '@ember/object';
import ethers from 'npm:ethers';
import formatKredits from 'kredits-web/utils/format-kredits';
export default function(dependentKey, options = {}) {
return computed(dependentKey, {
get () {
const value = this.get(dependentKey);
return formatKredits(value, options);
},
set (key, value) {
const bnValue = ethers.utils.bigNumberify(value);
this.set(dependentKey, bnValue);
return formatKredits(bnValue, options);
}
});
}
+1 -1
View File
@@ -1,4 +1,4 @@
import ethers from 'ethers'; import ethers from 'npm:ethers';
export default function(value, options = {}) { export default function(value, options = {}) {
let etherValue = ethers.utils.formatEther(value); let etherValue = ethers.utils.formatEther(value);
-21
View File
@@ -1,21 +0,0 @@
export default function processContributionData(data) {
const processed = {}
if (data.confirmedAtBlock && (typeof data.confirmedAtBlock.toNumber === 'function')) {
processed.confirmedAt = data.confirmedAtBlock.toNumber();
} else if (data.confirmedAt !== 'undefined') {
processed.confirmedAt = data.confirmedAt;
}
const otherProperties = [
'id', 'contributorId', 'amount', 'vetoed',
'ipfsHash', 'kind', 'description', 'details',
'url', 'date', 'time', 'pendingTx'
];
otherProperties.forEach(prop => {
processed[prop] = data[prop];
});
return processed;
}
-19
View File
@@ -1,19 +0,0 @@
export default function processContributorData(data) {
const processed = {
id: data.id.toString(),
balance: data.balanceInt,
totalKreditsEarned: data.totalKreditsEarned,
contributionsCount: data.contributionsCount?.toNumber()
}
const otherProperties = [
'account', 'accounts', 'ipfsHash', 'isCore', 'kind', 'name', 'url',
'github_username', 'github_uid', 'wiki_username', 'zoom_display_name'
];
otherProperties.forEach(prop => {
processed[prop] = data[prop];
});
return processed;
}
-20
View File
@@ -1,20 +0,0 @@
{
"schemaVersion": "1.0.0",
"packages": [
{
"name": "ember-cli",
"version": "3.18.0",
"blueprints": [
{
"name": "app",
"outputRepo": "https://github.com/ember-cli/ember-new-output",
"codemodsSource": "ember-app-codemods-manifest@1",
"isBaseBlueprint": true,
"options": [
"--no-welcome"
]
}
]
}
]
}
+10 -15
View File
@@ -12,7 +12,7 @@ module.exports = function(environment) {
EmberENV: { EmberENV: {
FEATURES: { FEATURES: {
// Here you can enable experimental features on an ember canary build // Here you can enable experimental features on an ember canary build
// e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true // e.g. 'with-controller': true
}, },
EXTEND_PROTOTYPES: { EXTEND_PROTOTYPES: {
// Prevent Ember Data from overriding Date.parse. // Prevent Ember Data from overriding Date.parse.
@@ -36,16 +36,11 @@ module.exports = function(environment) {
}, },
web3ProviderUrl: 'https://rinkeby.infura.io/v3/d4f788b7a6584f7db2fc3c268d4d09e9', web3ProviderUrl: 'https://rinkeby.infura.io/v3/d4f788b7a6584f7db2fc3c268d4d09e9',
web3RequiredNetwork: 'rinkeby',
githubConnectUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/connect/github',
githubSignupUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/github',
ipfs: { ipfs: {
host: 'ipfs.kosmos.org', host: 'ipfs.kosmos.org',
port: '5444', port: '5444',
protocol: 'https', protocol: 'https'
gatewayUrl: 'https://ipfs.kosmos.org/ipfs'
} }
}; };
@@ -57,14 +52,10 @@ module.exports = function(environment) {
// ENV.APP.LOG_VIEW_LOOKUPS = true; // ENV.APP.LOG_VIEW_LOOKUPS = true;
ENV.web3ProviderUrl = 'https://rinkeby.infura.io/v3/d4f788b7a6584f7db2fc3c268d4d09e9'; ENV.web3ProviderUrl = 'https://rinkeby.infura.io/v3/d4f788b7a6584f7db2fc3c268d4d09e9';
ENV.githubConnectUrl = 'http://localhost:8888/kredits/signup/connect/github';
ENV.githubSignupUrl = 'http://localhost:8888/kredits/signup/github';
ENV.ipfs = { ENV.ipfs = {
host: 'localhost', host: 'localhost',
port: '5001', port: '5001',
protocol: 'http', protocol: 'http'
gatewayUrl: 'http://localhost:8080/ipfs'
}; };
} }
@@ -80,12 +71,16 @@ module.exports = function(environment) {
ENV.APP.autoboot = false; ENV.APP.autoboot = false;
} }
if (environment === 'production') {
// here you can enable a production-specific feature
ENV.kreditsApmDomain = process.env.KREDITS_APM_DOMAIN || 'open.aragonpm.eth';
}
if (process.env.WEB3_PROVIDER_URL) { if (process.env.WEB3_PROVIDER_URL) {
ENV.web3ProviderUrl = process.env.WEB3_PROVIDER_URL; ENV.web3ProviderUrl = process.env.WEB3_PROVIDER_URL;
ENV.web3RequiredNetwork = null;
} }
if (process.env.KREDITS_DAO_ADDRESS) { if (process.env.KREDITS_KERNEL_ADDRESS) {
ENV.kreditsKernelAddress = process.env.KREDITS_DAO_ADDRESS; ENV.kreditsKernelAddress = process.env.KREDITS_KERNEL_ADDRESS;
} }
if (process.env.KREDITS_APM_DOMAIN) { if (process.env.KREDITS_APM_DOMAIN) {
ENV.kreditsApmDomain = process.env.KREDITS_APM_DOMAIN; ENV.kreditsApmDomain = process.env.KREDITS_APM_DOMAIN;
+1 -4
View File
@@ -1,6 +1,3 @@
{ {
"application-template-wrapper": false, "jquery-integration": true
"default-async-observers": true,
"jquery-integration": false,
"template-only-glimmer-components": true
} }

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