Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 970b390509 | |||
| b8f4040def | |||
| 8fbecd928b |
+1
-1
@@ -5,5 +5,5 @@
|
|||||||
|
|
||||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||||
*/
|
*/
|
||||||
"disableAnalytics": true
|
"disableAnalytics": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
# unconventional js
|
|
||||||
/blueprints/*/files/
|
|
||||||
/vendor/
|
|
||||||
|
|
||||||
# compiled output
|
|
||||||
/dist/
|
|
||||||
/tmp/
|
|
||||||
/release/
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/bower_components/
|
|
||||||
/node_modules/
|
|
||||||
|
|
||||||
# misc
|
|
||||||
/coverage/
|
|
||||||
!.*
|
|
||||||
|
|
||||||
# ember-try
|
|
||||||
/.node_modules.ember-try/
|
|
||||||
/bower.json.ember-try
|
|
||||||
/package.json.ember-try
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
parserOptions: {
|
|
||||||
ecmaVersion: 2018,
|
|
||||||
sourceType: 'module'
|
|
||||||
},
|
|
||||||
globals: {
|
|
||||||
console: true
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
'ember'
|
|
||||||
],
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:ember/recommended'
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
browser: true
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'ember/avoid-leaking-state-in-ember-objects': 'warn',
|
|
||||||
'no-console': 'off'
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
// node files
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'.eslintrc.js',
|
|
||||||
'.template-lintrc.js',
|
|
||||||
'ember-cli-build.js',
|
|
||||||
'testem.js',
|
|
||||||
'blueprints/*/index.js',
|
|
||||||
'config/**/*.js',
|
|
||||||
'lib/*/index.js',
|
|
||||||
'server/**/*.js'
|
|
||||||
],
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'script',
|
|
||||||
ecmaVersion: 2015
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: false,
|
|
||||||
node: true
|
|
||||||
},
|
|
||||||
plugins: ['node'],
|
|
||||||
rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {
|
|
||||||
// add your custom rules and overrides for node files here
|
|
||||||
|
|
||||||
// this can be removed once the following is fixed
|
|
||||||
// https://github.com/mysticatea/eslint-plugin-node/issues/77
|
|
||||||
'node/no-unpublished-require': 'off'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
template: |
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
$CHANGES
|
|
||||||
+7
-16
@@ -1,26 +1,17 @@
|
|||||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
/dist/
|
/dist
|
||||||
/tmp/
|
/tmp
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/bower_components/
|
/node_modules
|
||||||
/node_modules/
|
/bower_components
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
/.env*
|
|
||||||
/.pnp*
|
|
||||||
/.sass-cache
|
/.sass-cache
|
||||||
/connect.lock
|
/connect.lock
|
||||||
/coverage/
|
/coverage/*
|
||||||
/libpeerconnection.log
|
/libpeerconnection.log
|
||||||
/npm-debug.log*
|
npm-debug.log*
|
||||||
/testem.log
|
testem.log
|
||||||
/yarn-error.log
|
|
||||||
.tm_properties
|
|
||||||
|
|
||||||
# ember-try
|
|
||||||
/.node_modules.ember-try/
|
|
||||||
/bower.json.ember-try
|
|
||||||
/package.json.ember-try
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"predef": [
|
||||||
|
"document",
|
||||||
|
"window",
|
||||||
|
"-Promise"
|
||||||
|
],
|
||||||
|
"browser": true,
|
||||||
|
"boss": true,
|
||||||
|
"curly": true,
|
||||||
|
"debug": false,
|
||||||
|
"devel": true,
|
||||||
|
"eqeqeq": true,
|
||||||
|
"evil": true,
|
||||||
|
"forin": false,
|
||||||
|
"immed": false,
|
||||||
|
"laxbreak": false,
|
||||||
|
"newcap": true,
|
||||||
|
"noarg": true,
|
||||||
|
"noempty": false,
|
||||||
|
"nonew": false,
|
||||||
|
"nomen": false,
|
||||||
|
"onevar": false,
|
||||||
|
"plusplus": false,
|
||||||
|
"regexp": false,
|
||||||
|
"undef": true,
|
||||||
|
"sub": true,
|
||||||
|
"strict": false,
|
||||||
|
"white": false,
|
||||||
|
"eqnull": true,
|
||||||
|
"esversion": 6,
|
||||||
|
"unused": true
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
extends: 'recommended',
|
|
||||||
|
|
||||||
rules: {
|
|
||||||
'simple-unless': false,
|
|
||||||
'no-nested-interactive': false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
+12
-16
@@ -1,29 +1,25 @@
|
|||||||
---
|
---
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "10"
|
- "6"
|
||||||
|
|
||||||
sudo: false
|
sudo: false
|
||||||
dist: xenial
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email:
|
|
||||||
on_success: never
|
|
||||||
on_failure: always
|
|
||||||
|
|
||||||
addons:
|
|
||||||
chrome: stable
|
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.npm
|
- $HOME/.npm
|
||||||
|
- $HOME/.cache # includes bowers cache
|
||||||
|
|
||||||
env:
|
before_install:
|
||||||
global:
|
- npm config set spin false
|
||||||
# See https://git.io/vdao3 for details.
|
- npm install -g bower
|
||||||
- JOBS=1
|
- bower --version
|
||||||
|
- npm install phantomjs-prebuilt
|
||||||
|
- node_modules/phantomjs-prebuilt/bin/phantomjs --version
|
||||||
|
|
||||||
|
install:
|
||||||
|
- npm install
|
||||||
|
- bower install
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- npm run lint:hbs
|
|
||||||
- npm run lint:js
|
|
||||||
- npm test
|
- npm test
|
||||||
|
|||||||
@@ -10,24 +10,22 @@ A short introduction of this app could easily go here.
|
|||||||
You will need the following things properly installed on your computer.
|
You will need the following things properly installed on your computer.
|
||||||
|
|
||||||
* [Git](https://git-scm.com/)
|
* [Git](https://git-scm.com/)
|
||||||
* [Node.js](https://nodejs.org/) (with npm)
|
* [Node.js](https://nodejs.org/) (with NPM)
|
||||||
|
* [Bower](https://bower.io/)
|
||||||
* [Ember CLI](https://ember-cli.com/)
|
* [Ember CLI](https://ember-cli.com/)
|
||||||
* [Google Chrome](https://google.com/chrome/)
|
* [PhantomJS](http://phantomjs.org/)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
* `git clone git@github.com:67P/kredits-web.git` this repository
|
* `git clone <repository-url>` this repository
|
||||||
* `cd kredits-web`
|
* `cd kredits-web`
|
||||||
* `npm install`
|
* `npm install`
|
||||||
|
* `bower install`
|
||||||
|
|
||||||
## Running / Development
|
## Running / Development
|
||||||
|
|
||||||
|
* `ember serve`
|
||||||
* `ember serve` - by default kredits-web connects to the Ethreum Kovan network
|
|
||||||
* Visit your app at [http://localhost:4200](http://localhost:4200).
|
* Visit your app at [http://localhost:4200](http://localhost:4200).
|
||||||
* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
|
|
||||||
|
|
||||||
See [working with locally deployed contracts](https://github.com/67P/kredits-web#working-with-locally-deployed-contracts) for details on how to develop with locally deployed contracts.
|
|
||||||
|
|
||||||
### Code Generators
|
### Code Generators
|
||||||
|
|
||||||
@@ -38,12 +36,6 @@ Make use of the many generators for code, try `ember help generate` for more det
|
|||||||
* `ember test`
|
* `ember test`
|
||||||
* `ember test --server`
|
* `ember test --server`
|
||||||
|
|
||||||
### Linting
|
|
||||||
|
|
||||||
* `npm run lint:hbs`
|
|
||||||
* `npm run lint:js`
|
|
||||||
* `npm run lint:js -- --fix`
|
|
||||||
|
|
||||||
### Building
|
### Building
|
||||||
|
|
||||||
* `ember build` (development)
|
* `ember build` (development)
|
||||||
@@ -51,72 +43,35 @@ Make use of the many generators for code, try `ember help generate` for more det
|
|||||||
|
|
||||||
### Deploying
|
### Deploying
|
||||||
|
|
||||||
_(You need collaborator permissions on the 5apps Deploy project.)_
|
Specify what it takes to deploy your app.
|
||||||
|
|
||||||
`npm run deploy`
|
|
||||||
|
|
||||||
## Working with locally deployed contracts
|
## Working with locally deployed contracts
|
||||||
|
|
||||||
The smart contracts and their JavaScript wrapper library are developed in the
|
For development you might want to run your own development chain with your own development contracts.
|
||||||
[kredits-contracts](https://github.com/67P/kredits-contracts) repo/package.
|
The kredits-contracts helper scripts are available in kredits-web. See [kredits-contracts](https://github.com/67P/kredits-contracts) for more information.
|
||||||
|
|
||||||
You can run `kredits-web` on your machine, against a local, simulated Ethereum
|
Start your parity node with a KreditsChain:
|
||||||
network, provided e.g. by [ganache](http://truffleframework.com/ganache/).
|
|
||||||
|
|
||||||
[kredits-contracts](https://github.com/67P/kredits-contracts) holds all the tools
|
npm run kredits-parity
|
||||||
to start and set up such a simulated network, as well as to deploy smart
|
|
||||||
contracts to it.
|
|
||||||
|
|
||||||
These are the basic steps to get up and running:
|
Deploy the contracts to your local chain:
|
||||||
|
|
||||||
#### 1. IPFS
|
npm run kredits-deploy
|
||||||
|
|
||||||
Run a local IPFS deamon.
|
Run kredits-web with your localy deployed contracts:
|
||||||
|
|
||||||
* Make sure CORS headers are configured. See [IPFS](#ipfs) for more info.
|
KREDITS_CONTRACT_ADDR=`kredits-inspect -c Kredits -r -w address` \
|
||||||
* `ipfs daemon`
|
TOKEN_CONTRACT_ADDR=`kredits-inspect -c Token -r -w address` \
|
||||||
|
npm start
|
||||||
|
|
||||||
#### 2. kredits-contracts
|
You need to have `./node_modules/.bin` in your path for this to work. Otherwise simply copy the contract addresses and set the environment variables manually.
|
||||||
|
|
||||||
Get your local Ethereum development node running. (See [kredits-contracts README](https://github.com/67P/kredits-contracts)
|
|
||||||
for details.
|
|
||||||
|
|
||||||
* Clone [kredits-contracts](https://github.com/67P/kredits-contracts)
|
|
||||||
* `npm install`
|
|
||||||
* `npm run devchain` - runs a local development chain
|
|
||||||
* `npm run bootstrap` - bootstrap runs deploys all the contracts
|
|
||||||
* `npm link` - make the `kredits-contracts` module linkable as `kredits-contracts` on your machine (currently broken :( )
|
|
||||||
|
|
||||||
#### 3. kredits-web
|
|
||||||
|
|
||||||
With IPFS and Ethereum/ganache running, you can now start this Ember app.
|
|
||||||
|
|
||||||
* `npm link kredits-contracts` - link the local `kredits-contracts` package
|
|
||||||
* `npm run start:local` - WEB3_PROVIDER_URL=http://localhost:7545 must be set for local settings
|
|
||||||
|
|
||||||
If you restart the devchain your network ID changes. So kredits-contracts does not find the correct DAO address.
|
|
||||||
If that is the case the DAO address must be provided as `KREDITS_DAO_ADDRESS` environment variable.
|
|
||||||
|
|
||||||
* `npm run dao:address` in the kredits-contracts repo to find your address
|
|
||||||
* `KREDITS_DAO_ADDRESS=<0xYOURADDRESS> npm run start:local`
|
|
||||||
|
|
||||||
#### 4. Metamask network
|
|
||||||
|
|
||||||
Switch the network in Metamask to "Custom RPC" with the RPC URL `http://localhost:7545`.
|
|
||||||
|
|
||||||
#### IPFS
|
|
||||||
|
|
||||||
Install IPFS with your favorite package manager and run
|
|
||||||
|
|
||||||
ipfs init (on initial installation)
|
|
||||||
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["localhost:4200"]'
|
|
||||||
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]'
|
|
||||||
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
|
|
||||||
ipfs config Addresses.Gateway /ip4/127.0.0.1/tcp/8080
|
|
||||||
|
|
||||||
## Further Reading / Useful Links
|
## Further Reading / Useful Links
|
||||||
|
|
||||||
* [ember.js](https://emberjs.com/)
|
* [ember.js](http://emberjs.com/)
|
||||||
* [ember-cli](https://ember-cli.com/)
|
* [ember-cli](https://ember-cli.com/)
|
||||||
* Development Browser Extensions
|
* Development Browser Extensions
|
||||||
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
|
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
|
||||||
|
|||||||
+6
-2
@@ -1,9 +1,13 @@
|
|||||||
import Application from '@ember/application';
|
import Ember from 'ember';
|
||||||
import Resolver from './resolver';
|
import Resolver from './resolver';
|
||||||
import loadInitializers from 'ember-load-initializers';
|
import loadInitializers from 'ember-load-initializers';
|
||||||
import config from './config/environment';
|
import config from './config/environment';
|
||||||
|
|
||||||
const App = Application.extend({
|
let App;
|
||||||
|
|
||||||
|
Ember.MODEL_FACTORY_INJECTIONS = true;
|
||||||
|
|
||||||
|
App = Ember.Application.extend({
|
||||||
modulePrefix: config.modulePrefix,
|
modulePrefix: config.modulePrefix,
|
||||||
podModulePrefix: config.podModulePrefix,
|
podModulePrefix: config.podModulePrefix,
|
||||||
Resolver
|
Resolver
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import { and, notEmpty } from '@ember/object/computed';
|
|
||||||
import { isEmpty } from '@ember/utils';
|
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
|
|
||||||
attributes: null,
|
|
||||||
contributors: Object.freeze([]),
|
|
||||||
|
|
||||||
isValidContributor: notEmpty('contributorId'),
|
|
||||||
isValidKind: notEmpty('kind'),
|
|
||||||
isValidAmount: computed('amount', function() {
|
|
||||||
return parseInt(this.amount, 10) > 0;
|
|
||||||
}),
|
|
||||||
isValidDescription: notEmpty('description'),
|
|
||||||
isValidUrl: notEmpty('url'),
|
|
||||||
isValid: and('isValidContributor',
|
|
||||||
'isValidKind',
|
|
||||||
'isValidAmount',
|
|
||||||
'isValidDescription'),
|
|
||||||
|
|
||||||
init () {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.set('defaultDate', moment().startOf('hour').toDate());
|
|
||||||
|
|
||||||
// Default attributes used by reset
|
|
||||||
if (isEmpty(this.attributes)) {
|
|
||||||
this.set('attributes', {
|
|
||||||
contributorId: null,
|
|
||||||
kind: null,
|
|
||||||
date: this.defaultDate,
|
|
||||||
amount: null,
|
|
||||||
description: null,
|
|
||||||
url: null,
|
|
||||||
details: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reset();
|
|
||||||
},
|
|
||||||
|
|
||||||
reset () {
|
|
||||||
this.setProperties(this.attributes);
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
submit () {
|
|
||||||
if (!this.isValid) {
|
|
||||||
alert('Invalid data. Please review and try again.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attributes = this.getProperties(Object.keys(this.attributes));
|
|
||||||
|
|
||||||
let dateInput = (attributes.date instanceof Array) ?
|
|
||||||
attributes.date[0] : attributes.date;
|
|
||||||
|
|
||||||
const [ date, time ] = dateInput.toISOString().split('T');
|
|
||||||
[ attributes.date, attributes.time ] = [ date, time ];
|
|
||||||
|
|
||||||
this.set('inProgress', true);
|
|
||||||
|
|
||||||
this.save(attributes)
|
|
||||||
.then((/*contribution*/) => {
|
|
||||||
this.reset();
|
|
||||||
}, err => {
|
|
||||||
console.warn(err);
|
|
||||||
window.alert('Something went wrong. Check the browser console for details.');
|
|
||||||
})
|
|
||||||
.finally(() => this.set('inProgress', false));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
<form {{action "submit" on="submit"}}>
|
|
||||||
<label>
|
|
||||||
<p class="label">Contributor:</p>
|
|
||||||
<p>
|
|
||||||
<select required onchange={{action (mut contributorId) value="target.value"}}>
|
|
||||||
<option value="" selected disabled hidden></option>
|
|
||||||
{{#each contributors as |contributor|}}
|
|
||||||
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.name}}</option>
|
|
||||||
{{/each}}
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<p class="label">Kind:</p>
|
|
||||||
<p>
|
|
||||||
<select required onchange={{action (mut kind) value="target.value"}}>
|
|
||||||
<option value="" selected disabled hidden></option>
|
|
||||||
<option value="community" selected={{eq kind "community"}}>Community</option>
|
|
||||||
<option value="design" selected={{eq kind "design"}}>Design</option>
|
|
||||||
<option value="dev" selected={{eq kind "dev"}}>Development</option>
|
|
||||||
<option value="docs" selected={{eq kind "docs"}}>Documentation</option>
|
|
||||||
<option value="ops" selected={{eq kind "ops"}}>IT Operations</option>
|
|
||||||
<option value="special" selected={{eq kind "special"}}>Special</option>
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<p class="label">Date:</p>
|
|
||||||
<p>
|
|
||||||
{{ember-flatpickr
|
|
||||||
date=date
|
|
||||||
defaultDate=defaultDate
|
|
||||||
maxDate=defaultDate
|
|
||||||
enableTime=true
|
|
||||||
time_24hr=true
|
|
||||||
onChange=(action (mut date))
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<p class="label">Amount:</p>
|
|
||||||
<p>
|
|
||||||
{{input type="text"
|
|
||||||
placeholder="500"
|
|
||||||
value=amount
|
|
||||||
class=(if isValidAmount "valid" "")}}
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<p class="label">Description:</p>
|
|
||||||
<p>
|
|
||||||
{{input type="text"
|
|
||||||
value=description
|
|
||||||
class=(if isValidDescription "valid" "")}}
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<p class="label">URL (optional):</p>
|
|
||||||
<p>
|
|
||||||
{{input type="text"
|
|
||||||
value=url
|
|
||||||
class=(if isValidUrl "valid" "")}}
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{{#if details}}
|
|
||||||
<label>
|
|
||||||
<p class="label">Details:</p>
|
|
||||||
<p>
|
|
||||||
<pre>
|
|
||||||
{{details}}
|
|
||||||
</pre>
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<p class="actions">
|
|
||||||
{{input type="submit"
|
|
||||||
disabled=inProgress
|
|
||||||
value=(if inProgress "Processing" "Save")}}
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
@@ -1,81 +1,79 @@
|
|||||||
import Component from '@ember/component';
|
import Ember from 'ember';
|
||||||
import { computed } from '@ember/object';
|
import Contributor from 'kredits-web/models/contributor';
|
||||||
import { and, notEmpty } from '@ember/object/computed';
|
|
||||||
import { inject as service } from '@ember/service';
|
const {
|
||||||
import { isPresent } from '@ember/utils';
|
Component,
|
||||||
import { isAddress } from 'web3-utils';
|
isPresent,
|
||||||
|
inject: {
|
||||||
|
service
|
||||||
|
},
|
||||||
|
computed
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
|
||||||
kredits: service(),
|
kredits: service(),
|
||||||
|
|
||||||
attributes: null,
|
newContributor: null,
|
||||||
|
inProgress: false,
|
||||||
|
|
||||||
isValidAccount: computed('account', function() {
|
isValidAddress: function() {
|
||||||
return isAddress(this.account);
|
return this.get('kredits.web3')
|
||||||
}),
|
.isAddress(this.get('newContributor.address'));
|
||||||
|
}.property('kredits.web3', 'newContributor.address'),
|
||||||
|
|
||||||
isValidName: notEmpty('name'),
|
isValidName: function() {
|
||||||
isValidURL: notEmpty('url'),
|
return isPresent(this.get('newContributor.name'));
|
||||||
isValidGithubUID: notEmpty('github_uid'),
|
}.property('newContributor.name'),
|
||||||
isValidGithubUsername: notEmpty('github_username'),
|
|
||||||
isValidGiteaUsername: notEmpty('gitea_username'),
|
|
||||||
isValidWikiUsername: notEmpty('wiki_username'),
|
|
||||||
|
|
||||||
isValid: and(
|
isValidURL: function() {
|
||||||
'isValidAccount',
|
return isPresent(this.get('newContributor.url'));
|
||||||
|
}.property('newContributor.url'),
|
||||||
|
|
||||||
|
isValidGithubUID: function() {
|
||||||
|
return isPresent(this.get('newContributor.github_uid'));
|
||||||
|
}.property('newContributor.github_uid'),
|
||||||
|
|
||||||
|
isValidGithubUsername: function() {
|
||||||
|
return isPresent(this.get('newContributor.github_username'));
|
||||||
|
}.property('newContributor.github_username'),
|
||||||
|
|
||||||
|
isValidWikiUsername: function() {
|
||||||
|
return isPresent(this.get('newContributor.wiki_username'));
|
||||||
|
}.property('newContributor.wiki_username'),
|
||||||
|
|
||||||
|
isValid: computed.and(
|
||||||
|
'isValidAddress',
|
||||||
'isValidName',
|
'isValidName',
|
||||||
'isValidGithubUID'
|
'isValidGithubUID'
|
||||||
),
|
),
|
||||||
|
|
||||||
inProgress: false,
|
|
||||||
|
|
||||||
init () {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.setDefaultAttributes();
|
|
||||||
this.reset();
|
|
||||||
},
|
|
||||||
|
|
||||||
setDefaultAttributes () {
|
|
||||||
if (isPresent(this.attributes)) { return; }
|
|
||||||
|
|
||||||
this.set('attributes', {
|
|
||||||
account: null,
|
|
||||||
name: null,
|
|
||||||
kind: 'person',
|
|
||||||
url: null,
|
|
||||||
github_username: null,
|
|
||||||
github_uid: null,
|
|
||||||
gitea_username: null,
|
|
||||||
wiki_username: null
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
reset: function() {
|
reset: function() {
|
||||||
this.setProperties(this.attributes);
|
this.setProperties({
|
||||||
|
newContributor: Contributor.create({ kind: 'person' }),
|
||||||
|
inProgress: false
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
submit () {
|
save() {
|
||||||
if (!this.isValid) {
|
if (!this.get('contractInteractionEnabled')) {
|
||||||
alert('Invalid data. Please review and try again.');
|
alert('Only core team members can add new contributors. Please ask someone to set you up.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributes = Object.keys(this.attributes);
|
if (this.get('isValid')) {
|
||||||
const contributor = this.getProperties(attributes);
|
this.set('inProgress', true);
|
||||||
|
|
||||||
this.set('inProgress', true);
|
this.get('kredits').addContributor(this.get('newContributor')).then(contributor => {
|
||||||
|
this.reset();
|
||||||
this.save(contributor).then(() => {
|
this.get('contributors').pushObject(contributor);
|
||||||
this.reset();
|
window.scroll(0,0);
|
||||||
}).catch(err => {
|
});
|
||||||
console.warn(err);
|
} else {
|
||||||
window.alert('Something went wrong. Please check the browser console.');
|
alert('Invalid data. Please review and try again.');
|
||||||
}).finally(() => {
|
}
|
||||||
this.set('inProgress', false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,59 @@
|
|||||||
<form {{action "submit" on="submit"}}>
|
<form {{action "save" on="submit"}}>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-account">Ethereum account</label>
|
{{input type="checkbox" name="is-core" id="is-core" checked=newContributor.isCore}}
|
||||||
{{input name="account" id="c-account"
|
<label for="is-core" class="checkbox">
|
||||||
type="text" value=account
|
Core team member (can add contributors)
|
||||||
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
|
</label>
|
||||||
class=(if isValidAccount "valid" "")}}
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-kind">Kind</label>
|
{{input name="address"
|
||||||
<select required onchange={{action (mut kind) value="target.value"}} id="c-kind">
|
type="text"
|
||||||
<option value="person" selected={{eq kind "person"}}>Person</option>
|
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
|
||||||
<option value="organization" selected={{eq kind "organization"}}>Organization</option>
|
value=newContributor.address
|
||||||
|
class=(if isValidAddress 'valid' '')}}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<select required onchange={{action (mut newContributor.kind) value="target.value"}}>
|
||||||
|
<option value="person" selected={{eq newContributor.kind "person"}}>Person</option>
|
||||||
|
<option value="organization" selected={{eq newContributor.kind "organization"}}>Organization</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-name">Name</label>
|
{{input name="name"
|
||||||
{{input name="name" type="text" value=name placeholder="Zero Cool"
|
type="text"
|
||||||
class=(if isValidName "valid" "") id="c-name"}}
|
placeholder="Name"
|
||||||
|
value=newContributor.name
|
||||||
|
class=(if isValidName 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-url">URL</label>
|
{{input name="url"
|
||||||
{{input name="url" type="text" value=url placeholder="http://zerocool.bit"
|
type="text"
|
||||||
class=(if isValidURL "valid" "") id="c-url"}}
|
placeholder="URL"
|
||||||
|
value=newContributor.url
|
||||||
|
class=(if isValidURL 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-github-uid">GitHub UID</label>
|
{{input name="github_uid"
|
||||||
{{input name="github_uid" type="text" value=github_uid placeholder="2342"
|
type="text"
|
||||||
class=(if isValidGithubUID "valid" "") id="c-github-uid"}}
|
placeholder="GitHub UID (123)"
|
||||||
|
value=newContributor.github_uid
|
||||||
|
class=(if isValidGithubUID 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-github-username">GitHub username</label>
|
{{input name="github_username"
|
||||||
{{input name="github_username" type="text" value=github_username placeholder="zerocool"
|
type="text"
|
||||||
class=(if isValidGithubUsername "valid" "") id="c-github-username"}}
|
placeholder="GitHub username"
|
||||||
|
value=newContributor.github_username
|
||||||
|
class=(if isValidGithubUsername 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-gitea-username">Gitea username</label>
|
{{input name="wiki_username"
|
||||||
{{input name="gitea_username" type="text" value=gitea_username placeholder="zerocool"
|
type="text"
|
||||||
class=(if isValidGiteaUsername "valid" "") id="c-gitea-username"}}
|
placeholder="Wiki Username"
|
||||||
</p>
|
value=newContributor.wiki_username
|
||||||
<p>
|
class=(if isValidWikiUsername 'valid' '')}}
|
||||||
<label for="c-wiki-username">Wiki username</label>
|
|
||||||
{{input name="wiki_username" type="text" value=wiki_username placeholder="ZeroCool"
|
|
||||||
class=(if isValidWikiUsername "valid" "") id="c-wiki-username"}}
|
|
||||||
</p>
|
</p>
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
{{input type="submit" disabled=inProgress
|
{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
|
||||||
value=(if inProgress "Processing" "Save")}}
|
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,63 +1,65 @@
|
|||||||
import Component from '@ember/component';
|
import Ember from 'ember';
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import { and, notEmpty } from '@ember/object/computed';
|
const {
|
||||||
|
Component,
|
||||||
|
isPresent,
|
||||||
|
inject: {
|
||||||
|
service
|
||||||
|
},
|
||||||
|
computed
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
|
||||||
attributes: null,
|
kredits: service(),
|
||||||
contributors: Object.freeze([]),
|
|
||||||
|
|
||||||
isValidContributor: notEmpty('contributorId'),
|
proposal: null,
|
||||||
isValidAmount: computed('amount', function() {
|
contributors: null,
|
||||||
return parseInt(this.amount, 10) > 0;
|
inProgress: false,
|
||||||
|
|
||||||
|
isValidRecipient: computed('proposal.recipientAddress', function() {
|
||||||
|
return this.get('kredits.web3').isAddress(this.get('proposal.recipientAddress'));
|
||||||
}),
|
}),
|
||||||
isValidDescription: notEmpty('description'),
|
|
||||||
isValidUrl: notEmpty('url'),
|
|
||||||
isValid: and('isValidContributor',
|
|
||||||
'isValidAmount',
|
|
||||||
'isValidDescription'),
|
|
||||||
|
|
||||||
init () {
|
isValidAmount: computed('proposal.amount', function() {
|
||||||
this._super(...arguments);
|
return parseInt(this.get('proposal.amount'), 10) > 0;
|
||||||
|
}),
|
||||||
|
|
||||||
// Default attributes used by reset
|
isValidUrl: computed('proposal.url', function() {
|
||||||
this.set('attributes', {
|
return isPresent(this.get('proposal.url'));
|
||||||
contributorId: null,
|
}),
|
||||||
kind: 'community',
|
|
||||||
amount: null,
|
|
||||||
description: null,
|
|
||||||
url: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
didInsertElement() {
|
isValidDescription: computed('proposal.description', function() {
|
||||||
this._super(...arguments);
|
return isPresent(this.get('proposal.description'));
|
||||||
this.reset();
|
}),
|
||||||
},
|
|
||||||
|
|
||||||
reset: function() {
|
isValid: computed.and('isValidRecipient',
|
||||||
this.setProperties(this.attributes);
|
'isValidAmount',
|
||||||
},
|
'isValidDescription'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
submit() {
|
save() {
|
||||||
if (!this.isValid) {
|
if (! this.get('isValid')) {
|
||||||
alert('Invalid data. Please review and try again.');
|
alert('Invalid data. Please review and try again.');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
this.set('inProgress', true);
|
||||||
|
let proposal = this.get('proposal');
|
||||||
|
|
||||||
let attributes = Object.keys(this.attributes);
|
// Set the recipient's IPFS profile hash so it can be used in the
|
||||||
let proposal = this.getProperties(attributes);
|
// contribution object (which is to be stored in IPFS as well)
|
||||||
let saved = this.save(proposal);
|
let contributor = this.get('contributors').findBy('address', proposal.get('recipientAddress'));
|
||||||
|
proposal.set('recipientProfile', contributor.get('ipfsHash'));
|
||||||
|
|
||||||
// The promise handles inProgress
|
this.get('kredits').addProposal(proposal)
|
||||||
this.set('inProgress', saved);
|
.then(() => {
|
||||||
|
this.attrs.onSave();
|
||||||
saved.then(() => {
|
}).catch((error) => {
|
||||||
this.reset();
|
Ember.Logger.error('[add-proposal] error creating the proposal', error);
|
||||||
window.scroll(0,0);
|
alert('Something went wrong.');
|
||||||
window.alert('Proposal added.');
|
}).finally(() => {
|
||||||
});
|
this.set('inProgress', false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,42 @@
|
|||||||
<form {{action "submit" on="submit"}}>
|
<form {{action "save" on="submit"}}>
|
||||||
<p>
|
<p>
|
||||||
<select required onchange={{action (mut contributorId) value="target.value"}}>
|
<select required onchange={{action (mut proposal.recipientAddress) value="target.value"}}>
|
||||||
<option value="" selected disabled hidden>Contributor</option>
|
<option value="" selected disabled hidden>Contributor</option>
|
||||||
{{#each contributors as |contributor|}}
|
{{#each contributors as |contributor|}}
|
||||||
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.github_username}}</option>
|
<option value={{contributor.address}} selected={{eq proposal.recipientAddress contributor.address}}>{{contributor.github_username}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<select required onchange={{action (mut kind) value="target.value"}}>
|
<select required onchange={{action (mut proposal.kind) value="target.value"}}>
|
||||||
<option value="community" selected={{eq kind "community"}}>Community</option>
|
<option value="community" selected={{eq proposal.kind "community"}}>Community</option>
|
||||||
<option value="design" selected={{eq kind "design"}}>Design</option>
|
<option value="design" selected={{eq proposal.kind "design"}}>Design</option>
|
||||||
<option value="dev" selected={{eq kind "dev"}}>Development</option>
|
<option value="dev" selected={{eq proposal.kind "dev"}}>Development</option>
|
||||||
<option value="docs" selected={{eq kind "docs"}}>Documentation</option>
|
<option value="docs" selected={{eq proposal.kind "docs"}}>Documentation</option>
|
||||||
<option value="ops" selected={{eq kind "ops"}}>IT Operations</option>
|
<option value="ops" selected={{eq proposal.kind "ops"}}>IT Operations</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
value=amount
|
value=proposal.amount
|
||||||
class=(if isValidAmount "valid" "")}}
|
class=(if isValidAmount 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
value=description
|
value=proposal.description
|
||||||
class=(if isValidDescription "valid" "")}}
|
class=(if isValidDescription 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
placeholder="URL (optional)"
|
placeholder="URL (optional)"
|
||||||
value=url
|
value=proposal.url
|
||||||
class=(if isValidUrl "valid" "")}}
|
class=(if isValidUrl 'valid' '')}}
|
||||||
</p>
|
</p>
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
{{input type="submit"
|
{{input type="submit" value=(if inProgress 'Processing' 'Save') disabled=inProgress}}
|
||||||
disabled=(is-pending inProgress)
|
{{#link-to 'index'}}Back{{/link-to}}
|
||||||
value=(if (is-pending inProgress) "Processing" "Save")}}
|
|
||||||
{{#link-to "index"}}Back{{/link-to}}
|
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
|
|
||||||
let categoryColors = {
|
|
||||||
community: "#fb6868",
|
|
||||||
design: "#fbe468",
|
|
||||||
dev: "#e068fb",
|
|
||||||
docs: "#97fb68",
|
|
||||||
ops: "#8f68fb",
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
|
|
||||||
contributions: null,
|
|
||||||
chartOptions: Object.freeze({
|
|
||||||
legend: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
chartData: computed('contributions', function() {
|
|
||||||
let kredits = this.contributions
|
|
||||||
.filterBy('vetoed', false)
|
|
||||||
.map(c => {
|
|
||||||
return { kind: c.kind, amount: c.amount }
|
|
||||||
})
|
|
||||||
.reduce(function (kinds, c) {
|
|
||||||
if (c.kind in kinds) {
|
|
||||||
kinds[c.kind] = kinds[c.kind] + c.amount
|
|
||||||
} else {
|
|
||||||
kinds[c.kind] = c.amount;
|
|
||||||
}
|
|
||||||
return kinds;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return {
|
|
||||||
datasets: [{
|
|
||||||
data: [
|
|
||||||
kredits['community'],
|
|
||||||
kredits['design'],
|
|
||||||
kredits['dev'],
|
|
||||||
kredits['ops'],
|
|
||||||
kredits['docs'],
|
|
||||||
],
|
|
||||||
borderColor: [
|
|
||||||
categoryColors.community,
|
|
||||||
categoryColors.design,
|
|
||||||
categoryColors.dev,
|
|
||||||
categoryColors.ops,
|
|
||||||
categoryColors.docs,
|
|
||||||
],
|
|
||||||
borderWidth: 1
|
|
||||||
}],
|
|
||||||
labels: [
|
|
||||||
'Community',
|
|
||||||
'Design',
|
|
||||||
'Development',
|
|
||||||
'Operations & Infrastructure',
|
|
||||||
'Documentation'
|
|
||||||
],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<div class="chart">
|
|
||||||
{{ember-chart type="doughnut"
|
|
||||||
data=chartData
|
|
||||||
options=chartOptions
|
|
||||||
width=200
|
|
||||||
height=200}}
|
|
||||||
</div>
|
|
||||||
@@ -1,72 +1,16 @@
|
|||||||
import Component from '@ember/component';
|
import Ember from 'ember';
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import { sort } from '@ember/object/computed';
|
|
||||||
import { isPresent } from '@ember/utils';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
|
|
||||||
export default Component.extend({
|
export default Ember.Component.extend({
|
||||||
|
|
||||||
router: service(),
|
tagName: 'ul',
|
||||||
|
classNames: ['contribution-list'],
|
||||||
|
|
||||||
tagName: 'div',
|
contributionsWithContributors: function() {
|
||||||
classNames: ['contributions'],
|
return this.get('contributions').map((c) => {
|
||||||
|
c.set('contributor', this.get('contributors')
|
||||||
selectedContribution: null,
|
.findBy('address', c.get('recipientAddress')));
|
||||||
|
return c;
|
||||||
showQuickFilter: false,
|
|
||||||
hideSmallContributions: false,
|
|
||||||
contributorId: null,
|
|
||||||
contributionKind: null,
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
contributorsSorting: Object.freeze(['name:asc']),
|
|
||||||
contributors: sort('kredits.contributors', 'contributorsSorting'),
|
|
||||||
|
|
||||||
contributorsActive: computed('contributors.[]', 'contributions', function() {
|
|
||||||
let activeIds = this.contributions.mapBy('contributorId')
|
|
||||||
.map(id => id.toString())
|
|
||||||
.uniq();
|
|
||||||
return this.contributors.filter(c => {
|
|
||||||
return activeIds.includes(c.id.toString());
|
|
||||||
});
|
});
|
||||||
}),
|
}.property('contributions.@each')
|
||||||
|
|
||||||
contributionKinds: computed('contributions.[]', function() {
|
|
||||||
return this.contributions.mapBy('kind').uniq();
|
|
||||||
}),
|
|
||||||
|
|
||||||
contributionsFiltered: computed('contributions.[]', 'hideSmallContributions', 'contributorId', 'contributionKind', function() {
|
|
||||||
return this.contributions.filter(c => {
|
|
||||||
let included = true;
|
|
||||||
|
|
||||||
if (this.hideSmallContributions &&
|
|
||||||
c.amount <= 500) { included = false; }
|
|
||||||
|
|
||||||
if (isPresent(this.contributorId) &&
|
|
||||||
c.contributorId.toString() !== this.contributorId.toString()) { included = false; }
|
|
||||||
|
|
||||||
if (isPresent(this.contributionKind) &&
|
|
||||||
c.kind !== this.contributionKind) { included = false; }
|
|
||||||
|
|
||||||
return included;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
veto (contributionId) {
|
|
||||||
if (this.contractInteractionEnabled) {
|
|
||||||
this.vetoContribution(contributionId);
|
|
||||||
} else {
|
|
||||||
window.alert('Only members can veto contributions. Please ask someone to set you up.');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
openContributionDetails(contribution) {
|
|
||||||
this.router.transitionTo('dashboard.contributions.show', contribution);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,54 +1,7 @@
|
|||||||
{{#if showQuickFilter}}
|
{{#each contributionsWithContributors as |contribution|}}
|
||||||
<div class="quick-filter">
|
<li title="({{contribution.kind}}) {{contribution.description}}">
|
||||||
<p>
|
<span class="category {{contribution.kind}}">♥</span>
|
||||||
<label class="filter-contributor">
|
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
|
||||||
Contributor:
|
for <span class="recipient">{{contribution.contributor.name}}</span>
|
||||||
<select onchange={{action (mut contributorId) value="target.value"}}>
|
</li>
|
||||||
<option value="" selected>all</option>
|
{{/each}}
|
||||||
{{#each contributorsActive as |contributor|}}
|
|
||||||
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.name}}</option>
|
|
||||||
{{/each}}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="filter-contribution-kind">
|
|
||||||
Kind:
|
|
||||||
<select onchange={{action (mut contributionKind) value="target.value"}}>
|
|
||||||
<option value="" selected>all</option>
|
|
||||||
{{#each contributionKinds as |kind|}}
|
|
||||||
<option value={{kind}} selected={{eq contributionKind kind}}>{{capitalize-string kind}}</option>
|
|
||||||
{{/each}}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="filter-contribution-size">
|
|
||||||
{{input type="checkbox" checked=hideSmallContributions}}
|
|
||||||
Hide small contributions
|
|
||||||
</label>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<ul class="contribution-list">
|
|
||||||
{{#each contributionsFiltered as |contribution|}}
|
|
||||||
<li role="button" {{action "openContributionDetails" contribution}}
|
|
||||||
data-contribution-id={{contribution.id}}
|
|
||||||
class="{{contribution-status contribution}}{{if contribution.vetoed " vetoed"}}{{if (eq contribution.id selectedContributionId) " selected"}}">
|
|
||||||
<p class="meta">
|
|
||||||
<span class="recipient">{{user-avatar contributor=contribution.contributor}}</span>
|
|
||||||
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
|
|
||||||
<span class="title">{{contribution.description}}</span>
|
|
||||||
</p>
|
|
||||||
<p class="kredits-amount">
|
|
||||||
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
|
|
||||||
</p>
|
|
||||||
{{#unless contribution.vetoed}}
|
|
||||||
{{#unless (is-confirmed-contribution contribution)}}
|
|
||||||
<p class="voting">
|
|
||||||
<button {{action "veto" contribution.id}} class="small danger">veto</button>
|
|
||||||
</p>
|
|
||||||
{{/unless}}
|
|
||||||
{{/unless}}
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import Component from '@ember/component';
|
import Ember from 'ember';
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
|
|
||||||
export default Component.extend({
|
export default Ember.Component.extend({
|
||||||
|
|
||||||
router: service(),
|
|
||||||
|
|
||||||
tagName: 'table',
|
tagName: 'table',
|
||||||
classNames: 'contributor-list',
|
classNames: 'contributor-list',
|
||||||
|
selectedContributor: null,
|
||||||
selectedContributorId: null,
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
openContributorDetails(contributor) {
|
toggleContributorInfo(contributor) {
|
||||||
this.router.transitionTo('dashboard.contributors.show', contributor);
|
if (contributor.get('showMetadata')) {
|
||||||
|
contributor.set('showMetadata', false);
|
||||||
|
} else {
|
||||||
|
this.get('contributors').setEach('showMetadata', false);
|
||||||
|
contributor.set('showMetadata', true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{{#each contributorList as |c|}}
|
{{#each contributors as |contributor|}}
|
||||||
<tr role="button" {{action "openContributorDetails" c.contributor}}
|
<tr class="{{if contributor.isCurrentUser 'current-user'}}" {{action "toggleContributorInfo" contributor}}>
|
||||||
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id selectedContributorId) "selected"}}">
|
|
||||||
<td class="person">
|
<td class="person">
|
||||||
{{user-avatar contributor=c.contributor}} {{c.contributor.name}}
|
<img class="avatar" src={{contributor.avatarURL}}>
|
||||||
|
{{contributor.name}}
|
||||||
</td>
|
</td>
|
||||||
<td class="kredits">
|
<td class="kredits">
|
||||||
<span class="amount">
|
<span class="amount">{{contributor.kredits}}</span>
|
||||||
{{#if showUnconfirmedKredits}}
|
|
||||||
{{c.amountTotal}}
|
|
||||||
{{else}}
|
|
||||||
{{c.amountConfirmed}}
|
|
||||||
{{/if}}
|
|
||||||
</span>
|
|
||||||
<span class="symbol">₭S</span>
|
<span class="symbol">₭S</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr class="metadata {{if contributor.isCurrentUser 'current-user'}} {{if contributor.showMetadata 'visible'}}">
|
||||||
|
<td colspan="2">
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://testnet.etherscan.io/address/{{contributor.address}}">Inspect Ethereum transactions</a></li>
|
||||||
|
{{#if contributor.ipfsHash}}
|
||||||
|
<li><a href="https://ipfs.io/ipfs/{{contributor.ipfsHash}}">Inspect IPFS profile</a></li>
|
||||||
|
{{/if}}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: "",
|
|
||||||
account: null,
|
|
||||||
|
|
||||||
iconComponentName: computed('account.site', function() {
|
|
||||||
if (this.account.site.match(/github|gitea|wiki/)) {
|
|
||||||
return 'icon-account-' + this.account.site.replace(/\./g, '-');
|
|
||||||
} else {
|
|
||||||
return 'icon-web-globe';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<a href={{account.url}} target="_blank" rel="noopener" title="{{account.username}} on {{account.site}}">
|
|
||||||
<span class="site">{{account.site}}</span>
|
|
||||||
{{component iconComponentName}}
|
|
||||||
</a>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 7.0 KiB |
@@ -1,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<svg id="svg825" class="icon-github" width="260" height="260" enable-background="new 0 0 212.4575 186.23" version="1.1" viewBox="0 0 260 260" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
||||||
<g transform="translate(23.766 36.885)">
|
|
||||||
<path d="m151.79 86.234c-7.078 0.1229-14.154 0.5605-21.232 0.7069-12.318 0.248-24.64 0.5664-36.961 0.512-11.176-0.0472-22.358-0.9728-33.518-0.7052-7.3164 0.1776-14.373 2.2948-20.131 7.584-9.9472 9.1368-12.522 21-11.314 33.807 1.92 20.357 9.2716 31.449 29.115 37.064 15.793 4.4704 31.963 5.2088 48.199 4.9744 5.254 0 10.51 0.1367 15.758-0.0292 12.723-0.4064 25.264-2.2072 37.358-6.3596 10.846-3.7225 18.799-10.691 22.572-22.166 2.6736-8.1288 3.7148-16.388 2.5544-24.853-1.7772-12.959-13.275-30.86-32.4-30.535zm-81.576 59.996c-8.836 0-16-8.9551-16-20 0-11.045 7.164-20 16-20s16 8.9553 16 20c0 11.045-7.164 20-16 20zm76 0c-8.836 0-16-8.9551-16-20 0-11.045 7.164-20 16-20s16 8.9553 16 20c0 11.045-7.164 20-16 20z" fill="none"/>
|
|
||||||
<path class="fg" d="m200.25 59.188c-2.5076-3.4493-4.3632-6.3888-3.8748-11.44 0.6796-7.0664-0.3536-14.396-1.3204-21.518-1.2188-8.9843-3.5236-17.74-7.1776-26.23-9.0332 1.9356-17.981 3.49-25.963 8.0976-8.4648 4.8908-16.695 10.215-25.08 15.25-0.9532 0.5704-2.3124 0.8848-3.3848 0.7032-17.967-3.1368-35.92-3.0196-53.898-0.086-1.3632 0.2244-3.0724-0.2345-4.2948-0.9608-4.4436-2.6604-8.6564-5.7344-13.123-8.3516-11.656-6.844-23.66-12.822-37.494-14.59-0.3928 0.8597-0.7188 1.4785-0.9612 2.1253-4.164 11.22-6.6424 22.844-7.4276 34.82-0.2872 4.3516-0.6092 8.8048 0 13.072 0.3456 2.4296-4e-3 3.8788-1.3924 5.6132-4.8516 6.0488-8.6368 12.717-11.102 20.219-4.6972 14.295-4.4004 28.861-2.2756 43.475 1.7268 11.974 4.7344 23.537 11.324 33.873 13.186 20.682 32.892 29.774 55.572 31.705 19.308 1.6464 38.8 1.1992 58.213 1.2364 13.855 0.0251 27.57-1.4924 40.715-6.6389 20.375-7.9727 33.859-22.604 40.275-44.166 3.7344-12.545 5.2324-25.396 4.8144-38.498-0.4412-13.81-4.0504-26.566-12.144-37.711zm-18.609 82.435c-3.7736 11.475-11.727 18.444-22.572 22.166-12.094 4.1524-24.635 5.9532-37.358 6.3596-5.248 0.1659-10.504 0.0292-15.758 0.0292-16.236 0.2344-32.406-0.504-48.199-4.9744-19.844-5.6152-27.195-16.707-29.115-37.064-1.2072-12.807 1.3672-24.67 11.314-33.807 5.7576-5.2892 12.814-7.4064 20.131-7.584 11.16-0.2676 22.342 0.658 33.518 0.7052 12.32 0.0544 24.642-0.264 36.961-0.512 7.0784-0.1464 14.154-0.584 21.232-0.7069 19.125-0.3243 30.623 17.576 32.4 30.535 1.1604 8.4647 0.1192 16.724-2.5544 24.853z"/>
|
|
||||||
<ellipse class="fg" cx="70.214" cy="126.23" rx="16" ry="20"/>
|
|
||||||
<ellipse class="fg" cx="146.21" cy="126.23" rx="16" ry="20"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.7 KiB |
@@ -1,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<svg class="icon-wiki" width="120" height="120" version="1.1" viewBox="0 0 120 120" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
||||||
<g transform="translate(18.1,3)">
|
|
||||||
<polygon class="fg" transform="translate(-6.2,9)" points="6.2 5 45.7 20.5 45.7 91 6.2 75.5"/>
|
|
||||||
<polygon class="fg" transform="translate(-6.2,9)" points="90 5 50.5 20.5 50.5 91 90 75.5"/>
|
|
||||||
<polygon class="fg" transform="translate(-6.2,9)" points="87 88 87 81.8 60.4 91"/>
|
|
||||||
<polygon class="fg" transform="translate(-6.2,9)" points="9.2 88 9.2 81.8 34.9 91"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 703 B |
@@ -1,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
@@ -1,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,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<svg class="icon-web-globe" width="60" height="60" enable-background="new 0 0 64 64" version="1.1" viewBox="0 0 60 60" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
||||||
<path class="fg" d="m48.1 14.3c-0.4-0.5-0.9-1-1.4-1.5-4.3-4.2-10.2-6.8-16.7-6.8s-12.4 2.6-16.7 6.8c-0.5 0.5-1 1-1.4 1.5-3.7 4.2-5.9 9.7-5.9 15.7s2.2 11.5 5.8 15.7c0.4 0.5 0.9 1 1.4 1.5 4.3 4.2 10.3 6.8 16.8 6.8s12.5-2.6 16.8-6.9c0.5-0.5 0.9-0.9 1.4-1.5 3.6-4.1 5.8-9.6 5.8-15.6s-2.2-11.5-5.9-15.7zm3.9 14.7h-8.1c-0.1-3-1-7.3-2.4-10.5 1.8-0.8 3.5-1.8 5.1-3 3.1 3.6 5.1 8.5 5.4 13.5zm-21-20.1c3 2.3 5.8 5.2 7.6 8.5-2.4 0.9-4.6 1.3-7.6 1.4zm-2 9.9c-3-0.1-5.2-0.6-7.6-1.4 1.8-3.3 4.6-6.3 7.6-8.5zm0 2v8.2h-10.8c0.1-3 0.9-6.8 2.3-9.8 2.7 1 5.5 1.5 8.5 1.6zm0 10.2v8c-3 0.1-5.9 0.7-8.6 1.7-1.4-3-2.1-5.7-2.3-9.7zm0 10v10.1c-3-2.3-5.8-5.3-7.7-8.6 2.4-0.9 4.7-1.4 7.7-1.5zm2 0c3 0.1 5.3 0.6 7.7 1.5-1.9 3.4-4.7 6.3-7.7 8.6zm0-2v-8h10.8c-0.1 4-0.9 6.7-2.3 9.7-2.6-1-5.5-1.5-8.5-1.7zm0-10v-8.2c3-0.1 5.8-0.7 8.5-1.6 1.4 3 2.2 6.8 2.3 9.8zm9.5-12.3c-1.7-3.2-4.1-6-6.9-8.3 4.4 0.6 8.4 2.7 11.5 5.6-1.4 1.1-3 1.9-4.6 2.7zm-14.1-8.4c-2.9 2.3-5.2 5.2-6.9 8.3-1.6-0.7-3.2-1.6-4.6-2.6 3.1-2.9 7.1-5 11.5-5.7zm-12.9 7.2c1.6 1.2 3.3 2.2 5.1 3-1.5 3.3-2.3 7.5-2.4 10.5h-8.2c0.3-5 2.3-9.9 5.5-13.5zm-5.5 15.5h8.1c0.1 4 1 7.2 2.4 10.5-1.8 0.8-3.5 1.8-5.1 3-3.2-3.7-5.1-8.5-5.4-13.5zm11.4 12.2c1.7 3.2 4.1 6.1 7 8.4-4.5-0.7-8.5-2.8-11.6-5.8 1.5-1 3-1.8 4.6-2.6zm14.2 8.5c2.9-2.4 5.3-5.2 7-8.4 1.6 0.7 3.2 1.6 4.6 2.6-3.1 3-7.2 5-11.6 5.8zm13-7.3c-1.6-1.2-3.3-2.2-5.1-3 1.4-3.2 2.3-6.5 2.4-10.5h8.1c-0.3 5.1-2.2 9.9-5.4 13.5z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,6 +1,6 @@
|
|||||||
import Component from '@ember/component';
|
import Ember from 'ember';
|
||||||
|
|
||||||
export default Component.extend({
|
export default Ember.Component.extend({
|
||||||
|
|
||||||
classNames: ['loading-spinner']
|
classNames: ['loading-spinner']
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,5 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Loading data...
|
Loading data from Ethereum...
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Component from '@ember/component';
|
import Ember from 'ember';
|
||||||
|
|
||||||
export default Component.extend({
|
export default Ember.Component.extend({
|
||||||
|
|
||||||
tagName: 'ul',
|
tagName: 'ul',
|
||||||
classNames: ['proposal-list'],
|
classNames: ['proposal-list'],
|
||||||
@@ -8,8 +8,8 @@ export default Component.extend({
|
|||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
confirm(proposalId) {
|
confirm(proposalId) {
|
||||||
if (this.contractInteractionEnabled) {
|
if (this.get('contractInteractionEnabled')) {
|
||||||
this.confirmProposal(proposalId);
|
this.sendAction('confirmAction', proposalId);
|
||||||
} else {
|
} else {
|
||||||
window.alert('Only members can vote on proposals. Please ask someone to set you up.');
|
window.alert('Only members can vote on proposals. Please ask someone to set you up.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,10 @@
|
|||||||
{{#each proposals as |proposal|}}
|
{{#each proposals as |proposal|}}
|
||||||
<li data-proposal-id={{proposal.id}} class={{if proposal.isExecuted "confirmed" "unconfirmed"}}>
|
<li data-proposal-id={{proposal.id}} title="({{proposal.kind}}) {{proposal.description}}">
|
||||||
<p class="meta">
|
<span class="category {{proposal.kind}}">♥</span>
|
||||||
<span class="category {{proposal.kind}}">♥ ({{proposal.kind}})</span>
|
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
|
||||||
<span class="recipient">{{proposal.contributor.name}}:</span>
|
for <span class="recipient">{{proposal.recipientName}}</span>
|
||||||
</p>
|
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
|
||||||
<p class="kredits-amount">
|
|
||||||
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
|
{{#unless proposal.executed}}<button {{action "confirm" proposal.id}}>+1</button>{{/unless}}
|
||||||
</p>
|
|
||||||
<p class="description">
|
|
||||||
<span class="description">{{proposal.description}}</span>
|
|
||||||
</p>
|
|
||||||
<p class="voting">
|
|
||||||
{{#unless proposal.isExecuted}}
|
|
||||||
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
|
|
||||||
<button {{action "confirm" proposal.id}}>+1</button>
|
|
||||||
{{/unless}}
|
|
||||||
</p>
|
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|||||||
@@ -1,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);
|
|
||||||
}).volatile(),
|
|
||||||
|
|
||||||
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 setupInProgress}}
|
|
||||||
Connecting account...
|
|
||||||
{{else}}
|
|
||||||
{{#if (and kredits.hasAccounts kredits.currentUser)}}
|
|
||||||
{{kredits.currentUser.name}}
|
|
||||||
{{#if kredits.currentUserIsCore}}
|
|
||||||
<span class="core-flag">(core)</span>
|
|
||||||
{{/if}}
|
|
||||||
{{else}}
|
|
||||||
Anonymous
|
|
||||||
<button {{action "signup"}} class="small">Sign up</button>
|
|
||||||
{{#if showConnectButton}}
|
|
||||||
<button {{action "connectAccount"}} class="small green">Connect account</button>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
|
||||||
</section>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
|
|
||||||
const SIZES = {
|
|
||||||
'small': '128', // pixels
|
|
||||||
'medium': '256',
|
|
||||||
'large': '512'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
contributor: null,
|
|
||||||
tagName: 'img',
|
|
||||||
classNames: ['avatar'],
|
|
||||||
classNameBindings: ['size'],
|
|
||||||
attributeBindings: ['src', 'title'],
|
|
||||||
size: 'small',
|
|
||||||
|
|
||||||
src: alias('avatarURL'),
|
|
||||||
title: alias('contributor.name'),
|
|
||||||
|
|
||||||
avatarURL: computed('contributor.github_uid', 'size', function() {
|
|
||||||
const github_uid = this.contributor.github_uid;
|
|
||||||
|
|
||||||
if (github_uid) {
|
|
||||||
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=${SIZES[this.size]}`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import Controller from '@ember/controller';
|
import Ember from 'ember';
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Ember.Controller.extend({
|
||||||
kredits: service(),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import Controller from '@ember/controller';
|
|
||||||
import { alias, filterBy, sort } from '@ember/object/computed';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
contributors: alias('kredits.contributors'),
|
|
||||||
minedContributors: filterBy('contributors', 'id'),
|
|
||||||
|
|
||||||
contributorsSorting: Object.freeze(['name:asc']),
|
|
||||||
sortedContributors: sort('minedContributors', 'contributorsSorting'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
save (contribution) {
|
|
||||||
const contributor = this.contributors.findBy('id', contribution.contributorId);
|
|
||||||
contribution.contributorIpfsHash = contributor.ipfsHash;
|
|
||||||
|
|
||||||
return this.kredits.addContribution(contribution)
|
|
||||||
.then(contribution => {
|
|
||||||
this.transitionToRoute('index');
|
|
||||||
return contribution;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import ContributionsNewController from 'kredits-web/controllers/contributions/new';
|
|
||||||
|
|
||||||
export default ContributionsNewController.extend({
|
|
||||||
|
|
||||||
attributes: null,
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import Controller from '@ember/controller';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
save (attributes) {
|
|
||||||
return this.kredits
|
|
||||||
.updateContributor(this.model.id, attributes)
|
|
||||||
.then(() => this.transitionToRoute('index'))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import Controller from '@ember/controller';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
save (contributor) {
|
|
||||||
return this.kredits
|
|
||||||
.addContributor(contributor)
|
|
||||||
.then(() => this.transitionToRoute('index'))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import Controller from '@ember/controller';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import { alias, not, sort } from '@ember/object/computed';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
showDetailsPane: false,
|
|
||||||
selectedContributorId: null,
|
|
||||||
selectedContributionId: null,
|
|
||||||
|
|
||||||
currentBlock: alias('kredits.currentBlock'),
|
|
||||||
|
|
||||||
contributions: alias('kredits.contributions'),
|
|
||||||
contributionsConfirmed: alias('kredits.contributionsConfirmed'),
|
|
||||||
contributionsUnconfirmed: alias('kredits.contributionsUnconfirmed'),
|
|
||||||
|
|
||||||
contributionsSorting: Object.freeze(['date:desc', 'time:desc', 'id:desc']),
|
|
||||||
contributionsUnconfirmedSorted: sort('contributionsUnconfirmed', 'contributionsSorting'),
|
|
||||||
contributionsConfirmedSorted: sort('contributionsConfirmed', 'contributionsSorting'),
|
|
||||||
|
|
||||||
kreditsByContributor: alias('kredits.kreditsByContributor'),
|
|
||||||
|
|
||||||
kreditsToplistSorting: computed('showUnconfirmedKredits', function(){
|
|
||||||
return this.showUnconfirmedKredits ? ['amountTotal:desc'] : ['amountConfirmed:desc'];
|
|
||||||
}),
|
|
||||||
kreditsToplist: sort('kreditsByContributor', 'kreditsToplistSorting'),
|
|
||||||
|
|
||||||
showUnconfirmedKredits: true,
|
|
||||||
hideUnconfirmedKredits: not('showUnconfirmedKredits'),
|
|
||||||
|
|
||||||
showQuickFilterUnconfirmed: false,
|
|
||||||
showQuickFilterConfirmed: false,
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
vetoContribution (contributionId) {
|
|
||||||
this.kredits.veto(contributionId).then(transaction => {
|
|
||||||
console.debug('[controllers:index] Veto submitted to Ethereum blockhain: '+transaction.hash);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmProposal (proposalId) {
|
|
||||||
this.kredits.vote(proposalId).then(transaction => {
|
|
||||||
console.debug('[controllers:index] Vote submitted to Ethereum blockhain: '+transaction.hash);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleQuickFilterUnconfirmed () {
|
|
||||||
this.toggleProperty('showQuickFilterUnconfirmed');
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleQuickFilterConfirmed () {
|
|
||||||
this.toggleProperty('showQuickFilterConfirmed');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
|
|
||||||
import Controller from '@ember/controller';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import config from 'kredits-web/config/environment';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
|
||||||
|
|
||||||
ipfsGatewayUrl: computed(function() {
|
|
||||||
return config.ipfs.gatewayUrl;
|
|
||||||
}).volatile()
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import Controller from '@ember/controller';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
|
||||||
|
|
||||||
roleName: computed('model.isCore', 'totalKreditsEarned', function() {
|
|
||||||
if (this.model.isCore) return 'Core Contributor';
|
|
||||||
if (this.model.totalKreditsEarned <= 5000) return 'Newcomer';
|
|
||||||
return 'Contributor';
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import Ember from 'ember';
|
||||||
|
import Proposal from 'kredits-web/models/proposal';
|
||||||
|
|
||||||
|
const {
|
||||||
|
computed,
|
||||||
|
inject: {
|
||||||
|
service
|
||||||
|
}
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
|
export default Ember.Controller.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
contractInteractionEnabled: computed.alias('kredits.web3Provided'),
|
||||||
|
|
||||||
|
findContributorByAddress(address) {
|
||||||
|
return this.get('model.contributors')
|
||||||
|
.findBy('address', address);
|
||||||
|
},
|
||||||
|
|
||||||
|
proposalsOpen: function() {
|
||||||
|
let proposals = this.get('model.proposals')
|
||||||
|
.filterBy('executed', false)
|
||||||
|
.map(p => {
|
||||||
|
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name);
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
return proposals;
|
||||||
|
}.property('model.proposals.[]', 'model.proposals.@each.executed', 'model.contributors.[]'),
|
||||||
|
|
||||||
|
proposalsClosed: function() {
|
||||||
|
let proposals = this.get('model.proposals')
|
||||||
|
.filterBy('executed', true)
|
||||||
|
.map(p => {
|
||||||
|
p.set('recipientName', this.findContributorByAddress(p.get('recipientAddress')).name);
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
return proposals;
|
||||||
|
}.property('model.proposals.[]', 'model.proposals.@each.executed', 'model.contributors.[]'),
|
||||||
|
|
||||||
|
proposalsSorting: ['id:desc'],
|
||||||
|
proposalsClosedSorted: Ember.computed.sort('proposalsClosed', 'proposalsSorting'),
|
||||||
|
proposalsOpenSorted: Ember.computed.sort('proposalsOpen', 'proposalsSorting'),
|
||||||
|
|
||||||
|
contributorsWithKredits: function() {
|
||||||
|
return this.get('model.contributors').filter(c => {
|
||||||
|
return c.get('kredits') !== 0;
|
||||||
|
});
|
||||||
|
}.property('model.contributors.@each.kredits'),
|
||||||
|
|
||||||
|
contributorsSorting: ['kredits:desc'],
|
||||||
|
contributorsSorted: Ember.computed.sort('contributorsWithKredits', 'contributorsSorting'),
|
||||||
|
|
||||||
|
watchContractEvents: function() {
|
||||||
|
let events = this.get('kredits.kreditsContract')
|
||||||
|
.allEvents(/* [additionalFilterObject], */);
|
||||||
|
|
||||||
|
events.watch((error, data) => {
|
||||||
|
Ember.Logger.debug('[index] Received contract event', data);
|
||||||
|
|
||||||
|
switch (data.event) {
|
||||||
|
case 'ProposalCreated':
|
||||||
|
this._handleProposalCreated(data);
|
||||||
|
break;
|
||||||
|
case 'ProposalExecuted':
|
||||||
|
this._handleProposalExecuted(data);
|
||||||
|
break;
|
||||||
|
case 'ProposalVoted':
|
||||||
|
this._handleProposalVoted(data);
|
||||||
|
break;
|
||||||
|
case 'Transfer':
|
||||||
|
this._handleTransfer(data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}.on('init'),
|
||||||
|
|
||||||
|
_handleProposalCreated(data) {
|
||||||
|
if (Ember.isPresent(this.get('model.proposals')
|
||||||
|
.findBy('id', data.args.id.toNumber()))) {
|
||||||
|
Ember.Logger.debug('[index] proposal exists, not adding from event');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let proposal = Proposal.create({
|
||||||
|
id: data.args.id.toNumber(),
|
||||||
|
creatorAddress: data.args.creator,
|
||||||
|
recipientAddress: data.args.recipient,
|
||||||
|
recipientName: null,
|
||||||
|
votesCount: 0,
|
||||||
|
votesNeeded: 2,
|
||||||
|
amount: data.args.amount.toNumber(),
|
||||||
|
executed: false,
|
||||||
|
url: data.args.url,
|
||||||
|
ipfsHash: data.args.ipfsHash
|
||||||
|
});
|
||||||
|
|
||||||
|
this.get('model.proposals').pushObject(proposal);
|
||||||
|
},
|
||||||
|
|
||||||
|
_handleProposalExecuted(data) {
|
||||||
|
if (this.get('model.proposals')
|
||||||
|
.findBy('id', data.args.id.toNumber())
|
||||||
|
.get('executed')) {
|
||||||
|
Ember.Logger.debug('[index] proposal already executed, not adding from event');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.get('model.proposals')
|
||||||
|
.findBy('id', data.args.id.toNumber())
|
||||||
|
.setProperties({
|
||||||
|
'executed': true,
|
||||||
|
'votesCount': 2 // TODO use real count
|
||||||
|
});
|
||||||
|
|
||||||
|
this.get('model.contributors')
|
||||||
|
.findBy('address', data.args.recipient)
|
||||||
|
.incrementProperty('kredits', data.args.amount.toNumber());
|
||||||
|
},
|
||||||
|
|
||||||
|
_handleProposalVoted(data) {
|
||||||
|
this.get('model.proposals')
|
||||||
|
.findBy('id', data.args.id.toNumber())
|
||||||
|
.incrementProperty('votesCount', 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
_handleTransfer(data) {
|
||||||
|
this.get('model.contributors')
|
||||||
|
.findBy('address', data.args.from)
|
||||||
|
.incrementProperty('kredits', - data.args.value.toNumber());
|
||||||
|
this.get('model.contributors')
|
||||||
|
.findBy('address', data.args.to)
|
||||||
|
.incrementProperty('kredits', data.args.value.toNumber());
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
|
||||||
|
confirmProposal(proposalId) {
|
||||||
|
this.get('kredits').vote(proposalId).then(transactionId => {
|
||||||
|
window.confirm('Vote submitted to Ethereum blockhain: '+transactionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,24 +1,29 @@
|
|||||||
import Controller from '@ember/controller';
|
import Ember from 'ember';
|
||||||
import { alias, filterBy } from '@ember/object/computed';
|
import QueryParams from 'ember-parachute';
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
export const queryParams = new QueryParams({
|
||||||
kredits: service(),
|
recipient: {
|
||||||
|
defaultValue: ''
|
||||||
contributors: alias('kredits.contributors'),
|
},
|
||||||
minedContributors: filterBy('contributors', 'id'),
|
amount: {
|
||||||
|
defaultValue: ''
|
||||||
actions: {
|
},
|
||||||
save(proposal) {
|
url: {
|
||||||
// contributorIpfsHash is needed for the proposal ipfs data. I'm not happy to do this here but I think to load all the contributors in addProposal again is a bit too much. I hope we can refactor it later.
|
defaultValue: ''
|
||||||
let contributor = this.contributors.findBy('id', proposal.contributorId);
|
},
|
||||||
proposal.contributorIpfsHash = contributor.get('ipfsHash');
|
ipfsHash: {
|
||||||
|
defaultValue: ''
|
||||||
return this.kredits.addProposal(proposal)
|
|
||||||
.then((proposal) => {
|
|
||||||
this.transitionToRoute('index');
|
|
||||||
return proposal;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default Ember.Controller.extend(queryParams.Mixin, {
|
||||||
|
|
||||||
|
contributors: null,
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
onSave() {
|
||||||
|
this.transitionToRoute('index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|||||||
@@ -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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import Helper from '@ember/component/helper';
|
|
||||||
|
|
||||||
export default Helper.extend({
|
|
||||||
compute([string]) {
|
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import Helper from '@ember/component/helper';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
|
|
||||||
export default Helper.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
currentBlock: alias('kredits.currentBlock'),
|
|
||||||
|
|
||||||
compute([contribution]) {
|
|
||||||
if (contribution.vetoed) {
|
|
||||||
return 'vetoed';
|
|
||||||
} else if (contribution.confirmedAt > this.currentBlock) {
|
|
||||||
return 'unconfirmed';
|
|
||||||
} else {
|
|
||||||
return 'confirmed'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import Helper from '@ember/component/helper';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
|
|
||||||
export default Helper.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
currentBlock: alias('kredits.currentBlock'),
|
|
||||||
|
|
||||||
compute([contribution]) {
|
|
||||||
return !contribution.vetoed &&
|
|
||||||
(contribution.confirmedAt <= this.currentBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import Helper from '@ember/component/helper';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
import { isPresent } from '@ember/utils';
|
|
||||||
|
|
||||||
export default Helper.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
currentUser: alias('kredits.currentUser'),
|
|
||||||
|
|
||||||
compute([contributor]) {
|
|
||||||
return isPresent(contributor) && isPresent(this.currentUser) &&
|
|
||||||
contributor.account === this.currentUser.account;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
+2
-2
@@ -9,8 +9,8 @@
|
|||||||
|
|
||||||
{{content-for "head"}}
|
{{content-for "head"}}
|
||||||
|
|
||||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">
|
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
|
||||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/kredits-web.css">
|
<link rel="stylesheet" href="{{rootURL}}assets/kredits-web.css">
|
||||||
|
|
||||||
{{content-for "head-footer"}}
|
{{content-for "head-footer"}}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* This pauses the app's boot process until the window load event is fired.
|
||||||
|
* It allows the user provided Web3 instance (e.g. Metamask, Mist Browser)
|
||||||
|
* to be inserted before we try to use it.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
name: 'defer-loading',
|
||||||
|
initialize: function(application) {
|
||||||
|
// Load event already fired, so web3 should be loaded if available
|
||||||
|
if (window.windowLoadComplete) { return true; }
|
||||||
|
|
||||||
|
// Pause app loading
|
||||||
|
application.deferReadiness();
|
||||||
|
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
// Continue app loading
|
||||||
|
application.advanceReadiness();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+41
-28
@@ -1,40 +1,53 @@
|
|||||||
import EmberObject, { computed } from '@ember/object';
|
import Ember from 'ember';
|
||||||
import { isEmpty } from '@ember/utils';
|
|
||||||
import bignumber from 'kredits-web/utils/cps/bignumber';
|
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
export default EmberObject.extend({
|
export default Ember.Object.extend({
|
||||||
|
|
||||||
// Contract
|
// blockNumber: null,
|
||||||
id: null,
|
// blockHash: null,
|
||||||
|
// transactionHash: null,
|
||||||
|
recipientAddress: null,
|
||||||
contributorId: null,
|
contributorId: null,
|
||||||
amount: null,
|
|
||||||
confirmedAt: bignumber('confirmedAtBlock', 'toNumber'),
|
|
||||||
vetoed: null,
|
|
||||||
ipfsHash: null,
|
ipfsHash: null,
|
||||||
|
amount: null,
|
||||||
|
|
||||||
creatorAccount: null,
|
contributor: null,
|
||||||
|
contribution: null,
|
||||||
// IPFS
|
|
||||||
kind: null,
|
kind: null,
|
||||||
description: null,
|
description: null,
|
||||||
details: null,
|
|
||||||
url: null,
|
url: null,
|
||||||
date: null,
|
details: null,
|
||||||
time: null,
|
|
||||||
ipfsData: '',
|
|
||||||
|
|
||||||
init () {
|
/**
|
||||||
this._super(...arguments);
|
* Loads the contribution details from IPFS and sets local instance
|
||||||
if (isEmpty(this.details)) this.set('details', {});
|
* properties from it
|
||||||
|
*
|
||||||
|
* @method
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
loadDetails(ipfs) {
|
||||||
|
let promise = new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
|
ipfs.getFile(this.get('ipfsHash')).then(content => {
|
||||||
|
let contributionJSON = JSON.parse(content);
|
||||||
|
let contribution = Ember.Object.create(contributionJSON);
|
||||||
|
|
||||||
|
this.setProperties({
|
||||||
|
kind: contribution.get('kind'),
|
||||||
|
description: contribution.get('description'),
|
||||||
|
url: contribution.get('url')
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO load details
|
||||||
|
// let details = profile.get('accounts');
|
||||||
|
|
||||||
|
Ember.Logger.debug('[proposal] loaded contribution details', contributionJSON);
|
||||||
|
resolve();
|
||||||
|
}).catch((err) => {
|
||||||
|
Ember.Logger.error('[proposal] error trying to load contribution details', this.get('ipfsHash'), err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
iso8601Date: computed('date', 'time', function() {
|
|
||||||
return this.time ? `${this.date}T${this.time}` : this.date;
|
|
||||||
}),
|
|
||||||
|
|
||||||
jsDate: computed('iso8601Date', function() {
|
|
||||||
return moment(this.iso8601Date).toDate();
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
+110
-15
@@ -1,24 +1,119 @@
|
|||||||
import EmberObject from '@ember/object';
|
import Ember from 'ember';
|
||||||
import bignumber from 'kredits-web/utils/cps/bignumber';
|
|
||||||
import kreditsValue from 'kredits-web/utils/cps/kredits';
|
|
||||||
|
|
||||||
export default EmberObject.extend({
|
const {
|
||||||
// Contract
|
isPresent,
|
||||||
id: bignumber('idRaw', 'toString'),
|
} = Ember;
|
||||||
account: null,
|
|
||||||
balance: kreditsValue('balanceRaw'),
|
|
||||||
totalKreditsEarned: bignumber('totalKreditsEarnedRaw', 'toNumber'),
|
|
||||||
contributionsCount: bignumber('contributionsCountRaw', 'toNumber'),
|
|
||||||
isCore: false,
|
|
||||||
ipfsHash: null,
|
|
||||||
|
|
||||||
// IPFS
|
export default Ember.Object.extend({
|
||||||
kind: null,
|
|
||||||
|
address: null,
|
||||||
name: null,
|
name: null,
|
||||||
|
kind: null,
|
||||||
url: null,
|
url: null,
|
||||||
github_username: null,
|
github_username: null,
|
||||||
github_uid: null,
|
github_uid: null,
|
||||||
wiki_username: null,
|
wiki_username: null,
|
||||||
ipfsData: ''
|
ipfsHash: null,
|
||||||
|
kredits: null,
|
||||||
|
isCore: false,
|
||||||
|
isCurrentUser: false,
|
||||||
|
|
||||||
|
avatarURL: function() {
|
||||||
|
return `https\:\/\/avatars2.githubusercontent.com/u/${this.get('github_uid')}?v=3&s=128`;
|
||||||
|
}.property('github_uid'),
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the contributor's profile data from IPFS and sets local instance
|
||||||
|
* properties from it
|
||||||
|
*
|
||||||
|
* @method
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
loadProfile(ipfs) {
|
||||||
|
let promise = new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
|
ipfs.getFile(this.get('ipfsHash')).then(content => {
|
||||||
|
let profileJSON = JSON.parse(content);
|
||||||
|
let profile = Ember.Object.create(profileJSON);
|
||||||
|
|
||||||
|
this.setProperties({
|
||||||
|
name: profile.get('name'),
|
||||||
|
kind: profile.get('kind')
|
||||||
|
});
|
||||||
|
|
||||||
|
let accounts = profile.get('accounts');
|
||||||
|
let github = accounts.findBy('site', 'github.com');
|
||||||
|
let wiki = accounts.findBy('site', 'wiki.kosmos.org');
|
||||||
|
|
||||||
|
if (isPresent(github)) {
|
||||||
|
this.setProperties({
|
||||||
|
github_username: github.username,
|
||||||
|
github_uid: github.uid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (isPresent(wiki)) {
|
||||||
|
this.setProperties({
|
||||||
|
wiki_username: wiki.username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ember.Logger.debug('[contributor] loaded contributor profile', profile);
|
||||||
|
resolve();
|
||||||
|
}).catch((err) => {
|
||||||
|
Ember.Logger.error('[contributor] error trying to load contributor profile', this.get('ipfsHash'), err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a JSON-LD object of the contributor, according to
|
||||||
|
* https://github.com/67P/kosmos-schemas/blob/master/schemas/contributor.json
|
||||||
|
*
|
||||||
|
* @method
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
toJSON() {
|
||||||
|
let contributor = {
|
||||||
|
"@context": "https://schema.kosmos.org",
|
||||||
|
"@type": "Contributor",
|
||||||
|
"kind": this.get('kind'),
|
||||||
|
"name": this.get('name'),
|
||||||
|
"accounts": []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Ember.isPresent(this.get('url'))) {
|
||||||
|
contributor["url"] = this.get('url');
|
||||||
|
}
|
||||||
|
if (Ember.isPresent(this.get('github_uid'))) {
|
||||||
|
contributor.accounts.push({
|
||||||
|
"site": "github.com",
|
||||||
|
"uid": this.get('github_uid'),
|
||||||
|
"username": this.get('github_username'),
|
||||||
|
"url": `https://github.com/${this.get('github_username')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Ember.isPresent(this.get('wiki_username'))) {
|
||||||
|
contributor.accounts.push({
|
||||||
|
"site": "wiki.kosmos.org",
|
||||||
|
"username": this.get('wiki_username'),
|
||||||
|
"url": `https://wiki.kosmos.org/User:${this.get('wiki_username')}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return contributor;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the JSON-LD representation of the model as a string
|
||||||
|
*
|
||||||
|
* @method
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
serialize() {
|
||||||
|
return JSON.stringify(this.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
+92
-22
@@ -1,32 +1,102 @@
|
|||||||
import EmberObject from '@ember/object';
|
import Ember from 'ember';
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
import bignumber from 'kredits-web/utils/cps/bignumber';
|
|
||||||
|
|
||||||
export default EmberObject.extend({
|
const {
|
||||||
|
isPresent,
|
||||||
|
isEmpty,
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
// Contract
|
export default Ember.Object.extend({
|
||||||
id: bignumber('idRaw', 'toString'),
|
|
||||||
creatorAccount: null,
|
id: null,
|
||||||
contributorId: bignumber('contributorIdRaw', 'toString'),
|
creatorAddress: null,
|
||||||
amount: bignumber('amountRaw', 'toNumber'),
|
recipientAddress: null,
|
||||||
votesCount: bignumber('votesCountRaw', 'toNumber'),
|
recipientName: null,
|
||||||
votesNeeded: bignumber('votesNeededRaw', 'toNumber'),
|
recipientProfile: null,
|
||||||
|
votesCount: null,
|
||||||
|
votesNeeded: null,
|
||||||
|
amount: null,
|
||||||
executed: null,
|
executed: null,
|
||||||
ipfsHash: null,
|
contribution: null,
|
||||||
|
|
||||||
// Shortcuts
|
|
||||||
isExecuted: alias('executed'),
|
|
||||||
|
|
||||||
// IPFS
|
|
||||||
kind: null,
|
kind: null,
|
||||||
description: null,
|
description: null,
|
||||||
details: null,
|
|
||||||
url: null,
|
url: null,
|
||||||
ipfsData: '',
|
details: null,
|
||||||
|
ipfsHash: null,
|
||||||
|
|
||||||
init () {
|
/**
|
||||||
this._super(...arguments);
|
* Loads the contribution details from IPFS and sets local instance
|
||||||
this.set('details', {});
|
* properties from it
|
||||||
|
*
|
||||||
|
* @method
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
loadContribution(ipfs) {
|
||||||
|
let promise = new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
|
ipfs.getFile(this.get('ipfsHash')).then(content => {
|
||||||
|
let contributionJSON = JSON.parse(content);
|
||||||
|
let contribution = Ember.Object.create(contributionJSON);
|
||||||
|
|
||||||
|
this.setProperties({
|
||||||
|
kind: contribution.get('kind'),
|
||||||
|
description: contribution.get('description'),
|
||||||
|
url: contribution.get('url')
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO load details
|
||||||
|
// let details = profile.get('accounts');
|
||||||
|
|
||||||
|
Ember.Logger.debug('[proposal] loaded contribution details', contributionJSON);
|
||||||
|
resolve();
|
||||||
|
}).catch((err) => {
|
||||||
|
Ember.Logger.error('[proposal] error trying to load contribution details', this.get('ipfsHash'), err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a JSON-LD object of the contribution, according to
|
||||||
|
* https://github.com/67P/kosmos-schemas/blob/master/schemas/contribution.json
|
||||||
|
*
|
||||||
|
* @method
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
contributionToJSON() {
|
||||||
|
if (isEmpty(this.get('recipientProfile'))) {
|
||||||
|
throw new Error('IPFS hash for recipient profile missing from proposal object');
|
||||||
|
}
|
||||||
|
if (isEmpty(this.get('kind')) || isEmpty(this.get('description'))) {
|
||||||
|
throw new Error('Missing one or more required properties: kind, description');
|
||||||
|
}
|
||||||
|
|
||||||
|
let contribution = {
|
||||||
|
"@context": "https://schema.kosmos.org",
|
||||||
|
"@type": "Contribution",
|
||||||
|
"contributor": {
|
||||||
|
"ipfs": this.get('recipientProfile')
|
||||||
|
},
|
||||||
|
"kind": this.get('kind'),
|
||||||
|
"description": this.get('description'),
|
||||||
|
"details": {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPresent(this.get('url'))) {
|
||||||
|
contribution["url"] = this.get('url');
|
||||||
|
}
|
||||||
|
|
||||||
|
return contribution;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the JSON-LD representation of the contribution as a string
|
||||||
|
*
|
||||||
|
* @method
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
serializeContribution() {
|
||||||
|
return JSON.stringify(this.contributionToJSON());
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-24
@@ -1,37 +1,16 @@
|
|||||||
import EmberRouter from '@ember/routing/router';
|
import Ember from 'ember';
|
||||||
import config from './config/environment';
|
import config from './config/environment';
|
||||||
|
|
||||||
const Router = EmberRouter.extend({
|
const Router = Ember.Router.extend({
|
||||||
location: config.locationType,
|
location: config.locationType,
|
||||||
rootURL: config.rootURL
|
rootURL: config.rootURL
|
||||||
});
|
});
|
||||||
|
|
||||||
Router.map(function() {
|
Router.map(function() {
|
||||||
this.route('dashboard', function() {
|
this.route('spinner');
|
||||||
this.route('contributors', function() {
|
|
||||||
this.route('show', { path: ':id' });
|
|
||||||
});
|
|
||||||
|
|
||||||
this.route('contributions', function() {
|
|
||||||
this.route('show', { path: ':id' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.route('proposals', function() {
|
this.route('proposals', function() {
|
||||||
this.route('new');
|
this.route('new');
|
||||||
});
|
});
|
||||||
this.route('contributions', function() {
|
|
||||||
this.route('new');
|
|
||||||
this.route('resubmit', { path: ':id/resubmit' });
|
|
||||||
});
|
|
||||||
this.route('contributors', function() {
|
|
||||||
this.route('new');
|
|
||||||
this.route('edit', { path: ':id/edit' });
|
|
||||||
});
|
|
||||||
this.route('signup', function() {
|
|
||||||
this.route('github');
|
|
||||||
this.route('eth-account');
|
|
||||||
this.route('complete');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Router;
|
export default Router;
|
||||||
|
|||||||
@@ -1,26 +1,4 @@
|
|||||||
import { inject as service } from '@ember/service';
|
import Ember from 'ember';
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
|
|
||||||
export default Route.extend({
|
export default Ember.Route.extend({
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
beforeModel(/* transition */) {
|
|
||||||
const kredits = this.kredits;
|
|
||||||
|
|
||||||
return kredits.setup().then(() => {
|
|
||||||
kredits.get('kredits').preflightChecks().catch((error) => {
|
|
||||||
console.error('Kredits preflight check failed!');
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}).catch((error) => {
|
|
||||||
console.log('Error initializing Kredits', error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
afterModel() {
|
|
||||||
return this.kredits.loadInitialData()
|
|
||||||
.then(() => {
|
|
||||||
this.kredits.addContractEventHandlers();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,29 +0,0 @@
|
|||||||
import { inject as service } from '@ember/service';
|
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
|
|
||||||
export default Route.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
contributors: alias('kredits.contributors'),
|
|
||||||
|
|
||||||
model(params) {
|
|
||||||
return this.kredits.contributors.findBy('id', params.id);
|
|
||||||
},
|
|
||||||
|
|
||||||
setupController (controller, model) {
|
|
||||||
this._super(controller, model);
|
|
||||||
|
|
||||||
controller.set('attributes', {
|
|
||||||
account: model.account,
|
|
||||||
name: model.name,
|
|
||||||
kind: model.kind,
|
|
||||||
url: model.url,
|
|
||||||
github_username: model.github_username,
|
|
||||||
github_uid: model.github_uid,
|
|
||||||
gitea_username: model.gitea_username,
|
|
||||||
wiki_username: model.wiki_username
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { inject as service } from '@ember/service';
|
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
|
|
||||||
export default Route.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
contributions: alias('kredits.contributions'),
|
|
||||||
|
|
||||||
model (params) {
|
|
||||||
return this.contributions.findBy('id', parseInt(params.id));
|
|
||||||
},
|
|
||||||
|
|
||||||
setupController (controller, model) {
|
|
||||||
this._super(controller, model);
|
|
||||||
|
|
||||||
this.controllerFor('dashboard')
|
|
||||||
.setProperties({
|
|
||||||
showDetailsPane: true,
|
|
||||||
selectedContributionId: model.id
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
deactivate () {
|
|
||||||
this.controllerFor('dashboard')
|
|
||||||
.setProperties({
|
|
||||||
showDetailsPane: false,
|
|
||||||
selectedContributionId: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { inject as service } from '@ember/service';
|
|
||||||
import Route from '@ember/routing/route';
|
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
|
|
||||||
export default Route.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
contributors: alias('kredits.contributors'),
|
|
||||||
|
|
||||||
model (params) {
|
|
||||||
return this.contributors.findBy('id', params.id);
|
|
||||||
},
|
|
||||||
|
|
||||||
setupController (controller, model) {
|
|
||||||
this._super(controller, model);
|
|
||||||
|
|
||||||
this.controllerFor('dashboard')
|
|
||||||
.setProperties({
|
|
||||||
showDetailsPane: true,
|
|
||||||
selectedContributorId: model.id
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
deactivate () {
|
|
||||||
this.controllerFor('dashboard')
|
|
||||||
.setProperties({
|
|
||||||
showDetailsPane: false,
|
|
||||||
selectedContributorId: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
+29
-4
@@ -1,9 +1,34 @@
|
|||||||
import Route from '@ember/routing/route';
|
import Ember from 'ember';
|
||||||
|
import Contributor from 'kredits-web/models/contributor';
|
||||||
|
|
||||||
export default Route.extend({
|
export default Ember.Route.extend({
|
||||||
|
|
||||||
redirect () {
|
kredits: Ember.inject.service(),
|
||||||
this.transitionTo('dashboard');
|
|
||||||
|
beforeModel(transition) {
|
||||||
|
const kredits = this.get('kredits');
|
||||||
|
|
||||||
|
if (kredits.get('web3') && kredits.get('web3Provided')) {
|
||||||
|
kredits.get('web3').eth.getAccounts((error, accounts) => {
|
||||||
|
if (error || accounts.length === 0) {
|
||||||
|
if (confirm('It looks like you have an Ethereum wallet available. Please unlock your account.')) {
|
||||||
|
transition.retry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
model() {
|
||||||
|
let kredits = this.get('kredits');
|
||||||
|
|
||||||
|
return Ember.RSVP.hash({
|
||||||
|
contributors: kredits.getContributors(),
|
||||||
|
totalSupply: kredits.getValueFromContract('tokenContract', 'totalSupply'),
|
||||||
|
proposals: kredits.getProposals(),
|
||||||
|
contributions: kredits.getContributions(),
|
||||||
|
newContributor: Contributor.create({ kind: 'person' })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Ember from 'ember';
|
||||||
|
import Proposal from 'kredits-web/models/proposal';
|
||||||
|
|
||||||
|
const {
|
||||||
|
Route,
|
||||||
|
RSVP,
|
||||||
|
inject: {
|
||||||
|
service
|
||||||
|
}
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
|
export default Route.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
|
model(params) {
|
||||||
|
const proposal = Proposal.create({
|
||||||
|
recipientAddress: params.recipient,
|
||||||
|
amount: params.amount,
|
||||||
|
url: params.url,
|
||||||
|
kind: params.kind || 'dev',
|
||||||
|
ipfsHash: params.ipfsHash
|
||||||
|
});
|
||||||
|
|
||||||
|
return RSVP.hash({
|
||||||
|
proposal,
|
||||||
|
contributors: this.get('kredits').getContributors()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
this._super(controller, model.proposal);
|
||||||
|
|
||||||
|
controller.set('contributors', model.contributors);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
|
|
||||||
export default Route.extend({
|
|
||||||
});
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
|
|
||||||
export default Route.extend({
|
|
||||||
});
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Ember from 'ember';
|
||||||
|
|
||||||
|
export default Ember.Route.extend({
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import Ember from 'ember';
|
||||||
|
import ipfsAPI from 'npm:ipfs-api';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default Ember.Service.extend({
|
||||||
|
|
||||||
|
ipfsInstance: null,
|
||||||
|
|
||||||
|
ipfs: function() {
|
||||||
|
if (this.get('ipfsInstance')) {
|
||||||
|
return this.get('ipfsInstance');
|
||||||
|
}
|
||||||
|
let ipfs = ipfsAPI(config.ipfs);
|
||||||
|
this.set('ipfsInstance', ipfs);
|
||||||
|
return ipfs;
|
||||||
|
}.property('ipfsInstance'),
|
||||||
|
|
||||||
|
storeFile(content) {
|
||||||
|
let ipfs = this.get('ipfs');
|
||||||
|
return ipfs.add(new ipfs.Buffer(content)).then(res => {
|
||||||
|
Ember.Logger.debug('[ipfs] stored content in IPFS', content, res[0].hash);
|
||||||
|
return res[0].hash;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getFile(hash) {
|
||||||
|
return this.get('ipfs').cat(hash, { buffer: true }).then(res => {
|
||||||
|
return res.toString();
|
||||||
|
}, err => {
|
||||||
|
Ember.Logger.error('[ipfs] error trying to fetch file', hash, err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
+224
-363
@@ -1,423 +1,284 @@
|
|||||||
import ethers from 'ethers';
|
import Ember from 'ember';
|
||||||
import Kredits from 'kredits-contracts';
|
import Web3 from 'npm:web3';
|
||||||
import RSVP from 'rsvp';
|
|
||||||
|
|
||||||
import Service from '@ember/service';
|
|
||||||
import EmberObject from '@ember/object';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import { alias, notEmpty } from '@ember/object/computed';
|
|
||||||
import { isEmpty, isPresent } from '@ember/utils';
|
|
||||||
|
|
||||||
import groupBy from 'kredits-web/utils/group-by';
|
|
||||||
import formatKredits from 'kredits-web/utils/format-kredits';
|
|
||||||
|
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
import Contributor from 'kredits-web/models/contributor'
|
import Contributor from 'kredits-web/models/contributor';
|
||||||
import Proposal from 'kredits-web/models/proposal'
|
import Proposal from 'kredits-web/models/proposal';
|
||||||
import Contribution from 'kredits-web/models/contribution'
|
import Contribution from 'kredits-web/models/contribution';
|
||||||
|
import kreditsContracts from 'npm:kredits-contracts';
|
||||||
|
import uuid from 'npm:uuid';
|
||||||
|
|
||||||
|
const {
|
||||||
|
Service,
|
||||||
|
isPresent,
|
||||||
|
inject: {
|
||||||
|
service
|
||||||
|
}
|
||||||
|
} = Ember;
|
||||||
|
|
||||||
export default Service.extend({
|
export default Service.extend({
|
||||||
|
|
||||||
currentBlock: null,
|
ipfs: service(),
|
||||||
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
|
|
||||||
currentUser: null,
|
|
||||||
contributors: null,
|
|
||||||
contributions: null,
|
|
||||||
proposals: null,
|
|
||||||
githubAccessToken: null,
|
|
||||||
|
|
||||||
currentUserIsContributor: notEmpty('currentUser'),
|
web3Instance: null,
|
||||||
currentUserIsCore: alias('currentUser.isCore'),
|
web3Provided: false, // Web3 provided (using Mist Browser, Metamask et al.)
|
||||||
hasAccounts: notEmpty('currentUserAccounts'),
|
|
||||||
|
|
||||||
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
|
web3: function() {
|
||||||
return this.contributions.filter(contribution => {
|
if (this.get('web3Instance')) {
|
||||||
return contribution.confirmedAt > this.currentBlock;
|
return this.get('web3Instance');
|
||||||
});
|
}
|
||||||
}),
|
|
||||||
|
|
||||||
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() {
|
let web3Instance;
|
||||||
return this.contributions
|
|
||||||
.filterBy('vetoed', false)
|
|
||||||
.filter(contribution => {
|
|
||||||
return contribution.confirmedAt <= this.currentBlock;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
kreditsByContributor: computed('contributionsUnconfirmed.@each.vetoed', 'contributors.[]', function() {
|
if (typeof window.web3 !== 'undefined') {
|
||||||
const contributionsUnconfirmed = this.contributionsUnconfirmed.filterBy('vetoed', false);
|
Ember.Logger.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask');
|
||||||
const contributionsGrouped = groupBy(contributionsUnconfirmed, 'contributorId');
|
web3Instance = window.web3;
|
||||||
const contributorsWithUnconfirmed = contributionsGrouped.map(c => c.value.toString());
|
this.set('web3Provided', true);
|
||||||
const contributorsWithOnlyConfirmed = this.contributors.reject(c => contributorsWithUnconfirmed.includes(c.id))
|
} else {
|
||||||
|
Ember.Logger.debug('[kredits] Creating new instance from npm module class');
|
||||||
|
let providerUrl = localStorage.getItem('config:web3ProviderUrl') || config.web3ProviderUrl;
|
||||||
|
let provider = new Web3.providers.HttpProvider(providerUrl);
|
||||||
|
web3Instance = new Web3(provider);
|
||||||
|
}
|
||||||
|
|
||||||
const kreditsByContributor = contributionsGrouped.map(c => {
|
this.set('web3Instance', web3Instance);
|
||||||
const amountUnconfirmed = c.items.mapBy('amount').reduce((a, b) => a + b);
|
window.web3 = web3Instance;
|
||||||
const contributor = this.contributors.findBy('id', c.value.toString());
|
|
||||||
|
|
||||||
return EmberObject.create({
|
return web3Instance;
|
||||||
contributor: contributor,
|
}.property('web3Instance'),
|
||||||
amountUnconfirmed: amountUnconfirmed,
|
|
||||||
amountConfirmed: contributor.totalKreditsEarned,
|
|
||||||
amountTotal: contributor.totalKreditsEarned + amountUnconfirmed
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
contributorsWithOnlyConfirmed.forEach(c => {
|
currentUserAccounts: function() {
|
||||||
kreditsByContributor.push(EmberObject.create({
|
return (this.get('web3Provided') && this.get('web3').eth.accounts) || [];
|
||||||
contributor: c,
|
}.property('web3Provided', 'web3'),
|
||||||
amountUnconfirmed: 0,
|
|
||||||
amountConfirmed: c.totalKreditsEarned,
|
|
||||||
amountTotal: c.totalKreditsEarned
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
|
|
||||||
return kreditsByContributor;
|
initializeKreditsContract() {
|
||||||
}),
|
let contract = null;
|
||||||
|
|
||||||
init () {
|
if (isPresent(config.contractMetadata)) {
|
||||||
this._super(...arguments);
|
if (localStorage.getItem('config:networkId')) {
|
||||||
this.set('contributors', []);
|
config.contractMetadata['networkId'] = localStorage.getItem('config:networkId');
|
||||||
this.set('proposals', []);
|
}
|
||||||
this.set('contributions', []);
|
contract = kreditsContracts(this.get('web3'), config.contractMetadata)['Kredits'];
|
||||||
|
} else {
|
||||||
|
contract = kreditsContracts(this.get('web3'))['Kredits'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return contract;
|
||||||
},
|
},
|
||||||
|
|
||||||
// This is called in the application route's beforeModel(). So it is
|
kreditsContract: function() {
|
||||||
// initialized before everything else, and we can rely on the ethProvider and
|
if (this.get('kreditsContractInstance')) {
|
||||||
// the potential currentUserAccounts to be available
|
return this.get('kreditsContractInstance');
|
||||||
getEthProvider () {
|
}
|
||||||
let ethProvider;
|
|
||||||
|
|
||||||
return new RSVP.Promise(async (resolve) => {
|
let contract = this.initializeKreditsContract();
|
||||||
function instantiateWithoutAccount () {
|
|
||||||
console.debug('[kredits] Creating new instance from npm module class');
|
|
||||||
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
|
|
||||||
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
|
|
||||||
resolve({
|
|
||||||
ethProvider: ethProvider,
|
|
||||||
ethSigner: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function instantiateWithAccount (web3Provider, context) {
|
this.set('kreditsContractInstance', contract);
|
||||||
console.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask');
|
// window.Kredits = contract;
|
||||||
ethProvider = new ethers.providers.Web3Provider(web3Provider);
|
return contract;
|
||||||
|
}.property('kreditsContractInstance', 'web3'),
|
||||||
|
|
||||||
const network = await ethProvider.getNetwork();
|
tokenContract: function() {
|
||||||
if (isPresent(config.web3RequiredNetwork) &&
|
if (this.get('tokenContractInstance')) {
|
||||||
network.name !== config.web3RequiredNetwork) {
|
return this.get('tokenContractInstance');
|
||||||
window.alert(`Please switch your Ethereum wallet to the "${config.web3RequiredNetwork}" network before connecting your account.`);
|
}
|
||||||
return instantiateWithoutAccount();
|
|
||||||
}
|
|
||||||
|
|
||||||
ethProvider.listAccounts().then(accounts => {
|
let contract = kreditsContracts(this.get('web3'), config.contractMetadata)['Token'];
|
||||||
context.set('currentUserAccounts', accounts);
|
this.set('tokenContractInstance', contract);
|
||||||
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
|
window.Token = contract;
|
||||||
resolve({
|
return contract;
|
||||||
ethProvider,
|
}.property('tokenContractInstance', 'web3'),
|
||||||
ethSigner
|
|
||||||
|
getValueFromContract(contract, contractMethod, ...args) {
|
||||||
|
Ember.Logger.debug('[kredits] read from contract', contract);
|
||||||
|
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
|
this.get(contract)[contractMethod](...args, (err, data) => {
|
||||||
|
if (err) { reject(err); return; }
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getContributorData(i) {
|
||||||
|
let promise = new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
|
this.getValueFromContract('kreditsContract', 'contributorAddresses', i).then(address => {
|
||||||
|
this.getValueFromContract('kreditsContract', 'contributors', address).then(person => {
|
||||||
|
this.getValueFromContract('tokenContract', 'balanceOf', address).then(balance => {
|
||||||
|
Ember.Logger.debug('[kredits] person', address, person);
|
||||||
|
|
||||||
|
let contributor = Contributor.create({
|
||||||
|
address: address,
|
||||||
|
ipfsHash: person[2],
|
||||||
|
kredits: balance.toNumber(),
|
||||||
|
isCurrentUser: this.get('currentUserAccounts').includes(address)
|
||||||
|
});
|
||||||
|
|
||||||
|
contributor.loadProfile(this.get('ipfs')).then(
|
||||||
|
() => resolve(contributor),
|
||||||
|
err => reject(err)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}).catch(err => reject(err));
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
|
||||||
|
getContributors() {
|
||||||
|
return this.getValueFromContract('kreditsContract', 'contributorsCount').then(contributorsCount => {
|
||||||
|
let contributors = [];
|
||||||
|
|
||||||
|
for(var i = 0; i < contributorsCount.toNumber(); i++) {
|
||||||
|
contributors.push(this.getContributorData(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.ethereum) {
|
return Ember.RSVP.all(contributors);
|
||||||
if (window.ethereum.isConnected()) {
|
});
|
||||||
instantiateWithAccount(window.ethereum, this);
|
},
|
||||||
|
|
||||||
|
getProposalData(i) {
|
||||||
|
let promise = new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
|
this.getValueFromContract('kreditsContract', 'proposals', i).then(p => {
|
||||||
|
let proposal = Proposal.create({
|
||||||
|
id : i,
|
||||||
|
creatorAddress : p[0],
|
||||||
|
recipientAddress : p[1],
|
||||||
|
votesCount : p[2].toNumber(),
|
||||||
|
votesNeeded : p[3].toNumber(),
|
||||||
|
amount : p[4].toNumber(),
|
||||||
|
executed : p[5],
|
||||||
|
url : p[6],
|
||||||
|
ipfsHash : p[7]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (proposal.get('ipfsHash')) {
|
||||||
|
proposal.loadContribution(this.get('ipfs')).then(
|
||||||
|
() => resolve(proposal),
|
||||||
|
err => reject(err)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
instantiateWithoutAccount();
|
Ember.Logger.warn('[kredits] proposal from blockchain is missing IPFS hash', proposal);
|
||||||
|
resolve(proposal);
|
||||||
}
|
}
|
||||||
|
}).catch(err => reject(err));
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
|
||||||
|
getProposals() {
|
||||||
|
return this.getValueFromContract('kreditsContract', 'proposalsCount').then(proposalsCount => {
|
||||||
|
let proposals = [];
|
||||||
|
|
||||||
|
for(var i = 0; i < proposalsCount.toNumber(); i++) {
|
||||||
|
proposals.push(this.getProposalData(i));
|
||||||
}
|
}
|
||||||
// Legacy dapp browsers...
|
|
||||||
else if (window.web3) {
|
return Ember.RSVP.all(proposals);
|
||||||
instantiateWithAccount(window.web3.currentProvider, this);
|
|
||||||
}
|
|
||||||
// Non-dapp browsers...
|
|
||||||
else {
|
|
||||||
instantiateWithoutAccount();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setup () {
|
vote(proposalId) {
|
||||||
return this.getEthProvider().then((providerAndSigner) => {
|
Ember.Logger.debug('[kredits] vote for', proposalId);
|
||||||
let kredits = new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
|
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
addresses: { Kernel: config.kreditsKernelAddress },
|
this.get('kreditsContract').vote(proposalId, (err, data) => {
|
||||||
apm: config.kreditsApmDomain,
|
if (err) { reject(err); return; }
|
||||||
ipfsConfig: config.ipfs
|
Ember.Logger.debug('[kredits] vote response', data);
|
||||||
|
resolve(data);
|
||||||
});
|
});
|
||||||
return kredits
|
|
||||||
.init()
|
|
||||||
.then(async (kredits) => {
|
|
||||||
this.set('kredits', kredits);
|
|
||||||
this.set('currentBlock', await kredits.provider.getBlockNumber());
|
|
||||||
|
|
||||||
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
|
|
||||||
this.getCurrentUser.then((contributorData) => {
|
|
||||||
this.set('currentUser', contributorData);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return kredits;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
totalSupply: computed(function() {
|
addContributor(contributor) {
|
||||||
return this.kredits.Token.functions.totalSupply().then(total => {
|
Ember.Logger.debug('[kredits] add contributor', contributor);
|
||||||
return formatKredits(total);
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
|
|
||||||
totalKreditsEarned: computed(function() {
|
contributor.setProperties({
|
||||||
return this.kredits.Contribution.functions.totalKreditsEarned(true)
|
kredits: 0,
|
||||||
.then(total => total.toNumber());
|
isCurrentUser: this.get('currentUserAccounts').includes(contributor.address)
|
||||||
}),
|
});
|
||||||
|
|
||||||
|
let id = uuid.v4();
|
||||||
|
|
||||||
loadInitialData () {
|
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
return this.getContributors()
|
this.get('ipfs').storeFile(contributor.serialize()).then(ipfsHash => {
|
||||||
.then(contributors => this.contributors.pushObjects(contributors))
|
contributor.set('ipfsHash', ipfsHash);
|
||||||
.then(() => this.getContributions())
|
this.get('kreditsContract').addContributor(contributor.address, contributor.name, contributor.ipfsHash, contributor.isCore, id, (err, data) => {
|
||||||
.then(contributions => this.contributions.pushObjects(contributions))
|
if (err) { reject(err); return; }
|
||||||
},
|
Ember.Logger.debug('[kredits] add contributor response', data);
|
||||||
|
resolve(contributor);
|
||||||
addContributor (attributes) {
|
|
||||||
if (attributes.github_uid) {
|
|
||||||
const uidInt = parseInt(attributes.github_uid);
|
|
||||||
attributes.github_uid = uidInt;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('[kredits] add contributor', attributes);
|
|
||||||
|
|
||||||
return this.kredits.Contributor.add(attributes, { gasLimit: 350000 })
|
|
||||||
.then(data => {
|
|
||||||
console.debug('[kredits] add contributor response', data);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateContributor (id, attributes) {
|
|
||||||
if (attributes.github_uid) {
|
|
||||||
const uidInt = parseInt(attributes.github_uid);
|
|
||||||
attributes.github_uid = uidInt;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('[kredits] update contributor', attributes);
|
|
||||||
|
|
||||||
return this.kredits.Contributor.updateProfile(id, attributes, { gasLimit: 350000 })
|
|
||||||
.then(data => {
|
|
||||||
console.debug('[kredits] updateProfile response', data);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getContributors () {
|
|
||||||
return this.kredits.Contributor.all()
|
|
||||||
.then((contributors) => {
|
|
||||||
return contributors.map((contributor) => {
|
|
||||||
return Contributor.create(contributor);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addContribution (attributes) {
|
addProposal(proposal) {
|
||||||
console.debug('[kredits] add contribution', attributes);
|
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
|
const {
|
||||||
|
recipientAddress,
|
||||||
|
amount,
|
||||||
|
url
|
||||||
|
} = proposal.getProperties('recipientAddress', 'amount', 'url');
|
||||||
|
|
||||||
return this.kredits.Contribution.addContribution(attributes, { gasLimit: 300000 })
|
this.get('ipfs').storeFile(proposal.serializeContribution()).then(ipfsHash => {
|
||||||
.then(data => {
|
this.get('kreditsContract').addProposal(recipientAddress, amount, url, ipfsHash, (err, data) => {
|
||||||
console.debug('[kredits] add contribution response', data);
|
if (err) { reject(err); return; }
|
||||||
attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
|
Ember.Logger.debug('[kredits] add proposal response', data);
|
||||||
const contribution = Contribution.create(attributes);
|
resolve();
|
||||||
contribution.set('confirmedAtBlock', data.blockNumber + 40320);
|
|
||||||
this.contributions.pushObject(contribution);
|
|
||||||
return contribution;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
//
|
|
||||||
// TODO Implement proposals with voting
|
|
||||||
//
|
|
||||||
|
|
||||||
// addProposal (attributes) {
|
|
||||||
// console.debug('[kredits] add proposal', attributes);
|
|
||||||
//
|
|
||||||
// return this.kredits.Proposal.addProposal(attributes)
|
|
||||||
// .then((data) => {
|
|
||||||
// console.debug('[kredits] add proposal response', data);
|
|
||||||
// attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
|
|
||||||
// return Proposal.create(attributes);
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
|
|
||||||
// getProposals () {
|
|
||||||
// return this.kredits.Proposal.all()
|
|
||||||
// .then(proposals => {
|
|
||||||
// return proposals.map(proposal => {
|
|
||||||
// proposal.contributor = this.contributors.findBy('id', proposal.contributorId.toString());
|
|
||||||
// return Proposal.create(proposal);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
|
|
||||||
getContributions () {
|
|
||||||
return this.kredits.Contribution.all({page: {size: 200}})
|
|
||||||
.then(contributions => {
|
|
||||||
return contributions.map(contribution => {
|
|
||||||
contribution.contributor = this.contributors.findBy('id', contribution.contributorId.toString());
|
|
||||||
return Contribution.create(contribution);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
vote (proposalId) {
|
getTokenMintedEvents(fromBlock=0, toBlock='latest') {
|
||||||
console.debug('[kredits] vote for', proposalId);
|
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
|
const minted = this.get('tokenContract')
|
||||||
|
.Minted({}, {fromBlock: fromBlock, toBlock: toBlock});
|
||||||
|
|
||||||
return this.kredits.Proposal.functions.vote(proposalId)
|
minted.get((err, res) => {
|
||||||
.then(data => {
|
if (err) {
|
||||||
console.debug('[kredits] vote response', data);
|
Ember.Logger.error('[kredits] Error reading Token#Minted events', res);
|
||||||
return data;
|
reject(err);
|
||||||
});
|
return;
|
||||||
},
|
|
||||||
|
|
||||||
veto (contributionId) {
|
|
||||||
console.debug('[kredits] veto against', contributionId);
|
|
||||||
|
|
||||||
return this.kredits.Contribution.functions.veto(contributionId, { gasLimit: 300000 })
|
|
||||||
.then(data => {
|
|
||||||
console.debug('[kredits] veto response', data);
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getCurrentUser: computed('kredits.provider', function() {
|
|
||||||
if (isEmpty(this.currentUserAccounts)) {
|
|
||||||
return RSVP.resolve();
|
|
||||||
}
|
|
||||||
return this.kredits.Contributor
|
|
||||||
.functions.getContributorIdByAddress(this.get('currentUserAccounts.firstObject'))
|
|
||||||
.then((id) => {
|
|
||||||
// check if the user is a contributor or not
|
|
||||||
if (id === 0) {
|
|
||||||
return RSVP.resolve();
|
|
||||||
} else {
|
|
||||||
return this.kredits.Contributor.getById(id);
|
|
||||||
}
|
}
|
||||||
|
Ember.Logger.warn('[kredits] Token#Minted events', res);
|
||||||
|
resolve(res);
|
||||||
});
|
});
|
||||||
}),
|
|
||||||
|
|
||||||
findProposalById(proposalId) {
|
|
||||||
return this.proposals.findBy('id', proposalId.toString());
|
|
||||||
},
|
|
||||||
|
|
||||||
// Contract events
|
|
||||||
addContractEventHandlers () {
|
|
||||||
this.kredits.Contributor
|
|
||||||
.on('ContributorProfileUpdated', this.handleContributorChange.bind(this))
|
|
||||||
.on('ContributorAccountUpdated', this.handleContributorChange.bind(this))
|
|
||||||
.on('ContributorAdded', this.handleContributorChange.bind(this))
|
|
||||||
|
|
||||||
this.kredits.Contribution
|
|
||||||
.on('ContributionAdded', this.handleContributionAdded.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
|
|
||||||
.on('Transfer', this.handleTransfer.bind(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleContributorChange (contributorId, ...args) {
|
|
||||||
console.debug('[kredits] Contributor add/update event received for ID', contributorId);
|
|
||||||
console.debug('[kredits] Event data:', args);
|
|
||||||
const contributorData = await this.kredits.Contributor.getById(contributorId);
|
|
||||||
const newContributor = Contributor.create(contributorData);
|
|
||||||
|
|
||||||
const oldContributor = this.contributors.findBy('id', contributorId.toString());
|
|
||||||
if (oldContributor) {
|
|
||||||
console.debug('[kredits] old contributor', oldContributor);
|
|
||||||
this.contributors.removeObject(oldContributor);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('[kredits] new contributor', newContributor);
|
|
||||||
this.contributors.pushObject(newContributor);
|
|
||||||
},
|
|
||||||
|
|
||||||
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) {
|
getContributions() {
|
||||||
console.debug('[kredits] ContributionVetoed event received for ', contributionId);
|
// this.getTokenMintedEvents().then(events => {
|
||||||
const contribution = this.contributions.findBy('id', contributionId);
|
let contributions = [];
|
||||||
console.debug('[kredits] contribution', contribution);
|
|
||||||
|
|
||||||
if (contribution) {
|
config.fixtures.mintedEvents.forEach(mintedEvent => {
|
||||||
contribution.set('vetoed', true);
|
contributions.push(this.getContributionData(mintedEvent));
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return Ember.RSVP.all(contributions);
|
||||||
|
// });
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: We may want to reload that proposal to show the voter as voted
|
getContributionData(mintedEvent) {
|
||||||
handleProposalVoted (proposalId, voterId, totalVotes) {
|
return new Ember.RSVP.Promise((resolve, reject) => {
|
||||||
let proposal = this.findProposalById(proposalId);
|
let contribution = Contribution.create({
|
||||||
|
recipientAddress: mintedEvent.args.recipientAddress,
|
||||||
|
contributorId: mintedEvent.args.contributorId,
|
||||||
|
amount: mintedEvent.args.amount,
|
||||||
|
ipfsHash: mintedEvent.args.reference
|
||||||
|
});
|
||||||
|
|
||||||
if (proposal) {
|
if (contribution.get('ipfsHash')) {
|
||||||
proposal.set('votesCount', totalVotes);
|
contribution.loadDetails(this.get('ipfs')).then(
|
||||||
}
|
() => resolve(contribution),
|
||||||
|
err => reject(err)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Ember.Logger.warn('[kredits] contribution from Minted event is missing IPFS hash', contribution);
|
||||||
|
resolve(contribution);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleProposalExecuted (proposalId, contributorId, amount) {
|
// logKreditsContract: function() {
|
||||||
let proposal = this.findProposalById(proposalId);
|
// Ember.Logger.debug('[kredits] kreditsContract', this.get('kreditsContract'));
|
||||||
|
// }.on('init')
|
||||||
|
|
||||||
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) {
|
|
||||||
value = value.toNumber();
|
|
||||||
|
|
||||||
this.contributors
|
|
||||||
.findBy('address', from)
|
|
||||||
.decrementProperty('balance', value);
|
|
||||||
|
|
||||||
this.contributors
|
|
||||||
.findBy('address', to)
|
|
||||||
.incrementProperty('balance', value);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
button, input[type=submit], .button {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.6rem 2rem;
|
|
||||||
background-color: rgba(22, 21, 40, 0.6);
|
|
||||||
border: 1px solid rgba(22, 21, 40, 1);
|
|
||||||
border-radius: 3px;
|
|
||||||
color: $primary-color;
|
|
||||||
font-weight: 500;
|
|
||||||
text-decoration: none;
|
|
||||||
text-transform: uppercase;
|
|
||||||
cursor: pointer;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
|
|
||||||
&+button, &+input[type=submit], &+.button {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(22, 21, 40, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active, &.active {
|
|
||||||
border-color: $primary-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[disabled] {
|
|
||||||
color: rgba(255,255,255,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.small {
|
|
||||||
font-size: 0.86rem;
|
|
||||||
padding: 0.2rem 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.danger {
|
|
||||||
color: $red;
|
|
||||||
background-color: rgba(40, 21, 21, 0.6);
|
|
||||||
border-color: rgba(40, 21, 21, 1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(40, 21, 21, 0.8);
|
|
||||||
}
|
|
||||||
&:active, &.active {
|
|
||||||
border-color: $red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.green {
|
|
||||||
color: $green;
|
|
||||||
background-color: rgba(21, 40, 21, 0.6);
|
|
||||||
border-color: rgba(21, 40, 21, 1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(21, 40, 21, 0.8);
|
|
||||||
}
|
|
||||||
&:active, &.active {
|
|
||||||
border-color: $green;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon {
|
|
||||||
svg {
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-right: 1rem;
|
|
||||||
|
|
||||||
.fg {
|
|
||||||
fill: $primary-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,11 +5,4 @@ $green: #97fb68;
|
|||||||
$yellow: #fbe468;
|
$yellow: #fbe468;
|
||||||
$red: #fb6868;
|
$red: #fb6868;
|
||||||
|
|
||||||
$primary-color: $blue;
|
$primaryColor: $blue;
|
||||||
|
|
||||||
$body-text-color: #fff;
|
|
||||||
|
|
||||||
#topbar {
|
|
||||||
background-color: rgba(255, 0, 255, .2);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
+18
-107
@@ -1,120 +1,31 @@
|
|||||||
main {
|
section {
|
||||||
padding: 1rem 2rem;
|
@include outer-container;
|
||||||
|
margin-bottom: 8rem;
|
||||||
@include media-max(small) {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&#dashboard {
|
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
grid-row-gap: 2rem;
|
|
||||||
grid-template-areas:
|
|
||||||
"stats"
|
|
||||||
"contributions";
|
|
||||||
}
|
|
||||||
|
|
||||||
&.center-column {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
section {
|
|
||||||
width: 600px;
|
|
||||||
max-width: 100%;
|
|
||||||
|
|
||||||
header {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
.content {
|
|
||||||
a {
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.text-lg {
|
|
||||||
p {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
line-height: 1.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.text-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
&.mg-bottom-md {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.actions {
|
|
||||||
text-align: center;
|
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 550px) {
|
|
||||||
main {
|
|
||||||
&#dashboard {
|
|
||||||
grid-column-gap: 4rem;
|
|
||||||
grid-row-gap: 2rem;
|
|
||||||
grid-template-columns: 2fr 4fr;
|
|
||||||
grid-template-areas:
|
|
||||||
"stats contributions";
|
|
||||||
|
|
||||||
&.with-details {
|
|
||||||
grid-column-gap: 3rem;
|
|
||||||
grid-template-columns: 2fr 4fr 2fr;
|
|
||||||
grid-template-areas:
|
|
||||||
"stats contributions details";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main section {
|
|
||||||
margin-bottom: 5rem;
|
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
margin-top: 2rem;
|
margin-top: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-max(small) {
|
@include media($mobile) {
|
||||||
margin-bottom: 5rem;
|
margin-bottom: 5rem;
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
margin-bottom: 3rem;
|
@include span-columns(12);
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
text-align: center;
|
||||||
|
// background-color: purple;
|
||||||
|
|
||||||
&.with-nav {
|
@include media($mobile) {
|
||||||
display: grid;
|
@include span-columns(10);
|
||||||
grid-template-columns: auto 5rem;
|
@include shift(1);
|
||||||
grid-template-areas:
|
padding-bottom: 2rem;
|
||||||
"title" "actions";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
div.content {
|
||||||
display: inline-block;
|
@include span-columns(12);
|
||||||
// padding-left: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-max(small) {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
$breakpoints-max: (
|
|
||||||
small: 600px,
|
|
||||||
medium: 960px,
|
|
||||||
large: 1280px
|
|
||||||
);
|
|
||||||
|
|
||||||
$breakpoints-min: (
|
|
||||||
small: 601px,
|
|
||||||
medium: 961px,
|
|
||||||
large: 1281px
|
|
||||||
);
|
|
||||||
|
|
||||||
@mixin media-max($screen-size) {
|
|
||||||
@if map-has-key($breakpoints-max, $screen-size) {
|
|
||||||
@media (max-width: map-get($breakpoints-max, $screen-size)) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
// Debugging
|
|
||||||
@warn "'#{$screen-size}' has not been declared as a breakpoint."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin media-min($screen-size) {
|
|
||||||
@if map-has-key($breakpoints-min, $screen-size) {
|
|
||||||
@media (min-width: map-get($breakpoints-min, $screen-size)) {
|
|
||||||
@content;
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
// Debugging
|
|
||||||
@warn "'#{$screen-size}' has not been declared as a breakpoint."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+38
-48
@@ -10,23 +10,23 @@
|
|||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@import "mediaqueries";
|
@import "bourbon";
|
||||||
|
@import "settings/neat";
|
||||||
|
@import "neat";
|
||||||
|
@import "settings/breakpoints";
|
||||||
@import "layout";
|
@import "layout";
|
||||||
@import "colors";
|
@import "colors";
|
||||||
|
|
||||||
$font-family-sans: 'Open Sans', sans-serif;
|
$font-family-sans: 'Open Sans', sans-serif;
|
||||||
|
|
||||||
html, body {
|
|
||||||
font-family: $font-family-sans;
|
|
||||||
font-weight: 300;
|
|
||||||
font-size: 12px;
|
|
||||||
color: $body-text-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-image: linear-gradient(to bottom, rgba(22, 21, 40, .4), rgba(0, 0, 0, .75)), url('/img/bg.jpg');
|
background-image: url('/img/bg.jpg');
|
||||||
background-repeat: none;
|
background-repeat: none;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
font-family: $font-family-sans;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 300;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 900px) {
|
@media screen and (min-width: 900px) {
|
||||||
@@ -41,38 +41,28 @@ h1, h2, h3, h4, h5, input, button {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
color: $primary-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
section {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.4rem;
|
font-size: 2.8rem;
|
||||||
color: $primary-color;
|
color: $primaryColor;
|
||||||
|
@include media($mobile) {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&#people {
|
&#contributors {
|
||||||
.content {
|
.content {
|
||||||
p.stats {
|
p.stats {
|
||||||
margin-bottom: 1rem;
|
padding-top: 3rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: white;
|
color: white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
span.number {
|
span.number {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
@include media($mobile) {
|
||||||
}
|
padding-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&#contributions-by-type {
|
|
||||||
.chart {
|
|
||||||
width: 50%;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
|
|
||||||
@include media-max(small) {
|
|
||||||
width: 75%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,37 +71,37 @@ section {
|
|||||||
.actions {
|
.actions {
|
||||||
padding-top: 3rem;
|
padding-top: 3rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: $primary-color;
|
color: $primaryColor;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@include media-max(small) {
|
@include media($mobile) {
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: $primary-color;
|
color: $primaryColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 550px) {
|
button, input[type=submit] {
|
||||||
section {
|
display: inline-block;
|
||||||
h2 {
|
border: 1px solid rgba(22, 21, 40, 1);
|
||||||
font-size: 2rem;
|
background-color: rgba(22, 21, 40, 0.6);
|
||||||
}
|
color: $primaryColor;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(22, 21, 40, 0.8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@import "buttons";
|
|
||||||
@import "forms";
|
|
||||||
|
|
||||||
@import "components/contribution-list";
|
|
||||||
@import "components/contribution-details";
|
|
||||||
@import "components/contributor-list";
|
|
||||||
@import "components/contributor-profile";
|
|
||||||
@import "components/external-account-link";
|
|
||||||
@import "components/loading-spinner";
|
@import "components/loading-spinner";
|
||||||
|
@import "components/contributor-list";
|
||||||
|
@import "components/add-contributor";
|
||||||
@import "components/proposal-list";
|
@import "components/proposal-list";
|
||||||
@import "components/topbar";
|
@import "components/contribution-list";
|
||||||
@import "components/topbar-account-panel";
|
|
||||||
@import "components/user-avatar";
|
|
||||||
|
|||||||
@@ -1,37 +1,20 @@
|
|||||||
section#add-contributor,
|
section#add-contributor, section#add-proposal {
|
||||||
section#add-contribution,
|
|
||||||
section#add-proposal,
|
|
||||||
section#signup {
|
|
||||||
|
|
||||||
form {
|
form {
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
&.mg-bottom-md {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.label {
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.actions {
|
&.actions {
|
||||||
padding-top: 1.5rem;
|
padding-top: 1rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
a {
|
a {
|
||||||
color: $primary-color;
|
color: $primaryColor;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=text], select {
|
input[type=text], select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -40,22 +23,17 @@ section#signup {
|
|||||||
background-color: rgba(22, 21, 40, 0.3);
|
background-color: rgba(22, 21, 40, 0.3);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
transition: border-color 0.1s linear;
|
|
||||||
|
|
||||||
&:focus, &.valid {
|
&:focus, &.valid {
|
||||||
background-color: rgba(22, 21, 40, 0.6);
|
background-color: rgba(22, 21, 40, 0.6);
|
||||||
}
|
}
|
||||||
&:focus {
|
@include placeholder {
|
||||||
border-color: $blue;
|
|
||||||
}
|
|
||||||
&::placeholder {
|
|
||||||
color: rgba(238, 238, 238, 0.5);
|
color: rgba(238, 238, 238, 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
-moz-appearance: none;
|
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background-color: rgba(22, 21, 40, 0.6);
|
background-color: rgba(22, 21, 40, 0.6);
|
||||||
@@ -1,88 +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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,89 +1,19 @@
|
|||||||
main section {
|
|
||||||
@include media-max(small) {
|
|
||||||
&#contributions-unconfirmed, &#contributions-confirmed {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-filter {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
|
|
||||||
p {
|
|
||||||
padding: 0.2rem 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
&+ label {
|
|
||||||
margin-left: 3.6rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.contribution-list {
|
ul.contribution-list {
|
||||||
clear: both;
|
clear: both;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: grid;
|
// display: block;
|
||||||
grid-template-columns: auto 5rem 5rem;
|
padding: 0 1.2rem;
|
||||||
grid-row-gap: 0.5rem;
|
line-height: 4.2rem;
|
||||||
padding: 0.8rem 1.2rem;
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
background-color: rgba(255,255,255,0.1);
|
||||||
font-size: 1.2rem;
|
font-size: 1.4rem;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
border-top: 1px solid rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.confirmed {
|
|
||||||
grid-template-columns: auto 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.vetoed {
|
|
||||||
grid-template-columns: auto 5rem;
|
|
||||||
text-decoration: line-through;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background-color: rgba(255,255,255,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
align-self: center;
|
|
||||||
margin: 0;
|
|
||||||
line-height: 2rem;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&.kredits-amount, &.voting {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
&.title {
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
color: $body-text-color;
|
|
||||||
}
|
|
||||||
a:hover, a:active {
|
|
||||||
color: $primary-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
line-height: 1.4em;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category {
|
.category {
|
||||||
color: $blue;
|
color: $blue;
|
||||||
padding-right: 0.2rem;
|
padding-right: 0.2rem;
|
||||||
@@ -93,59 +23,17 @@ ul.contribution-list {
|
|||||||
&.docs { color: $green; }
|
&.docs { color: $green; }
|
||||||
&.ops { color: $purple; }
|
&.ops { color: $purple; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount {
|
.amount {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.symbol {
|
.symbol {
|
||||||
font-size: 0.8rem;
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
padding-left: 0.2rem;
|
padding-left: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipient {
|
.recipient {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.votes {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: $primary-color;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 550px) {
|
|
||||||
ul.proposal-list {
|
|
||||||
li {
|
|
||||||
grid-template-columns: auto 10rem;
|
|
||||||
grid-row-gap: 0.5rem;
|
|
||||||
|
|
||||||
p {
|
|
||||||
&.kredits-amount, &.voting {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.unconfirmed {
|
|
||||||
p {
|
|
||||||
&.kredits-amount, &.voting {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.description {
|
|
||||||
grid-column-start: span 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.voting {
|
|
||||||
grid-column-start: span 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,48 +1,79 @@
|
|||||||
table.contributor-list {
|
table.contributor-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
border-top: 1px solid rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
|
||||||
&.selected {
|
&.current-user {
|
||||||
background-color: rgba(255,255,255,0.2);
|
background-color: rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.current-user {
|
&.metadata {
|
||||||
font-weight: 500;
|
height: 0;
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 0 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $primaryColor;
|
||||||
|
&:hover, &:active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 0;
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline;
|
||||||
|
&+li {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
height: auto;
|
||||||
|
ul {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
padding: 0 1.2rem;
|
padding: 0 1.2rem;
|
||||||
line-height: 3.6rem;
|
line-height: 4.2rem;
|
||||||
|
|
||||||
&.person {
|
&.person {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 1.2rem;
|
font-size: 1.4rem;
|
||||||
|
|
||||||
img.avatar {
|
img.avatar {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
vertical-align: middle;
|
||||||
margin-right: 0.2rem;
|
margin-right: 0.2rem;
|
||||||
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.kredits {
|
&.kredits {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
.amount {
|
.amount {
|
||||||
font-size: 1.2rem;
|
font-size: 1.4rem;
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.symbol {
|
.symbol {
|
||||||
font-size: 0.8rem;
|
font-size: 1rem;
|
||||||
padding-left: 0.2rem;
|
padding-left: 0.2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
section#contributor-profile {
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
header {
|
|
||||||
z-index: 2;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin: 0 auto;
|
|
||||||
border: 3px solid rgba(255,255,255,0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
margin: -7.2rem 0 1.5rem;
|
|
||||||
padding: 6rem 1.2rem 2rem;
|
|
||||||
|
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p.role {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.external-accounts {
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
&+li {
|
|
||||||
margin-left: 0.6rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
.button {
|
|
||||||
margin-bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
.external-accounts a {
|
|
||||||
display: inline-block;
|
|
||||||
width: 4rem;
|
|
||||||
height: 4rem;
|
|
||||||
padding: 0.8rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
color: $body-text-color;
|
|
||||||
border-radius: 2rem;
|
|
||||||
|
|
||||||
span.site {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
display: block;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.fg {
|
|
||||||
fill: $body-text-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover, &:active {
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
.fg {
|
|
||||||
fill: $primary-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,9 +11,9 @@
|
|||||||
margin-top: 12rem;
|
margin-top: 12rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
color: $primary-color;
|
color: $primaryColor;
|
||||||
|
|
||||||
@include media-max(small) {
|
@include media($mobile) {
|
||||||
margin-top: 6rem;
|
margin-top: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
#path-comet {
|
#path-comet {
|
||||||
fill: $primary-color;
|
fill: $primaryColor;
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
|
|
||||||
animation-name: pulse;
|
animation-name: pulse;
|
||||||
|
|||||||
@@ -4,44 +4,16 @@ ul.proposal-list {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: grid;
|
// display: block;
|
||||||
grid-template-columns: auto 5rem;
|
padding: 0 1.2rem;
|
||||||
grid-row-gap: 0.5rem;
|
line-height: 4.2rem;
|
||||||
padding: 1rem 1.2rem;
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
background-color: rgba(255,255,255,0.1);
|
||||||
font-size: 1.2rem;
|
font-size: 1.4rem;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
border-top: 1px solid rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
|
||||||
align-self: center;
|
|
||||||
margin: 0;
|
|
||||||
line-height: 2rem;
|
|
||||||
|
|
||||||
&.kredits-amount, &.voting {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.description {
|
|
||||||
grid-column-start: span 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.voting {
|
|
||||||
grid-column-start: span 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
line-height: 1.4em;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category {
|
.category {
|
||||||
color: $blue;
|
color: $blue;
|
||||||
padding-right: 0.2rem;
|
padding-right: 0.2rem;
|
||||||
@@ -51,61 +23,30 @@ ul.proposal-list {
|
|||||||
&.docs { color: $green; }
|
&.docs { color: $green; }
|
||||||
&.ops { color: $purple; }
|
&.ops { color: $purple; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.amount {
|
.amount {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.symbol {
|
.symbol {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding-left: 0.2rem;
|
padding-left: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recipient {
|
.recipient {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.votes {
|
.votes {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: $primary-color;
|
color: $primaryColor;
|
||||||
margin-right: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
float: right;
|
||||||
|
margin-top: 1.1rem;
|
||||||
|
height: 2rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
padding: 0 0.6rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 550px) {
|
|
||||||
ul.proposal-list {
|
|
||||||
li {
|
|
||||||
grid-template-columns: auto 10rem;
|
|
||||||
grid-row-gap: 0.5rem;
|
|
||||||
|
|
||||||
p {
|
|
||||||
&.kredits-amount, &.voting {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.unconfirmed {
|
|
||||||
p {
|
|
||||||
&.kredits-amount, &.voting {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.description {
|
|
||||||
grid-column-start: span 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.voting {
|
|
||||||
grid-column-start: span 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.description {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
header#topbar section#user-account {
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button + button {
|
|
||||||
margin-left: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#topbar {
|
|
||||||
padding: 0 1rem;
|
|
||||||
height: 3rem;
|
|
||||||
line-height: 3rem;
|
|
||||||
background-color: rgba(0,0,0,.3);
|
|
||||||
|
|
||||||
@include media-min(medium) {
|
|
||||||
padding: 0 2rem;
|
|
||||||
height: 4rem;
|
|
||||||
line-height: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
display: inline-block;
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section#user-account {
|
|
||||||
display: inline-block;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
img.avatar {
|
|
||||||
width: 2rem;
|
|
||||||
height: 2rem;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-right: 0.2rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
|
|
||||||
&.medium {
|
|
||||||
margin: 0;
|
|
||||||
width: 10rem;
|
|
||||||
height: 10rem;
|
|
||||||
border-radius: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.large {
|
|
||||||
margin: 0;
|
|
||||||
width: 256px;
|
|
||||||
height: 256px;
|
|
||||||
border-radius: 128px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
$mobile: new-breakpoint(max-width 600px);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// $visual-grid: true;
|
||||||
|
// $visual-grid-color: red;
|
||||||
|
// $visual-grid-index: front;
|
||||||
|
// $visual-grid-opacity: 0.5;
|
||||||
|
$max-width: em(500, 12);
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
<header id="topbar">
|
<main>
|
||||||
<h1>{{link-to "Kosmos Kredits" "dashboard"}}</h1>
|
{{outlet}}
|
||||||
{{topbar-account-panel}}
|
</main>
|
||||||
</header>
|
|
||||||
|
|
||||||
{{outlet}}
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<main class="center-column">
|
|
||||||
|
|
||||||
<section id="add-contribution">
|
|
||||||
<header>
|
|
||||||
<h2>Submit a contribution</h2>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
{{add-contribution contributors=sortedContributors save=(action "save")}}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<main class="center-column">
|
|
||||||
|
|
||||||
<section id="add-contribution">
|
|
||||||
<header>
|
|
||||||
<h2>Re-submit contribution #{{model.id}}</h2>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
{{add-contribution attributes=attributes contributors=sortedContributors save=(action "save")}}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user