Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
ee664c06c1
|
|||
|
62b618b657
|
|||
| 348dc03429 | |||
|
d159fca816
|
|||
|
a582f41fde
|
|||
|
2082b51c5b
|
|||
|
e543708b42
|
|||
|
0b0dea095f
|
|||
|
c7eb81450c
|
|||
|
ca1ccf6d93
|
|||
| 0b79d48be9 | |||
|
f8cc453d7e
|
|||
|
08bedda288
|
|||
| 7c8ca45b27 | |||
|
ff3fe49dd0
|
|||
|
92056517a6
|
|||
|
11f19c8344
|
|||
|
0f8d7d84ca
|
|||
| e1780109aa | |||
|
8c2f6aa38b
|
|||
|
afbce03a4a
|
|||
|
1f2dd9d198
|
|||
|
e4814923f9
|
|||
|
ad015f1cee
|
|||
|
0c5aebf7b4
|
|||
| 52f7e86fc9 | |||
|
fb66bd4f0d
|
|||
|
19553b9777
|
|||
| 441dc21894 | |||
| 0732911e13 | |||
| 57934ae7a3 | |||
| 1e240b6539 | |||
| 2c773c147a | |||
|
e05449a7ff
|
|||
|
44b631c688
|
|||
|
6cd62d3764
|
|||
|
4c749bca35
|
|||
|
466354cff7
|
|||
|
721c330f3c
|
|||
| d07963ad6b | |||
|
eef9fa1aba
|
|||
|
77dbde8bb4
|
|||
| 64bd2477c7 | |||
|
aed29ff982
|
|||
| f3f158ff26 | |||
| 2a8fd3b001 | |||
|
087cb706d2
|
|||
|
b58870a03c
|
|||
|
ccda6ec354
|
|||
|
2045b94951
|
|||
|
be16a57043
|
|||
|
ae06385039
|
|||
|
7b1c3c813c
|
|||
|
ec50e7deef
|
|||
|
c3b1bd2f4c
|
|||
|
93e2a23ac7
|
|||
|
a41d1b1dbd
|
|||
|
c0ccf83061
|
|||
|
4ab5c3c372
|
|||
|
5d4fb1c423
|
|||
|
361ccb103d
|
|||
|
2bf590886c
|
|||
|
c7b6f9e3e7
|
|||
|
c1eb93c596
|
|||
|
d85235ff88
|
|||
|
63675a1e13
|
|||
|
87bc8f5cb5
|
|||
|
4abc1593cb
|
|||
|
25b2a8b0e5
|
|||
|
b5700092a8
|
|||
|
7288d75237
|
|||
|
444ec62a66
|
|||
|
326d46b1a1
|
|||
|
083c873631
|
|||
|
37dc91da57
|
|||
|
7c86edcaaa
|
|||
|
e4d2fdfce4
|
|||
|
4722064337
|
|||
|
9f35add07f
|
|||
|
e86541574d
|
|||
|
5f90bc23bc
|
|||
|
1dd63b2c7a
|
|||
|
f1bd20a6f4
|
|||
|
ec6b72008d
|
|||
|
dec71c6f4f
|
|||
|
52172ea065
|
|||
|
f63d14d7b4
|
|||
|
ef0fc11edf
|
|||
|
083334ed18
|
|||
|
69a6c850bf
|
|||
|
8074d2d0e3
|
|||
|
11c3078b55
|
|||
|
fb267813fb
|
|||
|
a96cca879d
|
|||
|
4849a755dc
|
|||
|
eaac245f4e
|
|||
|
95743330e4
|
|||
|
4a9dd5ff09
|
|||
|
3b127b0e81
|
|||
|
769317777c
|
|||
| 174073fc42 | |||
| 733e5e3683 | |||
| 090d1d0856 | |||
| e047cc45ba | |||
| c64a503546 | |||
| 13021d665d | |||
| b5dbb2449d | |||
| 302c3315a3 | |||
| bac4c1c425 | |||
| c54d754c89 | |||
| 63dd5430bc | |||
| eb2c613ac9 | |||
| bf6d9dbc31 | |||
| c491f10ec3 | |||
| d4b546bfcd | |||
| 69141a31c4 | |||
| e7c9620bbe | |||
| 832de3c06f | |||
| 50e1fc728c | |||
| 871731110b | |||
| f996c89dfe | |||
| 1d8e9ddf5d | |||
| 352c9fe3ab | |||
| e2a944ed5b | |||
| c57d4162d7 | |||
| 41cc37d3eb | |||
| 081584a508 | |||
| 3cc01cce4e | |||
| cedbe3cb3d | |||
| b8da04c145 | |||
| 009caa53e5 | |||
| 9a1f543921 | |||
| 22300c2980 | |||
| 018132034d | |||
| f5d7916556 | |||
| 12df28f7fd | |||
| 2f4f350a93 | |||
| 4946b3311d | |||
| cc2f91afce | |||
| aa351cbe17 | |||
| 065c635580 | |||
| 67e3c9a7c3 | |||
| 4f4b3c5c17 | |||
| 4f6bafa31e | |||
| 94846c6e00 | |||
| 45d553866e | |||
| 8ac6e3ad3c | |||
| 215ad1d640 | |||
| 3f4cc39067 | |||
| 0a6b6180b9 | |||
| 536868002f | |||
| 04a7f9139d | |||
| 863b542e3e | |||
| 23d86f1502 | |||
| 314cf58c29 | |||
| 9c063b82f1 | |||
| b397e4faf6 | |||
| 256197094e | |||
| 9898d4bb1d | |||
| c321dc51b0 |
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
|||||||
@@ -1,8 +1,22 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
|
parser: '@babel/eslint-parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2018,
|
requireConfigFile: false,
|
||||||
sourceType: 'module'
|
ecmaVersion: 2020,
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
legacyDecorators: true
|
||||||
|
},
|
||||||
|
babelOptions: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
plugins: [
|
||||||
|
['@babel/plugin-proposal-decorators', { legacy: true }],
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
console: true
|
console: true
|
||||||
@@ -19,7 +33,16 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'ember/avoid-leaking-state-in-ember-objects': 'warn',
|
'ember/avoid-leaking-state-in-ember-objects': 'warn',
|
||||||
'no-console': 'off'
|
'no-console': 'off',
|
||||||
|
'ember/no-jquery': 'error',
|
||||||
|
'ember/require-computed-property-dependencies': 'warn',
|
||||||
|
'ember/no-computed-properties-in-native-classes': 'warn',
|
||||||
|
'ember/no-observers': 'warn',
|
||||||
|
'ember/no-classic-classes': 'warn',
|
||||||
|
'ember/no-classic-components': 'warn',
|
||||||
|
'ember/no-controller-access-in-routes': 'warn',
|
||||||
|
'ember/no-actions-hash': 'warn',
|
||||||
|
'ember/require-tagless-components': 'warn'
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
// node files
|
// node files
|
||||||
@@ -35,8 +58,7 @@ module.exports = {
|
|||||||
'server/**/*.js'
|
'server/**/*.js'
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: 'script',
|
sourceType: 'script'
|
||||||
ecmaVersion: 2015
|
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: false,
|
browser: false,
|
||||||
|
|||||||
@@ -1,10 +1,94 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: 'recommended',
|
extends: 'octane',
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
'simple-unless': false,
|
'simple-unless': false,
|
||||||
'no-nested-interactive': false
|
'no-nested-interactive': false,
|
||||||
}
|
'no-html-comments': false
|
||||||
|
},
|
||||||
|
|
||||||
|
ignore: [
|
||||||
|
'kredits-web/templates/components/**',
|
||||||
|
'app/templates/components/**'
|
||||||
|
],
|
||||||
|
|
||||||
|
pending: [
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/dashboard",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/add-contribution/template",
|
||||||
|
"only": [
|
||||||
|
"no-action",
|
||||||
|
"no-curly-component-invocation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/add-contributor/template",
|
||||||
|
"only": [
|
||||||
|
"no-action",
|
||||||
|
"no-curly-component-invocation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/contribution-list/template",
|
||||||
|
"only": [
|
||||||
|
"no-action",
|
||||||
|
"no-curly-component-invocation"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/contributor-list/template",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/topbar-account-panel/template",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/contributions/new",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/contributions/resubmit",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/contributors/edit",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/contributors/new",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/signup/eth-account",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/templates/signup/index",
|
||||||
|
"only": [
|
||||||
|
"no-action"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,21 @@
|
|||||||
---
|
---
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "10"
|
- "14"
|
||||||
|
- "16"
|
||||||
|
|
||||||
sudo: false
|
sudo: false
|
||||||
dist: xenial
|
dist: precise
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
email:
|
email: false
|
||||||
on_success: never
|
|
||||||
on_failure: always
|
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
chrome: stable
|
chrome: stable
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
npm: false
|
||||||
- $HOME/.npm
|
|
||||||
|
|
||||||
env:
|
branches:
|
||||||
global:
|
only:
|
||||||
# See https://git.io/vdao3 for details.
|
- master
|
||||||
- JOBS=1
|
|
||||||
|
|
||||||
script:
|
|
||||||
- npm run lint:hbs
|
|
||||||
- npm run lint:js
|
|
||||||
- npm test
|
|
||||||
|
|||||||
@@ -5,23 +5,24 @@
|
|||||||
This README outlines the details of collaborating on this Ember application.
|
This README outlines the details of collaborating on this Ember application.
|
||||||
A short introduction of this app could easily go here.
|
A short introduction of this app could easily go here.
|
||||||
|
|
||||||
## Prerequisites
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
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)
|
||||||
* [Ember CLI](https://ember-cli.com/)
|
* [Ember CLI](https://ember-cli.com/)
|
||||||
* [Google Chrome](https://google.com/chrome/)
|
* [Google Chrome](https://google.com/chrome/) (only for running tests)
|
||||||
|
|
||||||
## Installation
|
### Installation
|
||||||
|
|
||||||
* `git clone git@github.com:67P/kredits-web.git` this repository
|
* `git clone git@github.com:67P/kredits-web.git` this repository
|
||||||
* `cd kredits-web`
|
* `cd kredits-web`
|
||||||
* `npm install`
|
* `npm install`
|
||||||
|
|
||||||
## Running / Development
|
### Building/running for development
|
||||||
|
|
||||||
|
|
||||||
* `ember serve` - by default kredits-web connects to the Ethreum Kovan network
|
* `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).
|
||||||
@@ -29,11 +30,11 @@ You will need the following things properly installed on your computer.
|
|||||||
|
|
||||||
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.
|
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
|
||||||
|
|
||||||
Make use of the many generators for code, try `ember help generate` for more details
|
Make use of the many generators for code, try `ember help generate` for more details
|
||||||
|
|
||||||
### Running Tests
|
### Running tests
|
||||||
|
|
||||||
* `ember test`
|
* `ember test`
|
||||||
* `ember test --server`
|
* `ember test --server`
|
||||||
@@ -55,17 +56,15 @@ _(You need collaborator permissions on the 5apps Deploy project.)_
|
|||||||
|
|
||||||
`npm run deploy`
|
`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
|
The smart contracts and their JavaScript wrapper library are developed in the
|
||||||
[kredits-contracts](https://github.com/67P/kredits-contracts) repo/package.
|
[kredits-contracts](https://github.com/67P/kredits-contracts) repo/package.
|
||||||
|
|
||||||
You can run `kredits-web` on your machine, against a local, simulated Ethereum
|
You can run `kredits-web` on your machine, against a local, simulated
|
||||||
network, provided e.g. by [ganache](http://truffleframework.com/ganache/).
|
blockchain. [kredits-contracts](https://github.com/67P/kredits-contracts)
|
||||||
|
contains all the tools to start and set up such a simulated network, as well as
|
||||||
[kredits-contracts](https://github.com/67P/kredits-contracts) holds all the tools
|
to deploy the Kredits smart contracts to it.
|
||||||
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:
|
These are the basic steps to get up and running:
|
||||||
|
|
||||||
@@ -78,37 +77,33 @@ Run a local IPFS deamon.
|
|||||||
|
|
||||||
#### 2. kredits-contracts
|
#### 2. kredits-contracts
|
||||||
|
|
||||||
Get your local Ethereum development node running. (See [kredits-contracts README](https://github.com/67P/kredits-contracts)
|
Run a local devchain with test data. (See [kredits-contracts
|
||||||
for details.
|
README](https://github.com/67P/kredits-contracts) for details.
|
||||||
|
|
||||||
* Clone [kredits-contracts](https://github.com/67P/kredits-contracts)
|
* Clone [kredits-contracts](https://github.com/67P/kredits-contracts)
|
||||||
* `npm install`
|
* `npm install`
|
||||||
* `npm run devchain` - runs a local development chain
|
* `npm run devchain` - runs a local development chain
|
||||||
* `npm run bootstrap` - bootstrap runs deploys all the contracts
|
* `npm run bootstrap` - deploys all contracts and seeds test data
|
||||||
* `npm link` - make the `kredits-contracts` module linkable as `kredits-contracts` on your machine (currently broken :( )
|
* `npm link` - makes the `kredits-contracts` module linkable as `kredits-contracts` on your machine
|
||||||
|
|
||||||
#### 3. kredits-web
|
#### 3. kredits-web
|
||||||
|
|
||||||
With IPFS and Ethereum/ganache running, you can now start this Ember app.
|
With IPFS and the local devchain running, you can now link the contracts and
|
||||||
|
start the Ember app:
|
||||||
|
|
||||||
* `npm link kredits-contracts` - link the local `kredits-contracts` package
|
* `npm link kredits-contracts` - links the local `kredits-contracts` package (has to be done again after every `npm install`)
|
||||||
* `npm run start:local` - WEB3_PROVIDER_URL=http://localhost:7545 must be set for local settings
|
* `npm run start:local` - runs the Ember app with WEB3_PROVIDER_URL=http://localhost:8545 set
|
||||||
|
|
||||||
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
|
#### 4. Metamask network
|
||||||
|
|
||||||
Switch the network in Metamask to "Custom RPC" with the RPC URL `http://localhost:7545`.
|
If you want to interact with the local contracts via a Web3 wallet, switch the
|
||||||
|
network to a "Custom RPC" one, with the RPC URL `http://localhost:8545`.
|
||||||
|
|
||||||
#### IPFS
|
#### IPFS
|
||||||
|
|
||||||
Install IPFS with your favorite package manager and run
|
If you haven't configured your IPFS node for CORS yet, you can do so by running
|
||||||
|
the following commands:
|
||||||
|
|
||||||
ipfs init (on initial installation)
|
|
||||||
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["localhost:4200"]'
|
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["localhost:4200"]'
|
||||||
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]'
|
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]'
|
||||||
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
|
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import Application from '@ember/application';
|
import Application from '@ember/application';
|
||||||
import Resolver from './resolver';
|
import Resolver from 'ember-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({
|
export default class App extends Application {
|
||||||
modulePrefix: config.modulePrefix,
|
modulePrefix = config.modulePrefix;
|
||||||
podModulePrefix: config.podModulePrefix,
|
podModulePrefix = config.podModulePrefix;
|
||||||
Resolver
|
Resolver = Resolver;
|
||||||
});
|
}
|
||||||
|
|
||||||
loadInitializers(App, config.modulePrefix);
|
loadInitializers(App, config.modulePrefix);
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import { computed } from '@ember/object';
|
import { computed } from '@ember/object';
|
||||||
import { and, notEmpty } from '@ember/object/computed';
|
import { and, notEmpty } from '@ember/object/computed';
|
||||||
import { isEmpty } from '@ember/utils';
|
import { assign } from '@ember/polyfills';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
attributes: null,
|
attributes: null,
|
||||||
contributors: Object.freeze([]),
|
|
||||||
|
contributors: computed('kredits.contributorsSorted.[]', function() {
|
||||||
|
return this.kredits.contributorsSorted.map(c => {
|
||||||
|
return {
|
||||||
|
id: c.id.toString(),
|
||||||
|
name: c.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
isValidContributor: notEmpty('contributorId'),
|
isValidContributor: notEmpty('contributorId'),
|
||||||
isValidKind: notEmpty('kind'),
|
isValidKind: notEmpty('kind'),
|
||||||
@@ -24,19 +34,17 @@ export default Component.extend({
|
|||||||
init () {
|
init () {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.set('defaultDate', moment().startOf('hour').toDate());
|
this.set('defaultDate', moment().startOf('hour').toDate());
|
||||||
|
this.set('defaultAttr', {
|
||||||
|
contributorId: null,
|
||||||
|
kind: null,
|
||||||
|
date: this.defaultDate,
|
||||||
|
amount: null,
|
||||||
|
description: null,
|
||||||
|
url: null,
|
||||||
|
details: null
|
||||||
|
});
|
||||||
|
|
||||||
// Default attributes used by reset
|
this.set('attributes', assign({}, this.defaultAttr, this.attributes));
|
||||||
if (isEmpty(this.attributes)) {
|
|
||||||
this.set('attributes', {
|
|
||||||
contributorId: null,
|
|
||||||
kind: null,
|
|
||||||
date: this.defaultDate,
|
|
||||||
amount: null,
|
|
||||||
description: null,
|
|
||||||
url: null,
|
|
||||||
details: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reset();
|
this.reset();
|
||||||
},
|
},
|
||||||
@@ -47,7 +55,9 @@ export default Component.extend({
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
submit () {
|
submit (evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
if (!this.isValid) {
|
if (!this.isValid) {
|
||||||
alert('Invalid data. Please review and try again.');
|
alert('Invalid data. Please review and try again.');
|
||||||
return;
|
return;
|
||||||
@@ -55,6 +65,8 @@ export default Component.extend({
|
|||||||
|
|
||||||
const attributes = this.getProperties(Object.keys(this.attributes));
|
const attributes = this.getProperties(Object.keys(this.attributes));
|
||||||
|
|
||||||
|
attributes.contributorId = parseInt(this.contributorId);
|
||||||
|
|
||||||
let dateInput = (attributes.date instanceof Array) ?
|
let dateInput = (attributes.date instanceof Array) ?
|
||||||
attributes.date[0] : attributes.date;
|
attributes.date[0] : attributes.date;
|
||||||
|
|
||||||
@@ -72,7 +84,5 @@ export default Component.extend({
|
|||||||
})
|
})
|
||||||
.finally(() => this.set('inProgress', false));
|
.finally(() => this.set('inProgress', false));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<form {{action "submit" on="submit"}}>
|
<form onsubmit={{action "submit"}}>
|
||||||
<label>
|
<label>
|
||||||
<p class="label">Contributor:</p>
|
<p class="label">Contributor:</p>
|
||||||
<p>
|
<p>
|
||||||
<select required onchange={{action (mut contributorId) value="target.value"}}>
|
<select required onchange={{action (mut this.contributorId) value="target.value"}}>
|
||||||
<option value="" selected disabled hidden></option>
|
<option value="" selected disabled hidden></option>
|
||||||
{{#each contributors as |contributor|}}
|
{{#each this.contributors as |contributor|}}
|
||||||
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.name}}</option>
|
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
@@ -13,14 +13,17 @@
|
|||||||
<label>
|
<label>
|
||||||
<p class="label">Kind:</p>
|
<p class="label">Kind:</p>
|
||||||
<p>
|
<p>
|
||||||
<select required onchange={{action (mut kind) value="target.value"}}>
|
<select required onchange={{action (mut this.kind) value="target.value"}}>
|
||||||
<option value="" selected disabled hidden></option>
|
<option value="" selected disabled hidden></option>
|
||||||
<option value="community" selected={{eq kind "community"}}>Community</option>
|
<option value="bureaucracy" selected={{eq this.kind "bureaucracy"}}>Bureaucracy</option>
|
||||||
<option value="design" selected={{eq kind "design"}}>Design</option>
|
<option value="community" selected={{eq this.kind "community"}}>Community</option>
|
||||||
<option value="dev" selected={{eq kind "dev"}}>Development</option>
|
<option value="design" selected={{eq this.kind "design"}}>Design</option>
|
||||||
<option value="docs" selected={{eq kind "docs"}}>Documentation</option>
|
<option value="dev" selected={{eq this.kind "dev"}}>Development</option>
|
||||||
<option value="ops" selected={{eq kind "ops"}}>IT Operations</option>
|
<option value="docs" selected={{eq this.kind "docs"}}>Documentation</option>
|
||||||
<option value="special" selected={{eq kind "special"}}>Special</option>
|
<option value="ops" selected={{eq this.kind "ops"}}>IT Operations</option>
|
||||||
|
<option value="outreach" selected={{eq this.kind "outreach"}}>Outreach</option>
|
||||||
|
<option value="qa" selected={{eq this.kind "qa"}}>Quality Assurance</option>
|
||||||
|
<option value="special" selected={{eq this.kind "special"}}>Special</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
@@ -28,12 +31,12 @@
|
|||||||
<p class="label">Date:</p>
|
<p class="label">Date:</p>
|
||||||
<p>
|
<p>
|
||||||
{{ember-flatpickr
|
{{ember-flatpickr
|
||||||
date=date
|
date=this.date
|
||||||
defaultDate=defaultDate
|
defaultDate=this.defaultDate
|
||||||
maxDate=defaultDate
|
maxDate=this.defaultDate
|
||||||
enableTime=true
|
enableTime=true
|
||||||
time_24hr=true
|
time_24hr=true
|
||||||
onChange=(action (mut date))
|
onChange=(action (mut this.date))
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
@@ -42,33 +45,33 @@
|
|||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
placeholder="500"
|
placeholder="500"
|
||||||
value=amount
|
value=this.amount
|
||||||
class=(if isValidAmount "valid" "")}}
|
class=(if this.isValidAmount "valid" "")}}
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<p class="label">Description:</p>
|
<p class="label">Description:</p>
|
||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
value=description
|
value=this.description
|
||||||
class=(if isValidDescription "valid" "")}}
|
class=(if this.isValidDescription "valid" "")}}
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<p class="label">URL (optional):</p>
|
<p class="label">URL (optional):</p>
|
||||||
<p>
|
<p>
|
||||||
{{input type="text"
|
{{input type="text"
|
||||||
value=url
|
value=this.url
|
||||||
class=(if isValidUrl "valid" "")}}
|
class=(if this.isValidUrl "valid" "")}}
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{{#if details}}
|
{{#if this.details}}
|
||||||
<label>
|
<label>
|
||||||
<p class="label">Details:</p>
|
<p class="label">Details:</p>
|
||||||
<p>
|
<p>
|
||||||
<pre>
|
<pre>
|
||||||
{{details}}
|
{{this.details}}
|
||||||
</pre>
|
</pre>
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
@@ -76,8 +79,8 @@
|
|||||||
|
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
{{input type="submit"
|
{{input type="submit"
|
||||||
disabled=inProgress
|
disabled=this.inProgress
|
||||||
value=(if inProgress "Processing" "Save")}}
|
value=(if this.inProgress "Processing" "Save")}}
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { isAddress } from 'web3-utils';
|
|||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
router: service(),
|
router: service(),
|
||||||
|
kredits: service(),
|
||||||
|
|
||||||
attributes: null,
|
attributes: null,
|
||||||
|
|
||||||
@@ -60,9 +60,15 @@ export default Component.extend({
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
submit () {
|
submit (evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
if (!this.kredits.currentUserIsCore) {
|
||||||
|
window.alert('Only core team members can edit profiles. Please ask someone to set you up.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.isValid) {
|
if (!this.isValid) {
|
||||||
alert('Invalid data. Please review and try again.');
|
window.alert('Invalid data. Please review and try again.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,56 @@
|
|||||||
<form {{action "submit" on="submit"}}>
|
<form onsubmit={{action "submit"}}>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-account">Ethereum account</label>
|
<label for="c-account">Ethereum account</label>
|
||||||
{{input name="account" id="c-account"
|
<Input @type="text"
|
||||||
type="text" value=account
|
@value={{this.account}}
|
||||||
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
|
name="account" id="c-account"
|
||||||
class=(if isValidAccount "valid" "")}}
|
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
|
||||||
|
class={{if this.isValidAccount "valid" ""}} />
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-kind">Kind</label>
|
<label for="c-kind">Kind</label>
|
||||||
<select required onchange={{action (mut kind) value="target.value"}} id="c-kind">
|
<select required onchange={{action (mut this.kind) value="target.value"}} id="c-kind">
|
||||||
<option value="person" selected={{eq kind "person"}}>Person</option>
|
<option value="person" selected={{eq this.kind "person"}}>Person</option>
|
||||||
<option value="organization" selected={{eq kind "organization"}}>Organization</option>
|
<option value="organization" selected={{eq this.kind "organization"}}>Organization</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-name">Name</label>
|
<label for="c-name">Name</label>
|
||||||
{{input name="name" type="text" value=name placeholder="Zero Cool"
|
{{input name="name" type="text" value=this.name placeholder="Zero Cool"
|
||||||
class=(if isValidName "valid" "") id="c-name"}}
|
class=(if this.isValidName "valid" "") id="c-name"}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-url">URL</label>
|
<label for="c-url">URL</label>
|
||||||
{{input name="url" type="text" value=url placeholder="http://zerocool.bit"
|
{{input name="url" type="text" value=this.url placeholder="http://zerocool.bit"
|
||||||
class=(if isValidURL "valid" "") id="c-url"}}
|
class=(if this.isValidURL "valid" "") id="c-url"}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-github-uid">GitHub UID</label>
|
<label for="c-github-uid">GitHub UID</label>
|
||||||
{{input name="github_uid" type="text" value=github_uid placeholder="2342"
|
{{input name="github_uid" type="text" value=this.github_uid placeholder="2342"
|
||||||
class=(if isValidGithubUID "valid" "") id="c-github-uid"}}
|
class=(if this.isValidGithubUID "valid" "") id="c-github-uid"}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-github-username">GitHub username</label>
|
<label for="c-github-username">GitHub username</label>
|
||||||
{{input name="github_username" type="text" value=github_username placeholder="zerocool"
|
{{input name="github_username" type="text" value=this.github_username placeholder="zerocool"
|
||||||
class=(if isValidGithubUsername "valid" "") id="c-github-username"}}
|
class=(if this.isValidGithubUsername "valid" "") id="c-github-username"}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-gitea-username">Gitea username</label>
|
<label for="c-gitea-username">Gitea username</label>
|
||||||
{{input name="gitea_username" type="text" value=gitea_username placeholder="zerocool"
|
{{input name="gitea_username" type="text" value=this.gitea_username placeholder="zerocool"
|
||||||
class=(if isValidGiteaUsername "valid" "") id="c-gitea-username"}}
|
class=(if this.isValidGiteaUsername "valid" "") id="c-gitea-username"}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-wiki-username">Wiki username</label>
|
<label for="c-wiki-username">Wiki username</label>
|
||||||
{{input name="wiki_username" type="text" value=wiki_username placeholder="ZeroCool"
|
{{input name="wiki_username" type="text" value=this.wiki_username placeholder="ZeroCool"
|
||||||
class=(if isValidWikiUsername "valid" "") id="c-wiki-username"}}
|
class=(if this.isValidWikiUsername "valid" "") id="c-wiki-username"}}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-zoom-display-name">Zoom display name</label>
|
<label for="c-zoom-display-name">Zoom display name</label>
|
||||||
{{input name="zoom_display_name" type="text" value=zoom_display_name placeholder="Zero Cool"
|
{{input name="zoom_display_name" type="text" value=this.zoom_display_name placeholder="Zero Cool"
|
||||||
class=(if isValidZoomDisplayName "valid" "") id="c-zoom-display-name"}}
|
class=(if this.isValidZoomDisplayName "valid" "") id="c-zoom-display-name"}}
|
||||||
</p>
|
</p>
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
{{input type="submit" disabled=inProgress
|
{{input type="submit" disabled=this.inProgress
|
||||||
value=(if inProgress "Processing" "Save")}}
|
value=(if this.inProgress "Processing" "Save")}}
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import moment from 'moment';
|
||||||
|
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
||||||
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
|
export default class AddExpenseItemComponent extends Component {
|
||||||
|
@tracked amount = '0';
|
||||||
|
@tracked currency = 'EUR';
|
||||||
|
@tracked date = moment().startOf('hour').toDate();
|
||||||
|
@tracked title = '';
|
||||||
|
@tracked description = '';
|
||||||
|
@tracked url = '';
|
||||||
|
@tracked tags = '';
|
||||||
|
|
||||||
|
defaultDate = moment().startOf('hour').toDate();
|
||||||
|
|
||||||
|
currencies = [
|
||||||
|
{ code: 'EUR' },
|
||||||
|
{ code: 'USD' }
|
||||||
|
];
|
||||||
|
|
||||||
|
get isValidAmount () {
|
||||||
|
return isValidAmount(this.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
get amountInputClass () {
|
||||||
|
return this.isValidTotal ? 'valid' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
validateForm () {
|
||||||
|
const formEl = document.querySelector('form#add-expense-item');
|
||||||
|
const inputFields = formEl.querySelectorAll('input');
|
||||||
|
inputFields.forEach(i => i.classList.remove('invalid'));
|
||||||
|
let validity = true;
|
||||||
|
|
||||||
|
if (!this.isValidAmount) {
|
||||||
|
document.querySelector('input[name=expense-amount]').classList.add('invalid');
|
||||||
|
validity = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!formEl.checkValidity()) {
|
||||||
|
inputFields.forEach(i => {
|
||||||
|
if (!i.validity.valid) {
|
||||||
|
i.classList.add('invalid');
|
||||||
|
validity = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return validity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateCurrency(event) {
|
||||||
|
this.currency = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
submit (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
let dateInput = (this.date instanceof Array) ?
|
||||||
|
this.date[0] : this.date;
|
||||||
|
|
||||||
|
const [ date ] = moment(dateInput).utcOffset(0, true)
|
||||||
|
.toISOString()
|
||||||
|
.split('T');
|
||||||
|
|
||||||
|
const isValid = this.validateForm();
|
||||||
|
if (!isValid) return false;
|
||||||
|
|
||||||
|
const expense = {
|
||||||
|
amount: parseFloat(this.amount),
|
||||||
|
currency: this.currency,
|
||||||
|
date: date,
|
||||||
|
title: this.title,
|
||||||
|
description: isPresent(this.description) ? this.description : undefined,
|
||||||
|
url: isPresent(this.url) ? this.url : undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPresent(this.tags)) {
|
||||||
|
expense.tags = this.tags.split(',')
|
||||||
|
.map(t => t.trim())
|
||||||
|
.filter(t => t.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.args.addExpenseItem(expense);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<form id="add-expense-item" {{on "submit" this.submit}} novalidate>
|
||||||
|
<fieldset class="horizontal">
|
||||||
|
<label>
|
||||||
|
<p class="label">Amount:</p>
|
||||||
|
<p>
|
||||||
|
<Input @name="expense-amount"
|
||||||
|
@type="text"
|
||||||
|
@placeholder="10"
|
||||||
|
@value={{this.amount}}
|
||||||
|
@required={{true}}
|
||||||
|
@pattern="([0-9]*[.])?[0-9]+"
|
||||||
|
@class={{this.amountInputClass}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Currency:</p>
|
||||||
|
<p>
|
||||||
|
<select required name="expense-currency" {{on "change" this.updateCurrency}}>
|
||||||
|
<option value="" selected disabled hidden></option>
|
||||||
|
{{#each this.currencies as |currency|}}
|
||||||
|
<option value={{currency.code}} selected={{eq this.currency currency.code}}>{{currency.code}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<label>
|
||||||
|
<p class="label">Date:</p>
|
||||||
|
<p>
|
||||||
|
<EmberFlatpickr @date={{this.date}}
|
||||||
|
@defaultDate={{this.defaultDate}}
|
||||||
|
@maxDate={{this.defaultDate}}
|
||||||
|
@enableTime={{false}}
|
||||||
|
@onChange={{fn (mut this.date)}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Title:</p>
|
||||||
|
<p>
|
||||||
|
<Input @name="expense-title"
|
||||||
|
@type="text"
|
||||||
|
@value={{this.title}}
|
||||||
|
@required={{true}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Description (optional):</p>
|
||||||
|
<p>
|
||||||
|
<Input @name="expense-description" @type="text" @value={{this.description}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">URL (optional):</p>
|
||||||
|
<p>
|
||||||
|
<Input @name="expense-url" @type="url" @value={{this.url}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">Tags (comma-separated, optional):</p>
|
||||||
|
<p>
|
||||||
|
<Input @name="expense-tags" @type="text" @value={{this.tags}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p class="actions">
|
||||||
|
<Input @type="submit" @value="Add" @class="green"
|
||||||
|
@title="Add item to reimbursement" />
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import { and, notEmpty } from '@ember/object/computed';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
|
|
||||||
attributes: null,
|
|
||||||
contributors: Object.freeze([]),
|
|
||||||
|
|
||||||
isValidContributor: notEmpty('contributorId'),
|
|
||||||
isValidAmount: computed('amount', function() {
|
|
||||||
return parseInt(this.amount, 10) > 0;
|
|
||||||
}),
|
|
||||||
isValidDescription: notEmpty('description'),
|
|
||||||
isValidUrl: notEmpty('url'),
|
|
||||||
isValid: and('isValidContributor',
|
|
||||||
'isValidAmount',
|
|
||||||
'isValidDescription'),
|
|
||||||
|
|
||||||
init () {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
// Default attributes used by reset
|
|
||||||
this.set('attributes', {
|
|
||||||
contributorId: null,
|
|
||||||
kind: 'community',
|
|
||||||
amount: null,
|
|
||||||
description: null,
|
|
||||||
url: null,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.reset();
|
|
||||||
},
|
|
||||||
|
|
||||||
reset: function() {
|
|
||||||
this.setProperties(this.attributes);
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
submit() {
|
|
||||||
if (!this.isValid) {
|
|
||||||
alert('Invalid data. Please review and try again.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let attributes = Object.keys(this.attributes);
|
|
||||||
let proposal = this.getProperties(attributes);
|
|
||||||
let saved = this.save(proposal);
|
|
||||||
|
|
||||||
// The promise handles inProgress
|
|
||||||
this.set('inProgress', saved);
|
|
||||||
|
|
||||||
saved.then(() => {
|
|
||||||
this.reset();
|
|
||||||
window.scroll(0,0);
|
|
||||||
window.alert('Proposal added.');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<form {{action "submit" on="submit"}}>
|
|
||||||
<p>
|
|
||||||
<select required onchange={{action (mut contributorId) value="target.value"}}>
|
|
||||||
<option value="" selected disabled hidden>Contributor</option>
|
|
||||||
{{#each contributors as |contributor|}}
|
|
||||||
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.github_username}}</option>
|
|
||||||
{{/each}}
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<select required onchange={{action (mut kind) value="target.value"}}>
|
|
||||||
<option value="community" selected={{eq kind "community"}}>Community</option>
|
|
||||||
<option value="design" selected={{eq kind "design"}}>Design</option>
|
|
||||||
<option value="dev" selected={{eq kind "dev"}}>Development</option>
|
|
||||||
<option value="docs" selected={{eq kind "docs"}}>Documentation</option>
|
|
||||||
<option value="ops" selected={{eq kind "ops"}}>IT Operations</option>
|
|
||||||
</select>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{input type="text"
|
|
||||||
placeholder="100"
|
|
||||||
value=amount
|
|
||||||
class=(if isValidAmount "valid" "")}}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{input type="text"
|
|
||||||
placeholder="Description"
|
|
||||||
value=description
|
|
||||||
class=(if isValidDescription "valid" "")}}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{{input type="text"
|
|
||||||
placeholder="URL (optional)"
|
|
||||||
value=url
|
|
||||||
class=(if isValidUrl "valid" "")}}
|
|
||||||
</p>
|
|
||||||
<p class="actions">
|
|
||||||
{{input type="submit"
|
|
||||||
disabled=(is-pending inProgress)
|
|
||||||
value=(if (is-pending inProgress) "Processing" "Save")}}
|
|
||||||
{{#link-to "index"}}Back{{/link-to}}
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { A } from '@ember/array';
|
||||||
|
import { scheduleOnce } from '@ember/runloop';
|
||||||
|
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
||||||
|
import readFileContent from 'kredits-web/utils/read-file-content';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default class AddReimbursementComponent extends Component {
|
||||||
|
@service router;
|
||||||
|
@service kredits;
|
||||||
|
@service exchangeRates;
|
||||||
|
|
||||||
|
@alias('kredits.contributorsSorted') contributors;
|
||||||
|
|
||||||
|
@tracked recipientId = null;
|
||||||
|
@tracked title = '';
|
||||||
|
@tracked total = '0';
|
||||||
|
@tracked expenses = A([]);
|
||||||
|
@tracked expenseFormVisible = true;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.exchangeRates.fetchRates();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isValidTotal () {
|
||||||
|
return isValidAmount(this.total);
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalInputClass () {
|
||||||
|
return this.isValidTotal ? 'valid' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalEUR () {
|
||||||
|
const expenses = this.expenses.filterBy('currency', 'EUR');
|
||||||
|
if (expenses.length > 0) {
|
||||||
|
return expenses.mapBy('amount')
|
||||||
|
.reduce((summation, current) => summation + current);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get totalUSD () {
|
||||||
|
const expenses = this.expenses.filterBy('currency', 'USD');
|
||||||
|
if (expenses.length > 0) {
|
||||||
|
return expenses.mapBy('amount')
|
||||||
|
.reduce((summation, current) => summation + current);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get submitButtonEnabled () {
|
||||||
|
return this.isValidTotal &&
|
||||||
|
(this.expenses.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get submitButtonDisabled () {
|
||||||
|
return !this.submitButtonEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToExpenseItemForm () {
|
||||||
|
const anchor = document.getElementById('new-expense-item');
|
||||||
|
anchor.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTotalAmountFromFiat() {
|
||||||
|
let btcAmount = parseFloat(this.total);
|
||||||
|
|
||||||
|
if (this.exchangeRates.btceur > 0 && this.totalEUR > 0) {
|
||||||
|
btcAmount += (this.totalEUR / this.exchangeRates.btceur);
|
||||||
|
}
|
||||||
|
if (this.exchangeRates.btcusd > 0 && this.totalUSD > 0) {
|
||||||
|
btcAmount += (this.totalUSD / this.exchangeRates.btcusd);
|
||||||
|
}
|
||||||
|
if (this.totalUSD === 0 && this.totalEUR === 0) {
|
||||||
|
btcAmount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.total = btcAmount.toFixed(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO use ember-concurrency here
|
||||||
|
// https://github.com/67P/kredits-web/pull/209#discussion_r1064234421
|
||||||
|
@action
|
||||||
|
async addExpensesFromFile (evt) {
|
||||||
|
const content = await readFileContent(evt.target.files[0]);
|
||||||
|
const expenses = JSON.parse(content);
|
||||||
|
|
||||||
|
if (expenses instanceof Array) {
|
||||||
|
for (const item of expenses) {
|
||||||
|
this.addExpenseItem(item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Expenses in file must be a list of items:");
|
||||||
|
console.debug(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateContributor (event) {
|
||||||
|
this.recipientId = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
showExpenseForm () {
|
||||||
|
this.expenseFormVisible = true;
|
||||||
|
scheduleOnce('afterRender', this, this.scrollToExpenseItemForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
addExpenseItem (expenseItem) {
|
||||||
|
this.expenses.pushObject(expenseItem);
|
||||||
|
this.updateTotalAmountFromFiat();
|
||||||
|
this.expenseFormVisible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
removeExpenseItem (expenseItem) {
|
||||||
|
this.expenses.removeObject(expenseItem);
|
||||||
|
this.updateTotalAmountFromFiat();
|
||||||
|
|
||||||
|
if (this.expenses.length === 0) {
|
||||||
|
this.expenseFormVisible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
submit (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.kredits.currentUser) { window.alert('You need to connect your RSK account first.'); return false }
|
||||||
|
if (!this.kredits.currentUserIsCore) { window.alert('Only core contributors can submit reimbursements.'); return false }
|
||||||
|
|
||||||
|
const contributor = this.contributors.findBy('id', parseInt(this.recipientId));
|
||||||
|
|
||||||
|
const attributes = {
|
||||||
|
amount: parseInt(parseFloat(this.total) * 100000000), // convert to sats
|
||||||
|
token: config.tokens['BTC'],
|
||||||
|
recipientId: parseInt(this.recipientId),
|
||||||
|
title: `Expenses covered by ${contributor.name}`,
|
||||||
|
description: this.description,
|
||||||
|
url: this.url,
|
||||||
|
expenses: JSON.parse(JSON.stringify((this.expenses)))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inProgress = true;
|
||||||
|
|
||||||
|
this.kredits.addReimbursement(attributes)
|
||||||
|
.then((/* reimbursement */) => {
|
||||||
|
this.router.transitionTo('budget');
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.error('Could not add reimbursement:', e);
|
||||||
|
window.alert('Something went wrong. Please check the browser console.')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.inProgress = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<form {{on "submit" this.submit}} novalidate>
|
||||||
|
<label>
|
||||||
|
<p class="label">Contributor:</p>
|
||||||
|
<p>
|
||||||
|
<select required {{on "change" this.updateContributor}}>
|
||||||
|
<option value="" selected disabled hidden></option>
|
||||||
|
{{#each this.contributors as |contributor|}}
|
||||||
|
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<fieldset class="horizontal thirds total-amounts">
|
||||||
|
<label>
|
||||||
|
<p class="label">Total amount (BTC):</p>
|
||||||
|
<p>
|
||||||
|
<Input @type="text"
|
||||||
|
@placeholder="0.0015"
|
||||||
|
@value={{this.total}}
|
||||||
|
@required={{true}}
|
||||||
|
@pattern="([0-9]*[.])?[0-9]+"
|
||||||
|
@class={{this.totalInputClass}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">EUR total</p>
|
||||||
|
<p>
|
||||||
|
<Input @type="text"
|
||||||
|
@name="total-eur"
|
||||||
|
@value={{this.totalEUR}}
|
||||||
|
@disabled={{true}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<p class="label">USD total</p>
|
||||||
|
<p>
|
||||||
|
<Input @type="text"
|
||||||
|
@name="total-usd"
|
||||||
|
@value={{this.totalUSD}}
|
||||||
|
@disabled={{true}} />
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<h3>Expense items</h3>
|
||||||
|
{{#if this.expenses}}
|
||||||
|
<ul class="expense-list">
|
||||||
|
{{#each this.expenses as |expense|}}
|
||||||
|
<li>
|
||||||
|
<div class="description" rowspan="2">
|
||||||
|
<h4>
|
||||||
|
<span class="date">{{expense.date}}:</span>
|
||||||
|
<span class="title">{{expense.title}}</span>
|
||||||
|
</h4>
|
||||||
|
<p class="description">{{expense.description}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="amount">
|
||||||
|
{{fmt-fiat-currency expense.amount expense.currency}}
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button {{on "click" (fn this.removeExpenseItem expense)}}
|
||||||
|
class="danger small" type="button">delete</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<p class="actions">
|
||||||
|
<button {{on "click" this.showExpenseForm}}
|
||||||
|
class="green small" type="button">+ Add another item</button>
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p>No line items yet.</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<p class="actions">
|
||||||
|
{{#if this.inProgress}}
|
||||||
|
<Input @type="submit" @value="Submitting..." @disabled={{true}}
|
||||||
|
@title="Submit/propose this reimbursement" />
|
||||||
|
{{else}}
|
||||||
|
<Input @type="submit" @value="Submit" @disabled={{this.submitButtonDisabled}}
|
||||||
|
@title="Submit/propose this reimbursement" />
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{#if this.expenseFormVisible}}
|
||||||
|
<h3 id="new-expense-item">New expense item</h3>
|
||||||
|
<AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} />
|
||||||
|
{{/if}}
|
||||||
|
</form>
|
||||||
|
<form id="add-expenses-from-file">
|
||||||
|
<h3>Add expense items from file</h3>
|
||||||
|
<input type="file" multiple="false"
|
||||||
|
onchange={{fn this.addExpensesFromFile}}
|
||||||
|
accept="application/json" />
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
|
||||||
|
export default class BudgetBalancesComponent extends Component {
|
||||||
|
@service communityFunds;
|
||||||
|
@alias('communityFunds.balances') balances;
|
||||||
|
|
||||||
|
get loading () {
|
||||||
|
return !this.communityFunds.balancesLoaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<table class="token-balances {{if this.loading 'loading'}}">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Token</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Fiat value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each this.balances as |balance|}}
|
||||||
|
<tr>
|
||||||
|
<th>{{balance.token.symbol}}</th>
|
||||||
|
<td class="amount">{{balance.confirmed_balance}}</td>
|
||||||
|
<td class="fiat-amount">~{{balance.balanceUSD}} USD</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<div class="chart">
|
<div class="chart">
|
||||||
{{ember-chart type="doughnut"
|
<EmberChart @type="doughnut"
|
||||||
data=chartData
|
@data={{this.chartData}}
|
||||||
options=chartOptions
|
@options={{this.chartOptions}}
|
||||||
width=200
|
@width="200" @height="200" />
|
||||||
height=200}}
|
|
||||||
</div>
|
</div>
|
||||||
@@ -24,12 +24,9 @@ export default Component.extend({
|
|||||||
contributors: sort('kredits.contributors', 'contributorsSorting'),
|
contributors: sort('kredits.contributors', 'contributorsSorting'),
|
||||||
|
|
||||||
contributorsActive: computed('contributors.[]', 'contributions', function() {
|
contributorsActive: computed('contributors.[]', 'contributions', function() {
|
||||||
let activeIds = this.contributions.mapBy('contributorId')
|
const activeIds = new Set(this.contributions.mapBy('contributorId'));
|
||||||
.map(id => id.toString())
|
|
||||||
.uniq();
|
return this.contributors.filter(c => activeIds.has(c.id));
|
||||||
return this.contributors.filter(c => {
|
|
||||||
return activeIds.includes(c.id.toString());
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
contributionKinds: computed('contributions.[]', function() {
|
contributionKinds: computed('contributions.[]', function() {
|
||||||
@@ -44,7 +41,7 @@ export default Component.extend({
|
|||||||
c.amount <= 500) { included = false; }
|
c.amount <= 500) { included = false; }
|
||||||
|
|
||||||
if (isPresent(this.contributorId) &&
|
if (isPresent(this.contributorId) &&
|
||||||
c.contributorId.toString() !== this.contributorId.toString()) { included = false; }
|
c.contributorId !== parseInt(this.contributorId)) { included = false; }
|
||||||
|
|
||||||
if (isPresent(this.contributionKind) &&
|
if (isPresent(this.contributionKind) &&
|
||||||
c.kind !== this.contributionKind) { included = false; }
|
c.kind !== this.contributionKind) { included = false; }
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
{{#if showQuickFilter}}
|
{{#if @showQuickFilter}}
|
||||||
<div class="quick-filter">
|
<div class="quick-filter">
|
||||||
<p>
|
<p>
|
||||||
<label class="filter-contributor">
|
<label class="filter-contributor">
|
||||||
Contributor:
|
Contributor:
|
||||||
<select onchange={{action (mut contributorId) value="target.value"}}>
|
<select onchange={{action (mut this.contributorId) value="target.value"}}>
|
||||||
<option value="" selected>all</option>
|
<option value="" selected>all</option>
|
||||||
{{#each contributorsActive as |contributor|}}
|
{{#each this.contributorsActive as |contributor|}}
|
||||||
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.name}}</option>
|
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="filter-contribution-kind">
|
<label class="filter-contribution-kind">
|
||||||
Kind:
|
Kind:
|
||||||
<select onchange={{action (mut contributionKind) value="target.value"}}>
|
<select onchange={{action (mut this.contributionKind) value="target.value"}}>
|
||||||
<option value="" selected>all</option>
|
<option value="" selected>all</option>
|
||||||
{{#each contributionKinds as |kind|}}
|
{{#each this.contributionKinds as |kind|}}
|
||||||
<option value={{kind}} selected={{eq contributionKind kind}}>{{capitalize-string kind}}</option>
|
<option value={{kind}} selected={{eq this.contributionKind kind}}>{{capitalize-string kind}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="filter-contribution-size">
|
<label class="filter-contribution-size">
|
||||||
{{input type="checkbox" checked=hideSmallContributions}}
|
<Input @type="checkbox" @checked={{this.hideSmallContributions}} />
|
||||||
Hide small contributions
|
Hide small contributions
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<ul class="contribution-list">
|
<ul class="item-list contribution-list {{if @loading 'loading'}}">
|
||||||
{{#each contributionsFiltered as |contribution|}}
|
{{#each this.contributionsFiltered as |contribution|}}
|
||||||
<li role="button" {{action "openContributionDetails" contribution}}
|
<li role="button" data-contribution-id={{contribution.id}}
|
||||||
data-contribution-id={{contribution.id}}
|
{{action "openContributionDetails" contribution}}
|
||||||
class="{{contribution-status contribution}}{{if (eq contribution.id selectedContributionId) " selected"}}">
|
class="{{item-status contribution}}{{if (eq contribution.id @selectedContributionId) " selected"}}">
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<span class="recipient">{{user-avatar contributor=contribution.contributor}}</span>
|
<span class="recipient"><UserAvatar @contributor={{contribution.contributor}} /></span>
|
||||||
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
|
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
|
||||||
<span class="title">{{contribution.description}}</span>
|
<span class="title">{{contribution.description}}</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -45,10 +45,9 @@
|
|||||||
{{#unless contribution.vetoed}}
|
{{#unless contribution.vetoed}}
|
||||||
{{#unless (is-confirmed-contribution contribution)}}
|
{{#unless (is-confirmed-contribution contribution)}}
|
||||||
<p class="voting">
|
<p class="voting">
|
||||||
{{input type="button" class="button small danger"
|
{{input type="button" class="button small danger" value="veto"
|
||||||
click=(action "veto" contribution.id)
|
click=(action "veto" contribution.id)
|
||||||
disabled=contribution.hasPendingChanges
|
disabled=contribution.hasPendingChanges}}
|
||||||
value="veto"}}
|
|
||||||
</p>
|
</p>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ import Component from '@ember/component';
|
|||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
|
||||||
router: service(),
|
router: service(),
|
||||||
|
|
||||||
tagName: 'table',
|
|
||||||
classNames: 'contributor-list',
|
|
||||||
|
|
||||||
selectedContributorId: null,
|
selectedContributorId: null,
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -17,5 +15,4 @@ export default Component.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
<tbody>
|
<table class="contributor-list {{if @loading 'loading'}}">
|
||||||
{{#each contributorList as |c|}}
|
<thead>
|
||||||
<tr role="button" {{action "openContributorDetails" c.contributor}}
|
</thead>
|
||||||
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id selectedContributorId) "selected"}}">
|
<tbody>
|
||||||
<td class="person">
|
{{#each @contributorList as |c|}}
|
||||||
{{user-avatar contributor=c.contributor}} {{c.contributor.name}}
|
<tr role="button"
|
||||||
</td>
|
onclick={{action "openContributorDetails" c.contributor}}
|
||||||
<td class="kredits">
|
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id @selectedContributorId) "selected"}}">
|
||||||
<span class="amount">
|
<td class="person">
|
||||||
{{#if showUnconfirmedKredits}}
|
<UserAvatar @contributor={{c.contributor}} /> {{c.contributor.name}}
|
||||||
{{c.amountTotal}}
|
</td>
|
||||||
{{else}}
|
<td class="kredits">
|
||||||
{{c.amountConfirmed}}
|
<span class="amount">
|
||||||
{{/if}}
|
{{#if @showUnconfirmedKredits}}
|
||||||
</span>
|
{{c.amountTotal}}
|
||||||
<span class="symbol">₭S</span>
|
{{else}}
|
||||||
</td>
|
{{c.amountConfirmed}}
|
||||||
</tr>
|
{{/if}}
|
||||||
{{/each}}
|
</span>
|
||||||
</tbody>
|
<span class="symbol">₭S</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<a href={{account.url}} target="_blank" rel="noopener" title="{{account.username}} on {{account.site}}">
|
<a href={{this.account.url}} target="_blank" rel="noopener noreferrer"
|
||||||
<span class="site">{{account.site}}</span>
|
title="{{this.account.username}} on {{this.account.site}}">
|
||||||
{{component iconComponentName}}
|
<span class="site">{{this.account.site}}</span>
|
||||||
|
{{component this.iconComponentName}}
|
||||||
</a>
|
</a>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
tagName: ""
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import Component from '@ember/component';
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
|
|
||||||
tagName: 'ul',
|
|
||||||
classNames: ['proposal-list'],
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
confirm(proposalId) {
|
|
||||||
if (this.contractInteractionEnabled) {
|
|
||||||
this.confirmProposal(proposalId);
|
|
||||||
} else {
|
|
||||||
window.alert('Only members can vote on proposals. Please ask someone to set you up.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{{#each proposals as |proposal|}}
|
|
||||||
<li data-proposal-id={{proposal.id}} class={{if proposal.isExecuted "confirmed" "unconfirmed"}}>
|
|
||||||
<p class="meta">
|
|
||||||
<span class="category {{proposal.kind}}">♥ ({{proposal.kind}})</span>
|
|
||||||
<span class="recipient">{{proposal.contributor.name}}:</span>
|
|
||||||
</p>
|
|
||||||
<p class="kredits-amount">
|
|
||||||
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
|
|
||||||
</p>
|
|
||||||
<p class="description">
|
|
||||||
<span class="description">{{proposal.description}}</span>
|
|
||||||
</p>
|
|
||||||
<p class="voting">
|
|
||||||
{{#unless proposal.isExecuted}}
|
|
||||||
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
|
|
||||||
<button {{action "confirm" proposal.id}}>+1</button>
|
|
||||||
{{/unless}}
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { sort } from '@ember/object/computed';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default class ReimbursementListComponent extends Component {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
|
||||||
|
@sort('args.items', 'itemSorting') itemsSorted;
|
||||||
|
|
||||||
|
get ipfsGatewayUrl () {
|
||||||
|
return config.ipfs.gatewayUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
veto (id) {
|
||||||
|
this.kredits.vetoReimbursement(id).then(transaction => {
|
||||||
|
console.debug('[controllers:budget] Veto submitted to chain: '+transaction.hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<ul class="item-list spaced reimbursement-list {{if @loading 'loading'}}">
|
||||||
|
{{#each this.itemsSorted as |reimbursement|}}
|
||||||
|
<li data-reimbursement-id={{reimbursement.id}}
|
||||||
|
class="{{item-status reimbursement}}">
|
||||||
|
<p class="meta">
|
||||||
|
<span class="title">
|
||||||
|
Expenses covered by {{reimbursement.contributor.name}}
|
||||||
|
</span>
|
||||||
|
<span class="recipient">
|
||||||
|
<UserAvatar @contributor={{reimbursement.contributor}} />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="token-amount">
|
||||||
|
<span class="amount">
|
||||||
|
{{sats-to-btc reimbursement.amount}}</span> <span class="symbol">BTC</span>
|
||||||
|
</p>
|
||||||
|
<ul class="expense-list">
|
||||||
|
{{#each reimbursement.expenses as |expense|}}
|
||||||
|
<li>
|
||||||
|
<div class="description" rowspan="2">
|
||||||
|
<h4>
|
||||||
|
<span class="date">{{expense.date}}:</span>
|
||||||
|
<span class="title">{{expense.title}}</span>
|
||||||
|
</h4>
|
||||||
|
<p class="description">{{expense.description}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="amount">
|
||||||
|
{{fmt-fiat-currency expense.amount expense.currency}}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{#if this.kredits.currentUserIsCore}}
|
||||||
|
<p>
|
||||||
|
<a href="{{this.ipfsGatewayUrl}}/{{reimbursement.ipfsHash}}"
|
||||||
|
class="button small" target="_blank" rel="noopener noreferrer">
|
||||||
|
Inspect IPFS data
|
||||||
|
</a>
|
||||||
|
<button {{on "click" (fn this.veto reimbursement.id)}}
|
||||||
|
disabled={{reimbursement.vetoed}}
|
||||||
|
class="button small danger" type="button">veto</button>
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
@@ -34,7 +34,6 @@ export default Component.extend({
|
|||||||
this.set('setupInProgress', true);
|
this.set('setupInProgress', true);
|
||||||
await this.kredits.setup();
|
await this.kredits.setup();
|
||||||
this.set('setupInProgress', false);
|
this.set('setupInProgress', false);
|
||||||
this.router.transitionTo('dashboard');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.set('setupInProgress', false);
|
this.set('setupInProgress', false);
|
||||||
console.log('Opening Ethereum wallet failed:', error);
|
console.log('Opening Ethereum wallet failed:', error);
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<section id="user-account">
|
<section id="user-account">
|
||||||
{{#if setupInProgress}}
|
{{#if this.setupInProgress}}
|
||||||
Connecting account...
|
Connecting account...
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if (and kredits.hasAccounts kredits.currentUser)}}
|
{{#if (and this.kredits.hasAccounts this.kredits.currentUser)}}
|
||||||
{{kredits.currentUser.name}}
|
{{this.kredits.currentUser.name}}
|
||||||
{{#if kredits.currentUserIsCore}}
|
{{#if this.kredits.currentUserIsCore}}
|
||||||
<span class="core-flag">(core)</span>
|
<span class="core-flag">(core)</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="anonymous">Anonymous</span>
|
<span class="anonymous">Anonymous</span>
|
||||||
<button {{action "signup"}} class="small">Sign up</button>
|
<button onclick={{action "signup"}} class="small" type="button">Sign up</button>
|
||||||
{{#if showConnectButton}}
|
{{#if this.showConnectButton}}
|
||||||
<button {{action "connectAccount"}} class="small green">Connect account</button>
|
<button onclick={{action "connectAccount"}} class="small green" type="button">Connect account</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export default Component.extend({
|
|||||||
|
|
||||||
if (github_uid) {
|
if (github_uid) {
|
||||||
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=${SIZES[this.size]}`;
|
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=${SIZES[this.size]}`;
|
||||||
|
} else {
|
||||||
|
// TODO use custom avatar
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
|
||||||
|
export default class BudgetController extends Controller {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
@alias('kredits.reimbursementsUnconfirmed') reimbursementsUnconfirmed;
|
||||||
|
@alias('kredits.reimbursementsConfirmed') reimbursementsConfirmed;
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ export default Controller.extend({
|
|||||||
|
|
||||||
kredits: service(),
|
kredits: service(),
|
||||||
|
|
||||||
|
queryParams: ['contributorId', 'kind', 'amount'],
|
||||||
|
|
||||||
contributors: alias('kredits.contributors'),
|
contributors: alias('kredits.contributors'),
|
||||||
minedContributors: filterBy('contributors', 'id'),
|
minedContributors: filterBy('contributors', 'id'),
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,7 @@ export default Controller.extend({
|
|||||||
|
|
||||||
vetoContribution (contributionId) {
|
vetoContribution (contributionId) {
|
||||||
this.kredits.veto(contributionId).then(transaction => {
|
this.kredits.veto(contributionId).then(transaction => {
|
||||||
console.debug('[controllers:index] Veto submitted to Ethereum blockhain: '+transaction.hash);
|
console.debug('[controllers:index] Veto submitted to chain: '+transaction.hash);
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
confirmProposal (proposalId) {
|
|
||||||
this.kredits.vote(proposalId).then(transaction => {
|
|
||||||
console.debug('[controllers:index] Vote submitted to Ethereum blockhain: '+transaction.hash);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import Controller from '@ember/controller';
|
|
||||||
import { alias, filterBy } from '@ember/object/computed';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
contributors: alias('kredits.contributors'),
|
|
||||||
minedContributors: filterBy('contributors', 'id'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
save(proposal) {
|
|
||||||
// contributorIpfsHash is needed for the proposal ipfs data. I'm not happy to do this here but I think to load all the contributors in addProposal again is a bit too much. I hope we can refactor it later.
|
|
||||||
let contributor = this.contributors.findBy('id', proposal.contributorId);
|
|
||||||
proposal.contributorIpfsHash = contributor.get('ipfsHash');
|
|
||||||
|
|
||||||
return this.kredits.addProposal(proposal)
|
|
||||||
.then((proposal) => {
|
|
||||||
this.transitionToRoute('index');
|
|
||||||
return proposal;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import Helper from '@ember/component/helper';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
import { once } from '@ember/runloop';
|
|
||||||
|
|
||||||
export default Helper.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
currentBlock: alias('kredits.currentBlock'),
|
|
||||||
|
|
||||||
compute([contribution]) {
|
|
||||||
this.setupRecompute(contribution);
|
|
||||||
|
|
||||||
let status = [];
|
|
||||||
|
|
||||||
if (contribution.vetoed) {
|
|
||||||
status.push('vetoed');
|
|
||||||
} else if (contribution.confirmedAt > this.currentBlock) {
|
|
||||||
status.push('unconfirmed');
|
|
||||||
} else {
|
|
||||||
status.push('confirmed');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contribution.hasPendingChanges) {
|
|
||||||
status.push('pending');
|
|
||||||
}
|
|
||||||
|
|
||||||
return status.join(' ');
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy () {
|
|
||||||
if (this.teardown) this.teardown();
|
|
||||||
this._super(...arguments);
|
|
||||||
},
|
|
||||||
|
|
||||||
setupRecompute (contribution) {
|
|
||||||
if (this.teardown) this.teardown();
|
|
||||||
|
|
||||||
contribution.addObserver('vetoed' , this, this.triggerRecompute);
|
|
||||||
contribution.addObserver('confirmedAt' , this, this.triggerRecompute);
|
|
||||||
contribution.addObserver('currentBlock' , this, this.triggerRecompute);
|
|
||||||
contribution.addObserver('hasPendingChanges' , this, this.triggerRecompute);
|
|
||||||
|
|
||||||
this.teardown = () => {
|
|
||||||
contribution.removeObserver('vetoed', this, this.triggerRecompute);
|
|
||||||
contribution.removeObserver('confirmedAt', this, this.triggerRecompute);
|
|
||||||
contribution.removeObserver('currentBlock', this, this.triggerRecompute);
|
|
||||||
contribution.removeObserver('hadPendingChanges', this, this.triggerRecompute);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
triggerRecompute () {
|
|
||||||
once(this, function () {
|
|
||||||
this.recompute();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function fmtCryptoCurrency(params/*, hash*/) {
|
||||||
|
let fmtAmount;
|
||||||
|
const amount = params[0];
|
||||||
|
const code = params[1];
|
||||||
|
|
||||||
|
switch(code) {
|
||||||
|
case 'RBTC':
|
||||||
|
fmtAmount = amount / 1000000000000000000;
|
||||||
|
break;
|
||||||
|
case 'BTC':
|
||||||
|
fmtAmount = amount / 100000000;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmtAmount;
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function fmtFiatCurrency(params) {
|
||||||
|
const lang = navigator.language || navigator.userLanguage;
|
||||||
|
const formatter = new Intl.NumberFormat(lang, {
|
||||||
|
style: 'currency',
|
||||||
|
currency: params[1] || 'EUR',
|
||||||
|
currencyDisplay: 'code'
|
||||||
|
})
|
||||||
|
return formatter.format(params[0]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import Helper from '@ember/component/helper';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { alias } from '@ember/object/computed';
|
||||||
|
import { once } from '@ember/runloop';
|
||||||
|
|
||||||
|
export default Helper.extend({
|
||||||
|
|
||||||
|
kredits: service(),
|
||||||
|
currentBlock: alias('kredits.currentBlock'),
|
||||||
|
|
||||||
|
compute([item]) {
|
||||||
|
this.setupRecompute(item);
|
||||||
|
|
||||||
|
let status = [];
|
||||||
|
|
||||||
|
if (item.vetoed) {
|
||||||
|
status.push('vetoed');
|
||||||
|
} else if (item.confirmedAt > this.currentBlock) {
|
||||||
|
status.push('unconfirmed');
|
||||||
|
} else {
|
||||||
|
status.push('confirmed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.hasPendingChanges) {
|
||||||
|
status.push('pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
return status.join(' ');
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy () {
|
||||||
|
if (this.teardown) this.teardown();
|
||||||
|
this._super(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
setupRecompute (item) {
|
||||||
|
if (this.teardown) this.teardown();
|
||||||
|
|
||||||
|
item.addObserver('vetoed' , this, this.triggerRecompute);
|
||||||
|
item.addObserver('confirmedAt' , this, this.triggerRecompute);
|
||||||
|
item.addObserver('currentBlock' , this, this.triggerRecompute);
|
||||||
|
item.addObserver('hasPendingChanges' , this, this.triggerRecompute);
|
||||||
|
|
||||||
|
this.teardown = () => {
|
||||||
|
item.removeObserver('vetoed', this, this.triggerRecompute);
|
||||||
|
item.removeObserver('confirmedAt', this, this.triggerRecompute);
|
||||||
|
item.removeObserver('currentBlock', this, this.triggerRecompute);
|
||||||
|
item.removeObserver('hasPendingChanges', this, this.triggerRecompute);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
triggerRecompute () {
|
||||||
|
once(this, this.recompute);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function satsToBtc(amount/*, hash*/) {
|
||||||
|
return amount / 100000000;
|
||||||
|
});
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
import EmberObject, { computed } from '@ember/object';
|
import EmberObject, { computed } from '@ember/object';
|
||||||
import { isEmpty, isPresent } from '@ember/utils';
|
import { isEmpty, isPresent } from '@ember/utils';
|
||||||
import bignumber from 'kredits-web/utils/cps/bignumber';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
export default EmberObject.extend({
|
export default EmberObject.extend({
|
||||||
|
|
||||||
// Contract
|
// Contract
|
||||||
id: null,
|
id: null,
|
||||||
contributorId: null,
|
contributorId: null,
|
||||||
amount: null,
|
amount: null,
|
||||||
confirmedAt: bignumber('confirmedAtBlock', 'toNumber'),
|
confirmedAt: null,
|
||||||
vetoed: null,
|
vetoed: null,
|
||||||
ipfsHash: null,
|
ipfsHash: null,
|
||||||
|
|
||||||
creatorAccount: null,
|
// contributor model instance
|
||||||
|
contributor: null,
|
||||||
|
|
||||||
|
// TODO contributor who submitted the contribution
|
||||||
|
// submittedBy: null,
|
||||||
|
|
||||||
// IPFS
|
// IPFS
|
||||||
kind: null,
|
kind: null,
|
||||||
@@ -22,7 +24,6 @@ export default EmberObject.extend({
|
|||||||
url: null,
|
url: null,
|
||||||
date: null,
|
date: null,
|
||||||
time: null,
|
time: null,
|
||||||
ipfsData: '',
|
|
||||||
|
|
||||||
pendingTx: null,
|
pendingTx: null,
|
||||||
|
|
||||||
@@ -41,6 +42,9 @@ export default EmberObject.extend({
|
|||||||
|
|
||||||
hasPendingChanges: computed('pendingTx', function() {
|
hasPendingChanges: computed('pendingTx', function() {
|
||||||
return isPresent(this.pendingTx);
|
return isPresent(this.pendingTx);
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
serialize () {
|
||||||
|
return JSON.stringify(this);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import EmberObject from '@ember/object';
|
import EmberObject from '@ember/object';
|
||||||
import bignumber from 'kredits-web/utils/cps/bignumber';
|
|
||||||
import kreditsValue from 'kredits-web/utils/cps/kredits';
|
|
||||||
|
|
||||||
export default EmberObject.extend({
|
export default EmberObject.extend({
|
||||||
// Contract
|
// Contract
|
||||||
id: bignumber('idRaw', 'toString'),
|
id: null,
|
||||||
account: null,
|
account: null,
|
||||||
balance: kreditsValue('balanceRaw'),
|
balance: 0,
|
||||||
totalKreditsEarned: bignumber('totalKreditsEarnedRaw', 'toNumber'),
|
totalKreditsEarned: 0,
|
||||||
contributionsCount: bignumber('contributionsCountRaw', 'toNumber'),
|
contributionsCount: 0,
|
||||||
isCore: false,
|
isCore: false,
|
||||||
ipfsHash: null,
|
ipfsHash: null,
|
||||||
|
|
||||||
@@ -20,6 +18,8 @@ export default EmberObject.extend({
|
|||||||
github_uid: null,
|
github_uid: null,
|
||||||
wiki_username: null,
|
wiki_username: null,
|
||||||
zoom_display_name: null,
|
zoom_display_name: null,
|
||||||
ipfsData: ''
|
|
||||||
|
|
||||||
|
serialize () {
|
||||||
|
return JSON.stringify(this);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import EmberObject from '@ember/object';
|
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
import bignumber from 'kredits-web/utils/cps/bignumber';
|
|
||||||
|
|
||||||
export default EmberObject.extend({
|
|
||||||
|
|
||||||
// Contract
|
|
||||||
id: bignumber('idRaw', 'toString'),
|
|
||||||
creatorAccount: null,
|
|
||||||
contributorId: bignumber('contributorIdRaw', 'toString'),
|
|
||||||
amount: bignumber('amountRaw', 'toNumber'),
|
|
||||||
votesCount: bignumber('votesCountRaw', 'toNumber'),
|
|
||||||
votesNeeded: bignumber('votesNeededRaw', 'toNumber'),
|
|
||||||
executed: null,
|
|
||||||
ipfsHash: null,
|
|
||||||
|
|
||||||
// Shortcuts
|
|
||||||
isExecuted: alias('executed'),
|
|
||||||
|
|
||||||
// IPFS
|
|
||||||
kind: null,
|
|
||||||
description: null,
|
|
||||||
details: null,
|
|
||||||
url: null,
|
|
||||||
ipfsData: '',
|
|
||||||
|
|
||||||
init () {
|
|
||||||
this._super(...arguments);
|
|
||||||
this.set('details', {});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import EmberObject, { computed } from '@ember/object';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
|
export default EmberObject.extend({
|
||||||
|
|
||||||
|
// Contract
|
||||||
|
id: null,
|
||||||
|
recipientId: null,
|
||||||
|
token: null,
|
||||||
|
amount: null,
|
||||||
|
confirmedAt: null,
|
||||||
|
vetoed: null,
|
||||||
|
ipfsHash: null,
|
||||||
|
|
||||||
|
// contributor model instance
|
||||||
|
recipient: null,
|
||||||
|
|
||||||
|
// TODO contributor who submitted the reimbursement
|
||||||
|
// recordedBy: null,
|
||||||
|
|
||||||
|
// IPFS
|
||||||
|
expenses: null, // Array of expense objects
|
||||||
|
|
||||||
|
pendingTx: null,
|
||||||
|
|
||||||
|
iso8601Date: computed('date', 'time', function() {
|
||||||
|
return this.time ? `${this.date}T${this.time}` : this.date;
|
||||||
|
}),
|
||||||
|
|
||||||
|
jsDate: computed('iso8601Date', function() {
|
||||||
|
return moment(this.iso8601Date).toDate();
|
||||||
|
}),
|
||||||
|
|
||||||
|
hasPendingChanges: computed('pendingTx', function() {
|
||||||
|
return isPresent(this.pendingTx);
|
||||||
|
}),
|
||||||
|
|
||||||
|
pendingStatus: computed('pendingTx', function() {
|
||||||
|
return isPresent(this.pendingTx) ? 'isPending' : 'notPending';
|
||||||
|
}),
|
||||||
|
|
||||||
|
serialize () {
|
||||||
|
return JSON.stringify(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Resolver from 'ember-resolver';
|
|
||||||
|
|
||||||
export default Resolver;
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import EmberRouter from '@ember/routing/router';
|
import EmberRouter from '@ember/routing/router';
|
||||||
import config from './config/environment';
|
import config from './config/environment';
|
||||||
|
|
||||||
const Router = EmberRouter.extend({
|
export default class Router extends EmberRouter {
|
||||||
location: config.locationType,
|
location = config.locationType;
|
||||||
rootURL: config.rootURL
|
rootURL = config.rootURL;
|
||||||
});
|
}
|
||||||
|
|
||||||
Router.map(function() {
|
Router.map(function() {
|
||||||
this.route('dashboard', function() {
|
this.route('dashboard', function() {
|
||||||
@@ -16,11 +16,8 @@ Router.map(function() {
|
|||||||
this.route('show', { path: ':id' });
|
this.route('show', { path: ':id' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.route('proposals', function() {
|
|
||||||
this.route('new');
|
|
||||||
});
|
|
||||||
this.route('contributions', function() {
|
this.route('contributions', function() {
|
||||||
this.route('new');
|
this.route('new', { queryParams: ['contributorId', 'kind', 'amount'] });
|
||||||
this.route('resubmit', { path: ':id/resubmit' });
|
this.route('resubmit', { path: ':id/resubmit' });
|
||||||
});
|
});
|
||||||
this.route('contributors', function() {
|
this.route('contributors', function() {
|
||||||
@@ -32,6 +29,13 @@ Router.map(function() {
|
|||||||
this.route('eth-account');
|
this.route('eth-account');
|
||||||
this.route('complete');
|
this.route('complete');
|
||||||
});
|
});
|
||||||
});
|
this.route('budget', function() {
|
||||||
|
this.route('expenses');
|
||||||
|
|
||||||
export default Router;
|
this.route('reimbursements', function() {});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route('reimbursements', function() {
|
||||||
|
this.route('new');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
import { inject as service } from '@ember/service';
|
|
||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { schedule } from '@ember/runloop';
|
||||||
|
|
||||||
export default Route.extend({
|
export default class ApplicationRoute extends Route {
|
||||||
kredits: service(),
|
@service kredits;
|
||||||
|
@service communityFunds;
|
||||||
|
|
||||||
beforeModel(/* transition */) {
|
beforeModel(/* transition */) {
|
||||||
const kredits = this.kredits;
|
return this.kredits.setup().then(() => {
|
||||||
|
this.kredits.kredits.preflightChecks().catch(error => {
|
||||||
return kredits.setup().then(() => {
|
|
||||||
kredits.get('kredits').preflightChecks().catch((error) => {
|
|
||||||
console.error('Kredits preflight check failed!');
|
console.error('Kredits preflight check failed!');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
}).catch((error) => {
|
}).catch(error => {
|
||||||
console.log('Error initializing Kredits', error);
|
console.log('Error initializing Kredits', error);
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return this.kredits.loadInitialData().then(() => {
|
||||||
|
this.kredits.addContractEventHandlers()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
afterModel() {
|
afterModel() {
|
||||||
return this.kredits.loadInitialData()
|
if (this.kredits.contributorsNeedSync) {
|
||||||
.then(() => {
|
schedule('afterRender', this.kredits.syncContributors,
|
||||||
this.kredits.addContractEventHandlers();
|
this.kredits.syncContributors.perform);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
schedule('afterRender', this.communityFunds.fetchBalances,
|
||||||
|
this.communityFunds.fetchBalances.perform);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { isEmpty, isPresent } from '@ember/utils';
|
||||||
|
import { schedule } from '@ember/runloop';
|
||||||
|
|
||||||
|
export default class BudgetRoute extends Route {
|
||||||
|
@service browserCache;
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
async model () {
|
||||||
|
if (isPresent(this.kredits.reimbursements) &&
|
||||||
|
isEmpty(this.kredits.reimbursementsPending)) {
|
||||||
|
console.debug('[route:budget] Reimbursements loaded before, no need to sync or load');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const numCachedReimbursements = await this.browserCache.reimbursements.length();
|
||||||
|
if (numCachedReimbursements > 0) {
|
||||||
|
await this.kredits.loadObjectsFromCache('Reimbursement');
|
||||||
|
this.kredits.set('reimbursementsNeedSync', true);
|
||||||
|
} else {
|
||||||
|
await this.kredits.fetchObjects('Reimbursement', { page: { size: 10 } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
afterModel() {
|
||||||
|
if (this.kredits.reimbursementsNeedSync) {
|
||||||
|
schedule('afterRender', this.kredits.syncReimbursements,
|
||||||
|
this.kredits.syncReimbursements.perform);
|
||||||
|
}
|
||||||
|
schedule('afterRender', this.kredits.fetchMissingReimbursements,
|
||||||
|
this.kredits.fetchMissingReimbursements.perform);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class BudgetExpensesRoute extends Route {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class ContributionsNewRoute extends Route {
|
||||||
|
|
||||||
|
model (params) {
|
||||||
|
return { params };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,19 +6,15 @@ export default Route.extend({
|
|||||||
kredits: service(),
|
kredits: service(),
|
||||||
|
|
||||||
model(params) {
|
model(params) {
|
||||||
const contribution = this.kredits.contributions.findBy('id', parseInt(params.id));
|
return this.kredits.contributions.findBy('id', parseInt(params.id));
|
||||||
contribution.contributorId = contribution.contributorId.toString();
|
|
||||||
|
|
||||||
return contribution;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController (controller, model) {
|
setupController (controller, model) {
|
||||||
this._super(controller, model);
|
this._super(controller, model);
|
||||||
|
|
||||||
controller.set('attributes', model.getProperties([
|
controller.set('attributes', model.getProperties([
|
||||||
'kind', 'amount', 'description', 'url', 'details'
|
'contributorId', 'kind', 'amount', 'description', 'url', 'details'
|
||||||
]));
|
]));
|
||||||
controller.set('attributes.contributorId', model.contributorId.toString());
|
|
||||||
controller.set('attributes.date', model.jsDate);
|
controller.set('attributes.date', model.jsDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { schedule } from '@ember/runloop';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
export default class DashboardRoute extends Route {
|
||||||
|
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
afterModel() {
|
||||||
|
if (this.kredits.contributionsNeedSync) {
|
||||||
|
schedule('afterRender', this.kredits.syncContributions,
|
||||||
|
this.kredits.syncContributions.perform);
|
||||||
|
}
|
||||||
|
schedule('afterRender', this.kredits.fetchMissingContributions,
|
||||||
|
this.kredits.fetchMissingContributions.perform);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class ReimbursementsNewRoute extends Route {
|
||||||
|
|
||||||
|
model (params) {
|
||||||
|
return { params };
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Service from '@ember/service';
|
||||||
|
import * as localforage from 'localforage';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
function createStore(name) {
|
||||||
|
let networkName;
|
||||||
|
if (config.web3RequiredNetworkName) {
|
||||||
|
networkName = config.web3RequiredNetworkName.toLocaleLowerCase().replace(' ', '-');
|
||||||
|
} else {
|
||||||
|
networkName = 'custom';
|
||||||
|
}
|
||||||
|
return localforage.createInstance({ name: `kredits:${networkName}:${name}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class BrowserCacheService extends Service {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.stores = {
|
||||||
|
contributors: createStore('contributors'),
|
||||||
|
contributions: createStore('contributions'),
|
||||||
|
reimbursements: createStore('reimbursements')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get contributors() {
|
||||||
|
return this.stores.contributors;
|
||||||
|
}
|
||||||
|
|
||||||
|
get contributions() {
|
||||||
|
return this.stores.contributions;
|
||||||
|
}
|
||||||
|
|
||||||
|
get reimbursements() {
|
||||||
|
return this.stores.reimbursements;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import Service, { inject as service } from '@ember/service';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { A } from '@ember/array';
|
||||||
|
import { task } from 'ember-concurrency-decorators';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default class CommunityFundsService extends Service {
|
||||||
|
@service exchangeRates;
|
||||||
|
|
||||||
|
@tracked balancesLoaded = false;
|
||||||
|
@tracked balances = A([]);
|
||||||
|
|
||||||
|
@task
|
||||||
|
*fetchBalances () {
|
||||||
|
yield fetch(config.btcBalanceAPI).then(res => res.json())
|
||||||
|
.then(res => {
|
||||||
|
return this.processBalances(res);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.log(`[community-funds] Fetching balances failed:`);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async processBalances (res) {
|
||||||
|
await this.exchangeRates.fetchRates();
|
||||||
|
// Format and round the approximate USD value
|
||||||
|
const lang = navigator.language || navigator.userLanguage;
|
||||||
|
const balanceUSD = res.confirmed_balance * this.exchangeRates.btcusd;
|
||||||
|
res.balanceUSD = Math.round(balanceUSD).toLocaleString(lang);
|
||||||
|
|
||||||
|
this.balances.pushObject({
|
||||||
|
...res,
|
||||||
|
...{ token: { name: 'BTC', symbol: 'BTC'} }
|
||||||
|
});
|
||||||
|
|
||||||
|
this.balancesLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import Service from '@ember/service';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
// Need to go through proxy for CORS headers
|
||||||
|
const bitstampBaseUrl = `${config.corsProxy}https://www.bitstamp.net/api/v2`;
|
||||||
|
|
||||||
|
async function fetchFromBitstamp(currencyPair) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${bitstampBaseUrl}/ticker/${currencyPair}/`).then(r => r.json());
|
||||||
|
return parseFloat(res.vwap); // Last 24 hours volume weighted average price
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Could not fetch exchange rate from Bitstamp:', e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ExchangeRatesService extends Service {
|
||||||
|
@tracked btceur = 0;
|
||||||
|
@tracked btcusd = 0;
|
||||||
|
|
||||||
|
async fetchRates (source='bitstamp') {
|
||||||
|
switch(source) {
|
||||||
|
case 'bitstamp':
|
||||||
|
this.btceur = await fetchFromBitstamp('btceur');
|
||||||
|
this.btcusd = await fetchFromBitstamp('btcusd');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,84 +1,58 @@
|
|||||||
import ethers from 'ethers';
|
import ethers from 'ethers';
|
||||||
import Kredits from 'kredits-contracts';
|
import Kredits from '@kredits/contracts';
|
||||||
import RSVP from 'rsvp';
|
|
||||||
|
|
||||||
import Service from '@ember/service';
|
import Service from '@ember/service';
|
||||||
import EmberObject from '@ember/object';
|
import EmberObject from '@ember/object';
|
||||||
import { computed } from '@ember/object';
|
import { computed } from '@ember/object';
|
||||||
import { alias, notEmpty } from '@ember/object/computed';
|
import { alias, filterBy, notEmpty, sort } from '@ember/object/computed';
|
||||||
import { isEmpty, isPresent } from '@ember/utils';
|
import { isEmpty, isPresent } from '@ember/utils';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
|
import { task, taskGroup } from 'ember-concurrency';
|
||||||
|
|
||||||
import groupBy from 'kredits-web/utils/group-by';
|
import groupBy from 'kredits-web/utils/group-by';
|
||||||
|
import processContributorData from 'kredits-web/utils/process-contributor-data';
|
||||||
|
import processContributionData from 'kredits-web/utils/process-contribution-data';
|
||||||
|
import processReimbursementData from 'kredits-web/utils/process-reimbursement-data';
|
||||||
import formatKredits from 'kredits-web/utils/format-kredits';
|
import formatKredits from 'kredits-web/utils/format-kredits';
|
||||||
|
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
import Contributor from 'kredits-web/models/contributor'
|
import Contributor from 'kredits-web/models/contributor';
|
||||||
import Proposal from 'kredits-web/models/proposal'
|
import Contribution from 'kredits-web/models/contribution';
|
||||||
import Contribution from 'kredits-web/models/contribution'
|
import Reimbursement from 'kredits-web/models/reimbursement';
|
||||||
|
// Lets us access the model classes dynamically
|
||||||
|
const models = { Contributor, Contribution, Reimbursement };
|
||||||
|
|
||||||
export default Service.extend({
|
export default Service.extend({
|
||||||
|
|
||||||
|
browserCache: service(),
|
||||||
|
|
||||||
currentBlock: null,
|
currentBlock: null,
|
||||||
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
|
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
contributors: null,
|
contributors: null,
|
||||||
contributions: null,
|
contributions: null,
|
||||||
proposals: null,
|
reimbursements: null,
|
||||||
githubAccessToken: null,
|
githubAccessToken: null,
|
||||||
|
|
||||||
currentUserIsContributor: notEmpty('currentUser'),
|
currentUserIsContributor: notEmpty('currentUser'),
|
||||||
currentUserIsCore: alias('currentUser.isCore'),
|
currentUserIsCore: alias('currentUser.isCore'),
|
||||||
hasAccounts: notEmpty('currentUserAccounts'),
|
hasAccounts: notEmpty('currentUserAccounts'),
|
||||||
|
|
||||||
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
|
contributorsMined: filterBy('contributors', 'id'),
|
||||||
return this.contributions.filter(contribution => {
|
contributorsSorting: Object.freeze(['name:asc']),
|
||||||
return contribution.confirmedAt > this.currentBlock;
|
contributorsSorted: sort('contributorsMined', 'contributorsSorting'),
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() {
|
// When data was loaded from cache, we need to fetch updates from the network
|
||||||
return this.contributions
|
contributorsNeedSync: false,
|
||||||
.filterBy('vetoed', false)
|
contributionsNeedSync: false,
|
||||||
.filter(contribution => {
|
reimbursementsNeedSync: false,
|
||||||
return contribution.confirmedAt <= this.currentBlock;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
kreditsByContributor: computed('contributionsUnconfirmed.@each.vetoed', 'contributors.[]', function() {
|
|
||||||
const contributionsUnconfirmed = this.contributionsUnconfirmed.filterBy('vetoed', false);
|
|
||||||
const contributionsGrouped = groupBy(contributionsUnconfirmed, 'contributorId');
|
|
||||||
const contributorsWithUnconfirmed = contributionsGrouped.map(c => c.value.toString());
|
|
||||||
const contributorsWithOnlyConfirmed = this.contributors.reject(c => contributorsWithUnconfirmed.includes(c.id))
|
|
||||||
|
|
||||||
const kreditsByContributor = contributionsGrouped.map(c => {
|
|
||||||
const amountUnconfirmed = c.items.mapBy('amount').reduce((a, b) => a + b);
|
|
||||||
const contributor = this.contributors.findBy('id', c.value.toString());
|
|
||||||
|
|
||||||
return EmberObject.create({
|
|
||||||
contributor: contributor,
|
|
||||||
amountUnconfirmed: amountUnconfirmed,
|
|
||||||
amountConfirmed: contributor.totalKreditsEarned,
|
|
||||||
amountTotal: contributor.totalKreditsEarned + amountUnconfirmed
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
contributorsWithOnlyConfirmed.forEach(c => {
|
|
||||||
kreditsByContributor.push(EmberObject.create({
|
|
||||||
contributor: c,
|
|
||||||
amountUnconfirmed: 0,
|
|
||||||
amountConfirmed: c.totalKreditsEarned,
|
|
||||||
amountTotal: c.totalKreditsEarned
|
|
||||||
}));
|
|
||||||
})
|
|
||||||
|
|
||||||
return kreditsByContributor;
|
|
||||||
}),
|
|
||||||
|
|
||||||
init () {
|
init () {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.set('contributors', []);
|
this.set('contributors', []);
|
||||||
this.set('proposals', []);
|
|
||||||
this.set('contributions', []);
|
this.set('contributions', []);
|
||||||
|
this.set('reimbursements', []);
|
||||||
},
|
},
|
||||||
|
|
||||||
// This is called in the application route's beforeModel(). So it is
|
// This is called in the application route's beforeModel(). So it is
|
||||||
@@ -87,7 +61,7 @@ export default Service.extend({
|
|||||||
getEthProvider () {
|
getEthProvider () {
|
||||||
let ethProvider;
|
let ethProvider;
|
||||||
|
|
||||||
return new RSVP.Promise(async (resolve) => {
|
return new Promise(resolve => {
|
||||||
function instantiateWithoutAccount () {
|
function instantiateWithoutAccount () {
|
||||||
console.debug('[kredits] Creating new instance from npm module class');
|
console.debug('[kredits] Creating new instance from npm module class');
|
||||||
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
|
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
|
||||||
@@ -99,13 +73,12 @@ export default Service.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function instantiateWithAccount (web3Provider, context) {
|
async function instantiateWithAccount (web3Provider, context) {
|
||||||
console.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask');
|
console.debug('[kredits] Using user-provided Web3 instance, e.g. from Metamask');
|
||||||
ethProvider = new ethers.providers.Web3Provider(web3Provider);
|
ethProvider = new ethers.providers.Web3Provider(web3Provider);
|
||||||
|
|
||||||
const network = await ethProvider.getNetwork();
|
const network = await ethProvider.getNetwork();
|
||||||
if (isPresent(config.web3RequiredNetwork) &&
|
if (isPresent(config.web3RequiredChainId) &&
|
||||||
network.name !== config.web3RequiredNetwork) {
|
network.chainId !== config.web3RequiredChainId) {
|
||||||
window.alert(`Please switch your Ethereum wallet to the "${config.web3RequiredNetwork}" network before connecting your account.`);
|
|
||||||
return instantiateWithoutAccount();
|
return instantiateWithoutAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,30 +110,43 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setup () {
|
async setup () {
|
||||||
return this.getEthProvider().then((providerAndSigner) => {
|
const kredits = await this.getEthProvider().then(providerAndSigner => {
|
||||||
let kredits = new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
|
return new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
|
||||||
addresses: { Kernel: config.kreditsKernelAddress },
|
|
||||||
apm: config.kreditsApmDomain,
|
|
||||||
ipfsConfig: config.ipfs
|
ipfsConfig: config.ipfs
|
||||||
});
|
});
|
||||||
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;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await kredits.init();
|
||||||
|
|
||||||
|
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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getCurrentUser: computed('kredits.provider', 'currentUserAccounts.[]', function() {
|
||||||
|
if (isEmpty(this.currentUserAccounts)) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return this.kredits.Contributor
|
||||||
|
.functions.getContributorIdByAddress(this.currentUserAccounts.firstObject)
|
||||||
|
.then((id) => {
|
||||||
|
// check if the user is a contributor or not
|
||||||
|
if (id === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
|
return this.kredits.Contributor.getById(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
totalSupply: computed(function() {
|
totalSupply: computed(function() {
|
||||||
return this.kredits.Token.functions.totalSupply().then(total => {
|
return this.kredits.Token.functions.totalSupply().then(total => {
|
||||||
return formatKredits(total);
|
return formatKredits(total);
|
||||||
@@ -168,16 +154,83 @@ export default Service.extend({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
totalKreditsEarned: computed(function() {
|
totalKreditsEarned: computed(function() {
|
||||||
return this.kredits.Contribution.functions.totalKreditsEarned(true)
|
return this.kredits.Contribution.functions.totalKreditsEarned(true);
|
||||||
.then(total => total.toNumber());
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
kreditsByContributor: computed('contributionsUnconfirmed.@each.vetoed', 'contributors.[]', function() {
|
||||||
|
const contributionsUnconfirmed = this.contributionsUnconfirmed.filterBy('vetoed', false);
|
||||||
|
const contributionsGrouped = groupBy(contributionsUnconfirmed, 'contributorId');
|
||||||
|
const contributorsWithUnconfirmed = contributionsGrouped.map(c => c.value);
|
||||||
|
const contributorsWithOnlyConfirmed = this.contributors.reject(c => contributorsWithUnconfirmed.includes(c.id))
|
||||||
|
|
||||||
loadInitialData () {
|
const kreditsByContributor = contributionsGrouped.map(c => {
|
||||||
return this.getContributors()
|
const amountUnconfirmed = c.items.mapBy('amount').reduce((a, b) => a + b);
|
||||||
.then(contributors => this.contributors.pushObjects(contributors))
|
const contributor = this.contributors.findBy('id', c.value);
|
||||||
.then(() => this.getContributions())
|
|
||||||
.then(contributions => this.contributions.pushObjects(contributions))
|
return EmberObject.create({
|
||||||
|
contributor: contributor,
|
||||||
|
amountUnconfirmed: amountUnconfirmed,
|
||||||
|
amountConfirmed: contributor.totalKreditsEarned,
|
||||||
|
amountTotal: contributor.totalKreditsEarned + amountUnconfirmed
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
contributorsWithOnlyConfirmed.forEach(c => {
|
||||||
|
kreditsByContributor.push(EmberObject.create({
|
||||||
|
contributor: c,
|
||||||
|
amountUnconfirmed: 0,
|
||||||
|
amountConfirmed: c.totalKreditsEarned,
|
||||||
|
amountTotal: c.totalKreditsEarned
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
|
||||||
|
return kreditsByContributor;
|
||||||
|
}),
|
||||||
|
|
||||||
|
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
|
||||||
|
return this.contributions
|
||||||
|
.filter(c => c.confirmedAt > this.currentBlock);
|
||||||
|
}),
|
||||||
|
|
||||||
|
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() {
|
||||||
|
return this.contributions
|
||||||
|
.filterBy('vetoed', false)
|
||||||
|
.filter(c => c.confirmedAt <= this.currentBlock);
|
||||||
|
}),
|
||||||
|
|
||||||
|
reimbursementsUnconfirmed: computed('reimbursements.[]', 'currentBlock', function() {
|
||||||
|
return this.reimbursements
|
||||||
|
.filter(r => r.confirmedAt > this.currentBlock);
|
||||||
|
}),
|
||||||
|
|
||||||
|
reimbursementsConfirmed: computed('reimbursements.[]', 'currentBlock', function() {
|
||||||
|
return this.reimbursements
|
||||||
|
.filterBy('vetoed', false)
|
||||||
|
.filter(r => r.confirmedAt <= this.currentBlock);
|
||||||
|
}),
|
||||||
|
|
||||||
|
reimbursementsPending: computed('reimbursements.[]', 'pendingTx', function() {
|
||||||
|
return this.reimbursements.filter(r => !r.id);
|
||||||
|
}),
|
||||||
|
|
||||||
|
async loadInitialData () {
|
||||||
|
const numCachedContributors = await this.browserCache.contributors.length();
|
||||||
|
if (numCachedContributors > 0) {
|
||||||
|
await this.loadObjectsFromCache('Contributor');
|
||||||
|
this.set('contributorsNeedSync', true);
|
||||||
|
} else {
|
||||||
|
await this.fetchContributors();
|
||||||
|
}
|
||||||
|
|
||||||
|
const numCachedContributions = await this.browserCache.contributions.length();
|
||||||
|
if (numCachedContributions > 0) {
|
||||||
|
await this.loadObjectsFromCache('Contribution');
|
||||||
|
this.set('contributionsNeedSync', true);
|
||||||
|
} else {
|
||||||
|
await this.fetchContributions({ page: { size: 30 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
|
|
||||||
addContributor (attributes) {
|
addContributor (attributes) {
|
||||||
@@ -208,75 +261,175 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getContributors () {
|
fetchContributors () {
|
||||||
|
console.debug(`[kredits] Fetching all contributors from the network`);
|
||||||
return this.kredits.Contributor.all()
|
return this.kredits.Contributor.all()
|
||||||
.then(contributors => {
|
.then(contributors => {
|
||||||
return contributors.map(contributor => {
|
return contributors.forEach(data => {
|
||||||
return Contributor.create(contributor);
|
this.loadContributorFromData(data);
|
||||||
|
return;
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return this.cacheLoadedContributors();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addContribution (attributes) {
|
loadContributorFromData (data) {
|
||||||
console.debug('[kredits] add contribution', attributes);
|
const contributor = Contributor.create(processContributorData(data));
|
||||||
|
const loadedContributor = this.contributors.findBy('id', contributor.id);
|
||||||
|
if (loadedContributor) { this.contributors.removeObject(loadedContributor); }
|
||||||
|
this.contributors.pushObject(contributor);
|
||||||
|
return contributor;
|
||||||
|
},
|
||||||
|
|
||||||
return this.kredits.Contribution.addContribution(attributes, { gasLimit: 300000 })
|
async cacheLoadedContributors () {
|
||||||
|
for (const c of this.contributors) {
|
||||||
|
await this.browserCache.contributors.setItem(c.id.toString(), c.serialize());
|
||||||
|
}
|
||||||
|
console.debug(`[kredits] Cached ${this.contributors.length} contributors in browser storage`);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadContributorsFromCache () {
|
||||||
|
return this.browserCache.contributors.iterate((value/*, key , iterationNumber */) => {
|
||||||
|
this.contributors.pushObject(Contributor.create(JSON.parse(value)));
|
||||||
|
}).then((/* result */) => {
|
||||||
|
console.debug(`[kredits] Loaded ${this.contributors.length} contributors from cache`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
syncContributors: task(function * () {
|
||||||
|
yield this.fetchContributors();
|
||||||
|
this.set('contributorsNeedSync', false);
|
||||||
|
}),
|
||||||
|
|
||||||
|
addContribution (attributes) {
|
||||||
|
console.debug('[kredits] Adding contribution', attributes);
|
||||||
|
|
||||||
|
return this.kredits.Contribution.add(attributes, { gasLimit: 300000 })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
console.debug('[kredits] add contribution response', data);
|
console.debug('[kredits] Contribution.add response', data);
|
||||||
attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
|
attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
|
||||||
const contribution = Contribution.create(attributes);
|
const contribution = Contribution.create(attributes);
|
||||||
contribution.set('pendingTx', data);
|
contribution.set('pendingTx', data);
|
||||||
contribution.set('confirmedAtBlock', data.blockNumber + 40320);
|
contribution.set('confirmedAtBlock', this.currentBlock + 40320);
|
||||||
this.contributions.pushObject(contribution);
|
this.contributions.pushObject(contribution);
|
||||||
return contribution;
|
return contribution;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
//
|
fetchContributions (options = { page: { size: 200 } }) {
|
||||||
// TODO Implement proposals with voting
|
console.debug(`[kredits] Fetching contributions from the network`);
|
||||||
//
|
return this.kredits.Contribution.all(options)
|
||||||
|
|
||||||
// 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 => {
|
.then(contributions => {
|
||||||
return contributions.map(contribution => {
|
return contributions.map(data => {
|
||||||
contribution.contributor = this.contributors.findBy('id', contribution.contributorId.toString());
|
const contribution = this.loadContributionFromData(data);
|
||||||
return Contribution.create(contribution);
|
return contribution;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(contributions => {
|
||||||
|
const cacheWrites = contributions.map(c => {
|
||||||
|
return this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
||||||
|
});
|
||||||
|
return Promise.all(cacheWrites).then(() => {
|
||||||
|
console.debug(`[kredits] Cached ${contributions.length} contributions in browser storage`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
vote (proposalId) {
|
loadContributionFromData(data) {
|
||||||
console.debug('[kredits] vote for', proposalId);
|
const contribution = Contribution.create(processContributionData(data));
|
||||||
|
contribution.set('contributor', this.contributors.findBy('id', data.contributorId));
|
||||||
return this.kredits.Proposal.functions.vote(proposalId)
|
const loadedContribution = this.contributions.findBy('id', contribution.id);
|
||||||
.then(data => {
|
if (loadedContribution) { this.contributions.removeObject(loadedContribution); }
|
||||||
console.debug('[kredits] vote response', data);
|
this.contributions.pushObject(contribution);
|
||||||
return data;
|
return contribution;
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async cacheLoadedContributions () {
|
||||||
|
for (const c of this.contributions) {
|
||||||
|
await this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
||||||
|
}
|
||||||
|
console.debug(`[kredits] Cached ${this.contributions.length} contributions in browser storage`);
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadContributionsFromCache () {
|
||||||
|
return this.browserCache.contributions.iterate((value/*, key , iterationNumber */) => {
|
||||||
|
this.contributions.pushObject(Contribution.create(JSON.parse(value)));
|
||||||
|
}).then((/* result */) => {
|
||||||
|
console.debug(`[kredits] Loaded ${this.contributions.length} contributions from cache`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
contributionTasks: taskGroup().enqueue(),
|
||||||
|
|
||||||
|
syncContributions: task(function * () {
|
||||||
|
yield this.fetchNewContributions.perform();
|
||||||
|
yield this.syncUnconfirmedContributions.perform();
|
||||||
|
this.set('contributionsNeedSync', false);
|
||||||
|
}).group('contributionTasks'),
|
||||||
|
|
||||||
|
fetchNewContributions: task(function * () {
|
||||||
|
const count = yield this.kredits.Contribution.functions.contributionsCount();
|
||||||
|
const lastKnownContributionId = Math.max.apply(null, this.contributions.mapBy('id'));
|
||||||
|
const toFetch = count - lastKnownContributionId;
|
||||||
|
|
||||||
|
if (toFetch > 0) {
|
||||||
|
console.debug(`[kredits] Fetching ${toFetch} new contributions`);
|
||||||
|
for (let id = lastKnownContributionId; id <= count; id++) {
|
||||||
|
const data = yield this.kredits.Contribution.getById(id);
|
||||||
|
const c = this.loadContributionFromData(data);
|
||||||
|
yield this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.debug(`[kredits] No new contributions to fetch`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
fetchMissingContributions: task(function * () {
|
||||||
|
const count = yield this.kredits.Contribution.functions.contributionsCount();
|
||||||
|
const allIds = [...Array(count+1).keys()];
|
||||||
|
allIds.shift(); // remove first item, which is 0
|
||||||
|
const loadedContributions = new Set(this.contributions.mapBy('id'));
|
||||||
|
const toFetch = allIds.filter(id => !loadedContributions.has(id));
|
||||||
|
if (toFetch.length === 0) {
|
||||||
|
console.debug(`[kredits] No contributions left to fetch`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.debug(`[kredits] Fetching ${toFetch.length} past contributions`);
|
||||||
|
let countFetched = 0;
|
||||||
|
|
||||||
|
for (let id = count; id > 0; id--) {
|
||||||
|
if (loadedContributions.has(id)) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
const data = yield this.kredits.Contribution.getById(id);
|
||||||
|
const c = this.loadContributionFromData(data);
|
||||||
|
yield this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
||||||
|
countFetched++;
|
||||||
|
if (countFetched % 20 === 0) {
|
||||||
|
console.debug(`[kredits] Fetched ${countFetched} more contributions`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.debug(`[kredits] Cached ${countFetched} past contributions`);
|
||||||
|
}).group('contributionTasks'),
|
||||||
|
|
||||||
|
syncUnconfirmedContributions: task(function * () {
|
||||||
|
if (this.contributionsUnconfirmed.length > 0) {
|
||||||
|
console.debug(`[kredits] Syncing unconfirmed contributions`);
|
||||||
|
for (const c of this.contributionsUnconfirmed) {
|
||||||
|
const data = yield this.kredits.Contribution.getById(c.id);
|
||||||
|
const contribution = this.loadContributionFromData(data);
|
||||||
|
yield this.browserCache.contributions.setItem(c.id.toString(), contribution.serialize());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.debug(`[kredits] No unconfirmed contributions to sync`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
veto (contributionId) {
|
veto (contributionId) {
|
||||||
console.debug('[kredits] veto against', contributionId);
|
console.debug('[kredits] veto against', contributionId);
|
||||||
const contribution = this.contributions.findBy('id', contributionId);
|
const contribution = this.contributions.findBy('id', contributionId);
|
||||||
@@ -289,27 +442,197 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getCurrentUser: computed('kredits.provider', function() {
|
//
|
||||||
if (isEmpty(this.currentUserAccounts)) {
|
// Generic data handling (for objects that can be vetoed)
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
findProposalById(proposalId) {
|
fetchObjects(objectClass, options = { page: { size: 200 } }) {
|
||||||
return this.proposals.findBy('id', proposalId.toString());
|
const objectClassLowerCase = objectClass.toLowerCase();
|
||||||
|
console.debug(`[kredits] Fetching ${objectClassLowerCase}s from the network`);
|
||||||
|
return this.kredits[objectClass].all(options)
|
||||||
|
.then(objects => {
|
||||||
|
return objects.map(data => {
|
||||||
|
const classInstance = this[`load${objectClass}FromData`](data);
|
||||||
|
return classInstance;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(objects => {
|
||||||
|
const cacheWrites = objects.map(o => {
|
||||||
|
return this.browserCache[objectClassLowerCase+'s']
|
||||||
|
.setItem(o.id.toString(), o.serialize());
|
||||||
|
});
|
||||||
|
return Promise.all(cacheWrites).then(() => {
|
||||||
|
console.debug(`[kredits] Cached ${objects.length} ${objectClassLowerCase+'s'} in browser storage`);
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeObjectFromCollectionIfLoaded (collection, objectId) {
|
||||||
|
const loadedObj = this[collection].findBy('id', objectId);
|
||||||
|
if (loadedObj) { this[collection].removeObject(loadedObj); }
|
||||||
|
},
|
||||||
|
|
||||||
|
async cacheLoadedObjects (collection) {
|
||||||
|
for (const o of this[collection]) {
|
||||||
|
await this.browserCache[collection].setItem(o.id, o.serialize());
|
||||||
|
}
|
||||||
|
console.debug(`[kredits] Cached ${this[collection].length} ${collection} in browser storage`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadObjectsFromCache (objectClass) {
|
||||||
|
const collection = objectClass.toLowerCase()+'s';
|
||||||
|
return this.browserCache[collection].iterate((value/*, key , iterationNumber */) => {
|
||||||
|
const obj = models[objectClass].create(JSON.parse(value));
|
||||||
|
this.removeObjectFromCollectionIfLoaded(collection, obj.id)
|
||||||
|
this[collection].pushObject(obj);
|
||||||
|
}).then((/* result */) => {
|
||||||
|
console.debug(`[kredits] Loaded ${this[collection].length} ${collection} from cache`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
syncTaskGroup: taskGroup().enqueue(),
|
||||||
|
|
||||||
|
fetchNewObjects: task(function * (objectClass) {
|
||||||
|
const collection = objectClass.toLowerCase()+'s';
|
||||||
|
const count = yield this.kredits[objectClass].functions[`${collection}Count`]();
|
||||||
|
const lastKnownObjectId = Math.max.apply(null, this[collection].mapBy('id'));
|
||||||
|
const toFetch = count - lastKnownObjectId;
|
||||||
|
|
||||||
|
if (toFetch > 0) {
|
||||||
|
console.debug(`[kredits] Fetching ${toFetch} new ${collection}`);
|
||||||
|
for (let id = lastKnownObjectId; id <= count; id++) {
|
||||||
|
const data = yield this.kredits[objectClass].getById(id);
|
||||||
|
const o = this[`load${objectClass}FromData`](data);
|
||||||
|
yield this.browserCache[collection].setItem(o.id.toString(), o.serialize());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.debug(`[kredits] No new ${collection} to fetch`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
fetchMissingObjects: task(function * (objectClass) {
|
||||||
|
const collection = objectClass.toLowerCase()+'s';
|
||||||
|
const count = yield this.kredits[objectClass].functions[`${collection}Count`]();
|
||||||
|
const allIds = [...Array(count+1).keys()];
|
||||||
|
allIds.shift(); // remove first item, which is 0
|
||||||
|
const loadedObjects = new Set(this[collection].mapBy('id'));
|
||||||
|
const toFetch = allIds.filter(id => !loadedObjects.has(id));
|
||||||
|
if (toFetch.length === 0) {
|
||||||
|
console.debug(`[kredits] No ${collection} left to fetch`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.debug(`[kredits] Fetching ${toFetch.length} past ${collection}`);
|
||||||
|
let countFetched = 0;
|
||||||
|
|
||||||
|
for (let id = count; id > 0; id--) {
|
||||||
|
if (loadedObjects.has(id)) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
const data = yield this.kredits[objectClass].getById(id);
|
||||||
|
const o = this[`load${objectClass}FromData`](data);
|
||||||
|
yield this.browserCache[collection].setItem(o.id.toString(), o.serialize());
|
||||||
|
countFetched++;
|
||||||
|
if (countFetched % 20 === 0) {
|
||||||
|
console.debug(`[kredits] Fetched ${countFetched} more ${collection}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.debug(`[kredits] Cached ${countFetched} past ${collection}`);
|
||||||
|
}),
|
||||||
|
|
||||||
|
syncUnconfirmedObjects: task(function * (objectClass) {
|
||||||
|
const collection = objectClass.toLowerCase()+'s';
|
||||||
|
if (this.get(`${collection}Unconfirmed`).length > 0) {
|
||||||
|
console.debug(`[kredits] Syncing unconfirmed ${collection}`);
|
||||||
|
for (const o of this[`${collection}Unconfirmed`]) {
|
||||||
|
if (isEmpty(o.id)) return;
|
||||||
|
const data = yield this.kredits[objectClass].getById(o.id);
|
||||||
|
const object = this[`load${objectClass}FromData`](data);
|
||||||
|
yield this.browserCache[collection]
|
||||||
|
.setItem(o.id.toString(), object.serialize());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.debug(`[kredits] No unconfirmed ${collection} to sync`);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
vetoAgainstObject (objectClass, objectId) {
|
||||||
|
console.debug(`[kredits] veto against ${objectClass.toLowerCase()}`, objectId);
|
||||||
|
const collection = objectClass.toLowerCase()+'s';
|
||||||
|
const object = this[collection].findBy('id', objectId);
|
||||||
|
|
||||||
|
return this.kredits[objectClass].functions.veto(objectId, { gasLimit: 300000 })
|
||||||
|
.then(data => {
|
||||||
|
console.debug('[kredits] veto response', data);
|
||||||
|
object.set('pendingTx', data);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reimbursements
|
||||||
|
//
|
||||||
|
|
||||||
|
loadReimbursementFromData(data) {
|
||||||
|
const obj = Reimbursement.create(processReimbursementData(data));
|
||||||
|
obj.set('contributor', this.contributors.findBy('id', data.recipientId));
|
||||||
|
this.removeObjectFromCollectionIfLoaded('reimbursements', obj.id);
|
||||||
|
this.reimbursements.pushObject(obj);
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
addReimbursement (attributes) {
|
||||||
|
console.debug('[kredits] add reimbursement', attributes);
|
||||||
|
|
||||||
|
return this.kredits.Reimbursement.add(attributes, { gasLimit: 300000 })
|
||||||
|
.then(data => {
|
||||||
|
console.debug('[kredits] add reimbursement response', data);
|
||||||
|
const reimbursement = Reimbursement.create(attributes);
|
||||||
|
reimbursement.setProperties({
|
||||||
|
contributor: this.contributors.findBy('id', attributes.recipientId),
|
||||||
|
pendingTx: data,
|
||||||
|
confirmedAt: this.currentBlock + 40320
|
||||||
|
});
|
||||||
|
this.reimbursements.pushObject(reimbursement);
|
||||||
|
|
||||||
|
// Listen to tx mining/execution status
|
||||||
|
data.wait()
|
||||||
|
.then(d => console.debug('[kredits] tx successful', d))
|
||||||
|
.catch(e => {
|
||||||
|
window.alert('The transaction failed to execute. Please check the browser console.');
|
||||||
|
console.log('[kredits] tx error', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
return reimbursement;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
syncReimbursements: task(function * () {
|
||||||
|
yield this.fetchNewObjects.perform('Reimbursement');
|
||||||
|
yield this.syncUnconfirmedObjects.perform('Reimbursement');
|
||||||
|
this.set('reimbursementsNeedSync', false);
|
||||||
|
}).group('syncTaskGroup'),
|
||||||
|
|
||||||
|
fetchMissingReimbursements: task(function * () {
|
||||||
|
yield this.fetchMissingObjects.perform('Reimbursement');
|
||||||
|
}).group('syncTaskGroup'),
|
||||||
|
|
||||||
|
vetoReimbursement (id) {
|
||||||
|
console.debug('[kredits] veto against reimbursement', id);
|
||||||
|
const reimbursement = this.reimbursements.findBy('id', id);
|
||||||
|
|
||||||
|
return this.kredits.Reimbursement.functions.veto(id, { gasLimit: 300000 })
|
||||||
|
.then(data => {
|
||||||
|
console.debug('[kredits] veto response', data);
|
||||||
|
reimbursement.set('pendingTx', data);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
// Contract events
|
// Contract events
|
||||||
|
//
|
||||||
|
|
||||||
addContractEventHandlers () {
|
addContractEventHandlers () {
|
||||||
this.kredits.Contributor
|
this.kredits.Contributor
|
||||||
.on('ContributorProfileUpdated', this.handleContributorChange.bind(this))
|
.on('ContributorProfileUpdated', this.handleContributorChange.bind(this))
|
||||||
@@ -320,10 +643,9 @@ export default Service.extend({
|
|||||||
.on('ContributionAdded', this.handleContributionAdded.bind(this))
|
.on('ContributionAdded', this.handleContributionAdded.bind(this))
|
||||||
.on('ContributionVetoed', this.handleContributionVetoed.bind(this))
|
.on('ContributionVetoed', this.handleContributionVetoed.bind(this))
|
||||||
|
|
||||||
this.kredits.Proposal
|
this.kredits.Reimbursement
|
||||||
.on('ProposalCreated', this.handleProposalCreated.bind(this))
|
.on('ReimbursementAdded', this.handleReimbursementAdded.bind(this))
|
||||||
.on('ProposalVoted', this.handleProposalVoted.bind(this))
|
.on('ReimbursementVetoed', this.handleReimbursementVetoed.bind(this))
|
||||||
.on('ProposalExecuted', this.handleProposalExecuted.bind(this));
|
|
||||||
|
|
||||||
this.kredits.Token
|
this.kredits.Token
|
||||||
.on('Transfer', this.handleTransfer.bind(this));
|
.on('Transfer', this.handleTransfer.bind(this));
|
||||||
@@ -335,13 +657,15 @@ export default Service.extend({
|
|||||||
const contributorData = await this.kredits.Contributor.getById(contributorId);
|
const contributorData = await this.kredits.Contributor.getById(contributorId);
|
||||||
const newContributor = Contributor.create(contributorData);
|
const newContributor = Contributor.create(contributorData);
|
||||||
|
|
||||||
const oldContributor = this.contributors.findBy('id', contributorId.toString());
|
// TODO check for actual differences in the contributor data first
|
||||||
|
|
||||||
|
const oldContributor = this.contributors.findBy('id', contributorId);
|
||||||
if (oldContributor) {
|
if (oldContributor) {
|
||||||
console.debug('[kredits] old contributor', oldContributor);
|
// console.debug('[kredits] cached contributor', oldContributor);
|
||||||
this.contributors.removeObject(oldContributor);
|
this.contributors.removeObject(oldContributor);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('[kredits] new contributor', newContributor);
|
// console.debug('[kredits] incoming contributor data', newContributor);
|
||||||
this.contributors.pushObject(newContributor);
|
this.contributors.pushObject(newContributor);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -350,69 +674,60 @@ export default Service.extend({
|
|||||||
|
|
||||||
const pendingContribution = this.contributions.find(c => {
|
const pendingContribution = this.contributions.find(c => {
|
||||||
return (c.id === null) &&
|
return (c.id === null) &&
|
||||||
(c.contributorId.toString() === contributorId.toString()) &&
|
(c.contributorId === contributorId) &&
|
||||||
(c.amount.toString() === amount.toString());
|
(c.amount.toString() === amount.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pendingContribution) {
|
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);
|
this.contributions.removeObject(pendingContribution);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await this.kredits.Contribution.getById(id);
|
||||||
|
const c = this.loadContributionFromData(data);
|
||||||
|
await this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
||||||
},
|
},
|
||||||
|
|
||||||
handleContributionVetoed (contributionId) {
|
async handleContributionVetoed (contributionId) {
|
||||||
console.debug('[kredits] ContributionVetoed event received for ', contributionId);
|
console.debug('[kredits] ContributionVetoed event received for #', contributionId);
|
||||||
const contribution = this.contributions.findBy('id', contributionId);
|
const c = this.contributions.findBy('id', contributionId);
|
||||||
console.debug('[kredits] contribution', contribution);
|
|
||||||
|
|
||||||
if (contribution) {
|
if (c) {
|
||||||
contribution.set('vetoed', true);
|
console.debug('[kredits] Updating contribution', c);
|
||||||
contribution.set('pendingTx', null);
|
c.set('vetoed', true);
|
||||||
|
c.set('pendingTx', null);
|
||||||
|
await this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleProposalCreated (proposalId) {
|
async handleReimbursementAdded (id, addedByAccount, amount) {
|
||||||
let proposal = this.findProposalById(proposalId);
|
console.debug('[kredits] ReimbursementAdded event received', { id, addedByAccount, amount });
|
||||||
|
|
||||||
if (proposal) {
|
const pendingReimbursement = this.reimbursementsPending.find(r => {
|
||||||
console.debug('[events] proposal exists, not adding from event');
|
return r.amount.toString() === amount.toString();
|
||||||
return;
|
});
|
||||||
|
|
||||||
|
if (pendingReimbursement) {
|
||||||
|
console.debug('[kredits] Found a pending reimbursement matching the event. Replacing it with the final record...');
|
||||||
|
this.reimbursements.removeObject(pendingReimbursement);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.kredits.Proposal.getById(proposalId)
|
const data = await this.kredits.Reimbursement.getById(id);
|
||||||
.then((proposal) => {
|
const r = this.loadReimbursementFromData(data);
|
||||||
proposal.contributor = this.contributors.findBy('id', proposal.contributorId.toString());
|
this.browserCache.reimbursements.setItem(r.id.toString(), r.serialize());
|
||||||
this.proposals.pushObject(Proposal.create(proposal));
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: We may want to reload that proposal to show the voter as voted
|
async handleReimbursementVetoed (id) {
|
||||||
handleProposalVoted (proposalId, voterId, totalVotes) {
|
console.debug(`[kredits] ReimbursementVetoed received for #${id}`);
|
||||||
let proposal = this.findProposalById(proposalId);
|
const r = this.reimbursements.findBy('id', id);
|
||||||
|
console.debug('[kredits] reimbursement', r);
|
||||||
|
|
||||||
if (proposal) {
|
if (r) {
|
||||||
proposal.set('votesCount', totalVotes);
|
r.set('vetoed', true);
|
||||||
|
r.set('pendingTx', null);
|
||||||
|
this.browserCache.reimbursements.setItem(r.id.toString(), r.serialize());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleProposalExecuted (proposalId, contributorId, amount) {
|
|
||||||
let proposal = this.findProposalById(proposalId);
|
|
||||||
|
|
||||||
if (proposal.get('isExecuted')) {
|
|
||||||
console.debug('[events] proposal already executed, not adding from event');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
proposal.set('executed', true);
|
|
||||||
|
|
||||||
this.contributors
|
|
||||||
.findBy('id', contributorId.toString())
|
|
||||||
.incrementProperty('balance', amount);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTransfer (from, to, value) {
|
handleTransfer (from, to, value) {
|
||||||
value = value.toNumber();
|
value = value.toNumber();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ button, input[type=submit], .button {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.1em;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
|
||||||
&+button, &+input[type=submit], &+.button {
|
&+button, &+input[type=submit], &+.button {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
@@ -19,7 +20,7 @@ button, input[type=submit], .button {
|
|||||||
background-color: rgba(22, 21, 40, 0.8);
|
background-color: rgba(22, 21, 40, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active, &.active {
|
&:focus, &:active, &.active {
|
||||||
border-color: $primary-color;
|
border-color: $primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ button, input[type=submit], .button {
|
|||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(40, 21, 21, 0.8);
|
background-color: rgba(40, 21, 21, 0.8);
|
||||||
}
|
}
|
||||||
&:active, &.active {
|
&:focus, &:active, &.active {
|
||||||
border-color: $red;
|
border-color: $red;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,7 @@ button, input[type=submit], .button {
|
|||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(21, 40, 21, 0.8);
|
background-color: rgba(21, 40, 21, 0.8);
|
||||||
}
|
}
|
||||||
&:active, &.active {
|
&:focus, &:active, &.active {
|
||||||
border-color: $green;
|
border-color: $green;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,3 +8,7 @@ $red: #fb6868;
|
|||||||
$primary-color: $blue;
|
$primary-color: $blue;
|
||||||
|
|
||||||
$body-text-color: #fff;
|
$body-text-color: #fff;
|
||||||
|
|
||||||
|
$item-background-color: rgba(255,255,255,0.1);
|
||||||
|
$item-highlighted-background-color: rgba(255,255,255,0.2);
|
||||||
|
$item-border-color: rgba(255,255,255,0.2);
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
section#add-contributor,
|
section#add-contributor,
|
||||||
section#add-contribution,
|
section#add-contribution,
|
||||||
section#add-proposal,
|
section#add-contribution,
|
||||||
|
section#add-item, // TODO use for all forms for adding data
|
||||||
section#signup {
|
section#signup {
|
||||||
|
|
||||||
form {
|
form {
|
||||||
|
h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 1.5rem;
|
font-size: 1.2rem;
|
||||||
|
|
||||||
&.mg-bottom-md {
|
&.mg-bottom-md {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.label {
|
&.label {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
margin-bottom: .5rem;
|
margin-bottom: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.actions {
|
&.actions {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
padding-top: 1.5rem;
|
padding-top: 1.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
a {
|
a {
|
||||||
@@ -30,9 +39,34 @@ section#signup {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text], select {
|
fieldset {
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 2rem;
|
||||||
|
|
||||||
|
&.thirds {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.total-amounts {
|
||||||
|
grid-template-columns: 2fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text], input[type=url], select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -40,17 +74,24 @@ 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;
|
||||||
|
font-weight: normal;
|
||||||
transition: border-color 0.1s linear;
|
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 {
|
&:focus :not(:invalid) {
|
||||||
border-color: $blue;
|
border-color: $blue;
|
||||||
}
|
}
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: rgba(238, 238, 238, 0.5);
|
color: rgba(238, 238, 238, 0.5);
|
||||||
}
|
}
|
||||||
|
&.invalid {
|
||||||
|
border-color: $red;
|
||||||
|
}
|
||||||
|
&:disabled {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -110,7 +151,5 @@ section#signup {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
ul.item-list {
|
||||||
|
list-style: none;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
padding: 0.8rem 1.2rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
background-color: $item-background-color;
|
||||||
|
border-bottom: 1px solid $item-border-color;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
border-top: 1px solid $item-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: $item-highlighted-background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
@include loading-border-top;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
&:first-of-type {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.spaced {
|
||||||
|
> li {
|
||||||
|
border-top: 1px solid $item-border-color;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,15 @@ main {
|
|||||||
"contributions";
|
"contributions";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&#budget {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-row-gap: 2rem;
|
||||||
|
grid-template-areas:
|
||||||
|
"aside"
|
||||||
|
"content";
|
||||||
|
}
|
||||||
|
|
||||||
&.center-column {
|
&.center-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -29,6 +38,16 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div#content {
|
||||||
|
section {
|
||||||
|
&:first-of-type {
|
||||||
|
@include media-max(small) {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
.content {
|
.content {
|
||||||
a {
|
a {
|
||||||
@@ -68,7 +87,7 @@ main {
|
|||||||
|
|
||||||
@include media-max(small) {
|
@include media-max(small) {
|
||||||
padding: 2rem 1rem;
|
padding: 2rem 1rem;
|
||||||
font-size: 1.4rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
@@ -111,6 +130,13 @@ main {
|
|||||||
"stats contributions details";
|
"stats contributions details";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&#budget {
|
||||||
|
grid-column-gap: 3rem;
|
||||||
|
grid-template-columns: 2fr 4fr 2fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"aside content empty";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
@mixin loading-border-top {
|
||||||
|
&::before {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
content: '';
|
||||||
|
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.2) 40%, #68d7fb 60%, rgba(255, 255, 255, 0.2));
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: kitt 2.5s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes kitt {
|
||||||
|
0%{ background-position: 0% 0%}
|
||||||
|
50%{ background-position: 100% 0%}
|
||||||
|
100%{ background-position: 0% 0%}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ a {
|
|||||||
|
|
||||||
section {
|
section {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.5rem;
|
||||||
color: $primary-color;
|
color: $primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,22 +76,6 @@ section {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&#proposals-open, &#proposals-closed {
|
|
||||||
.actions {
|
|
||||||
padding-top: 3rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: $primary-color;
|
|
||||||
text-align: center;
|
|
||||||
@include media-max(small) {
|
|
||||||
padding-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $primary-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 550px) {
|
@media (min-width: 550px) {
|
||||||
@@ -104,14 +88,18 @@ section {
|
|||||||
|
|
||||||
@import "buttons";
|
@import "buttons";
|
||||||
@import "forms";
|
@import "forms";
|
||||||
|
@import "sugar";
|
||||||
|
@import "item-list";
|
||||||
|
|
||||||
@import "components/contribution-list";
|
@import "components/budget-balances";
|
||||||
@import "components/contribution-details";
|
@import "components/contribution-details";
|
||||||
|
@import "components/contribution-list";
|
||||||
@import "components/contributor-list";
|
@import "components/contributor-list";
|
||||||
@import "components/contributor-profile";
|
@import "components/contributor-profile";
|
||||||
|
@import "components/expense-list";
|
||||||
@import "components/external-account-link";
|
@import "components/external-account-link";
|
||||||
@import "components/loading-spinner";
|
@import "components/loading-spinner";
|
||||||
@import "components/proposal-list";
|
@import "components/reimbursement-list";
|
||||||
@import "components/topbar";
|
@import "components/topbar";
|
||||||
@import "components/topbar-account-panel";
|
@import "components/topbar-account-panel";
|
||||||
@import "components/user-avatar";
|
@import "components/user-avatar";
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
section#funds {
|
||||||
|
@include media-max(small) {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.token-balances {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s linear;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
&.amount {
|
||||||
|
font-size: 2rem;
|
||||||
|
padding-right: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fiat-amount {
|
||||||
|
color: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,9 @@ section#contribution-details {
|
|||||||
margin: 0 0 1.5rem;
|
margin: 0 0 1.5rem;
|
||||||
padding: 2rem 2rem;
|
padding: 2rem 2rem;
|
||||||
|
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
border-top: 1px solid $item-border-color;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
border-bottom: 1px solid $item-border-color;
|
||||||
background-color: rgba(255,255,255,0.1);
|
background-color: $item-background-color;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@@ -67,7 +67,7 @@ section#contribution-details {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
border-top: 1px solid $item-border-color;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
@@ -84,5 +84,17 @@ section#contribution-details {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On small screens, hide intro text, contributor and contributions list when
|
||||||
|
// showing details
|
||||||
|
@include media-max(small) {
|
||||||
|
#dashboard.with-details {
|
||||||
|
#contributions, #stats {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#intro.with-details {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,22 +24,13 @@ main section {
|
|||||||
ul.contribution-list {
|
ul.contribution-list {
|
||||||
clear: both;
|
clear: both;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 5rem 5rem;
|
grid-template-columns: auto 5rem 5rem;
|
||||||
grid-row-gap: 0.5rem;
|
grid-row-gap: 0.5rem;
|
||||||
padding: 0.8rem 1.2rem;
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
font-size: 1.2rem;
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.confirmed {
|
&.confirmed {
|
||||||
grid-template-columns: auto 5rem;
|
grid-template-columns: auto 5rem;
|
||||||
}
|
}
|
||||||
@@ -50,10 +41,6 @@ ul.contribution-list {
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background-color: rgba(255,255,255,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -113,39 +100,4 @@ ul.contribution-list {
|
|||||||
margin-right: 0.5rem;
|
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,19 +1,20 @@
|
|||||||
table.contributor-list {
|
table.contributor-list {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
background-color: rgba(255,255,255,0.1);
|
background-color: $item-background-color;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
border-bottom: 1px solid $item-border-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
border-top: 1px solid $item-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
background-color: rgba(255,255,255,0.2);
|
background-color: $item-highlighted-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.current-user {
|
&.current-user {
|
||||||
@@ -48,4 +49,11 @@ table.contributor-list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
@include loading-border-top;
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ section#contributor-profile {
|
|||||||
|
|
||||||
img {
|
img {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border: 3px solid rgba(255,255,255,0.2);
|
border: 3px solid $item-border-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,9 +18,9 @@ section#contributor-profile {
|
|||||||
margin: -7.2rem 0 1.5rem;
|
margin: -7.2rem 0 1.5rem;
|
||||||
padding: 6rem 1.2rem 2rem;
|
padding: 6rem 1.2rem 2rem;
|
||||||
|
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
border-top: 1px solid $item-border-color;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
border-bottom: 1px solid $item-border-color;
|
||||||
background-color: rgba(255,255,255,0.1);
|
background-color: $item-background-color;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -42,6 +42,10 @@ section#contributor-profile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
ul.expense-list {
|
||||||
|
grid-column-start: span 2;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 10rem;
|
||||||
|
grid-row-gap: 0.5rem;
|
||||||
|
padding-top: 1.2rem;
|
||||||
|
border-top: 1px solid $item-border-color;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
padding-bottom: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
grid-row-start: 1;
|
||||||
|
grid-row-end: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
justify-self: end;
|
||||||
|
// font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 2rem;
|
||||||
|
|
||||||
|
&.description {
|
||||||
|
font-size: 1rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
margin-top: 12rem;
|
margin-top: 12rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1.4rem;
|
font-size: 1.5rem;
|
||||||
color: $primary-color;
|
color: $primary-color;
|
||||||
|
|
||||||
@include media-max(small) {
|
@include media-max(small) {
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
ul.proposal-list {
|
|
||||||
clear: both;
|
|
||||||
width: 100%;
|
|
||||||
list-style: none;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 5rem;
|
|
||||||
grid-row-gap: 0.5rem;
|
|
||||||
padding: 1rem 1.2rem;
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
font-size: 1.2rem;
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
|
||||||
|
|
||||||
&:first-of-type {
|
|
||||||
border-top: 1px solid rgba(255,255,255,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
align-self: center;
|
|
||||||
margin: 0;
|
|
||||||
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 {
|
|
||||||
color: $blue;
|
|
||||||
padding-right: 0.2rem;
|
|
||||||
&.community { color: $red; }
|
|
||||||
&.dev { color: $pink; }
|
|
||||||
&.design { color: $yellow; }
|
|
||||||
&.docs { color: $green; }
|
|
||||||
&.ops { color: $purple; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.symbol {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
padding-left: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipient {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.votes {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: $primary-color;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 550px) {
|
|
||||||
ul.proposal-list {
|
|
||||||
li {
|
|
||||||
grid-template-columns: auto 10rem;
|
|
||||||
grid-row-gap: 0.5rem;
|
|
||||||
|
|
||||||
p {
|
|
||||||
&.kredits-amount, &.voting {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.unconfirmed {
|
|
||||||
p {
|
|
||||||
&.kredits-amount, &.voting {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.description {
|
|
||||||
grid-column-start: span 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.voting {
|
|
||||||
grid-column-start: span 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.description {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
ul.reimbursement-list {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 12rem;
|
||||||
|
grid-row-gap: 0.5rem;
|
||||||
|
padding-top: 1.6rem;
|
||||||
|
|
||||||
|
&.vetoed {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-amount {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{{loading-spinner}}
|
<LoadingSpinner />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<header id="topbar">
|
<header id="topbar">
|
||||||
<h1>{{link-to "Kosmos Kredits" "dashboard"}}</h1>
|
<h1><LinkTo @route="dashboard">Kosmos Kredits</LinkTo></h1>
|
||||||
{{topbar-account-panel}}
|
<TopbarAccountPanel />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<main id="budget">
|
||||||
|
|
||||||
|
<div id="aside">
|
||||||
|
<!-- <section id="main-nav"> -->
|
||||||
|
<!-- <header> -->
|
||||||
|
<!-- <h2>Budget</h2> -->
|
||||||
|
<!-- </header> -->
|
||||||
|
<!-- <div class="content"> -->
|
||||||
|
<!-- <ul> -->
|
||||||
|
<!-- <li> -->
|
||||||
|
<!-- <LinkTo @route="budget.expenses">Expenses</LinkTo> -->
|
||||||
|
<!-- </li> -->
|
||||||
|
<!-- <li> -->
|
||||||
|
<!-- <a href="#">Rewards</a> -->
|
||||||
|
<!-- </li> -->
|
||||||
|
<!-- </ul> -->
|
||||||
|
<!-- </div> -->
|
||||||
|
<!-- </section> -->
|
||||||
|
|
||||||
|
<section id="funds">
|
||||||
|
<header>
|
||||||
|
<h2>Community funds</h2>
|
||||||
|
</header>
|
||||||
|
<div class="content">
|
||||||
|
<BudgetBalances />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
{{#if this.reimbursementsUnconfirmed}}
|
||||||
|
<section id="expenses-unconfirmed">
|
||||||
|
<header class="with-nav">
|
||||||
|
<h2>Proposed Reimbursements</h2>
|
||||||
|
<nav>
|
||||||
|
<LinkTo @route="reimbursements.new" @title="Submit a reimbursement" class="button small green">add</LinkTo>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div class="content">
|
||||||
|
<ReimbursementList @items={{this.reimbursementsUnconfirmed}} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.reimbursementsConfirmed}}
|
||||||
|
<section id="expenses-confirmed">
|
||||||
|
<header class="with-nav">
|
||||||
|
<h2>Confirmed Expenses</h2>
|
||||||
|
<nav>
|
||||||
|
<LinkTo @route="reimbursements.new" @title="Submit a reimbursement" class="button small green">add</LinkTo>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<div class="content">
|
||||||
|
<ReimbursementList @items={{this.reimbursementsConfirmed}} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="empty">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{{outlet}}
|
||||||
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 703 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 750 B After Width: | Height: | Size: 750 B |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -6,7 +6,8 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{add-contribution contributors=sortedContributors save=(action "save")}}
|
<AddContribution @attributes={{this.model.params}}
|
||||||
|
@save={{action "save"}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
<section id="add-contribution">
|
<section id="add-contribution">
|
||||||
<header>
|
<header>
|
||||||
<h2>Re-submit contribution #{{model.id}}</h2>
|
<h2>Re-submit contribution #{{this.model.id}}</h2>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{add-contribution attributes=attributes contributors=sortedContributors save=(action "save")}}
|
<AddContribution @attributes={{this.attributes}}
|
||||||
|
@contributors={{this.sortedContributors}}
|
||||||
|
@save={{action "save"}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{add-contributor attributes=attributes save=(action "save")}}
|
<AddContributor @attributes={{this.attributes}} @save={{action "save"}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{add-contributor save=(action "save")}}
|
<AddContributor @save={{action "save"}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,45 @@
|
|||||||
{{#if showIntroText}}
|
{{#if this.showIntroText}}
|
||||||
<div id="intro">
|
<div id="intro" class={{if this.showDetailsPane "with-details"}}>
|
||||||
<h2>
|
<h2>
|
||||||
Welcome to the contribution dashboard of the
|
Welcome to the contribution dashboard of the
|
||||||
<a href="https://kosmos.org" target="_blank" rel="noopener">Kosmos</a> project!
|
<a href="https://kosmos.org" target="_blank" rel="noreferrer noopener">Kosmos</a> project!
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
If you want to learn more about what the numbers mean and how this works,
|
If you want to learn more about what the numbers mean and how this works,
|
||||||
check out the
|
check out the
|
||||||
<a href="https://wiki.kosmos.org/Kredits" target="_blank" rel="noopener">Kredits documentation</a>.
|
<a href="https://wiki.kosmos.org/Kredits" target="_blank" rel="noreferrer noopener">Kredits documentation</a>.
|
||||||
If you want to start earning kredits for your contributions,
|
If you want to start earning kredits for your contributions,
|
||||||
{{link-to "create a contributor profile" "signup"}}.
|
<LinkTo @route="signup">create a contributor profile</LinkTo>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<main id="dashboard" class={{if showDetailsPane "with-details"}}>
|
<main id="dashboard" class={{if this.showDetailsPane "with-details"}}>
|
||||||
|
|
||||||
<div id="stats">
|
<div id="stats">
|
||||||
<section id="people">
|
<section id="people">
|
||||||
<header class="with-nav">
|
<header class="with-nav">
|
||||||
<h2>Contributors</h2>
|
<h2>Contributors</h2>
|
||||||
{{#if kredits.hasAccounts}}
|
{{#if this.kredits.hasAccounts}}
|
||||||
<nav>
|
<nav>
|
||||||
{{link-to "add" "contributors.new" title="Add contributor profile" class="button small green"}}
|
<LinkTo @route="contributors.new" @title="Add contributor profile" class="button small green">add</LinkTo>
|
||||||
</nav>
|
</nav>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</header>
|
</header>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{contributor-list contributorList=kreditsToplist
|
<ContributorList @contributorList={{this.kreditsToplist}}
|
||||||
showUnconfirmedKredits=showUnconfirmedKredits
|
@showUnconfirmedKredits={{this.showUnconfirmedKredits}}
|
||||||
selectedContributorId=selectedContributorId}}
|
@selectedContributorId={{this.selectedContributorId}}
|
||||||
|
@loading={{this.kredits.syncContributors.isRunning}} />
|
||||||
|
|
||||||
<p class="stats">
|
<p class="stats">
|
||||||
<span class="number">{{await kredits.totalKreditsEarned}}</span> kredits confirmed and issued to
|
<span class="number">{{await this.kredits.totalKreditsEarned}}</span> kredits confirmed and issued to
|
||||||
<span class="number">{{contributorsWithKredits.length}}</span> contributors
|
<span class="number">{{this.contributorsWithKredits.length}}</span> contributors
|
||||||
</p>
|
</p>
|
||||||
<p class="stats">
|
<p class="stats">
|
||||||
{{input type="checkbox" id="hide-unnconfirmed-kredits" checked=showUnconfirmedKredits}}
|
<Input id="hide-unnconfirmed-kredits"
|
||||||
|
@type="checkbox"
|
||||||
|
@checked={{this.showUnconfirmedKredits}} />
|
||||||
<label for="hide-unnconfirmed-kredits">Show unconfirmed kredits in toplist</label>
|
<label for="hide-unnconfirmed-kredits">Show unconfirmed kredits in toplist</label>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,30 +50,39 @@
|
|||||||
<h2>Contributions by type</h2>
|
<h2>Contributions by type</h2>
|
||||||
</header>
|
</header>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{chart-contributions-by-type contributions=contributions}}
|
<ChartContributionsByType @contributions={{this.contributions}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="contributions">
|
<div id="contributions">
|
||||||
{{#if contributionsUnconfirmed}}
|
{{#if this.contributionsUnconfirmed}}
|
||||||
<section id="contributions-unconfirmed">
|
<section id="contributions-unconfirmed">
|
||||||
<header class="with-nav">
|
<header class="with-nav">
|
||||||
<h2>Latest Contributions</h2>
|
<h2>
|
||||||
|
Latest Contributions
|
||||||
|
</h2>
|
||||||
<nav>
|
<nav>
|
||||||
<button {{action "toggleQuickFilterUnconfirmed"}} class="small {{if showQuickFilterUnconfirmed "active"}}">filter</button>
|
<button type="button"
|
||||||
{{#if kredits.hasAccounts}}
|
onclick={{action "toggleQuickFilterUnconfirmed"}}
|
||||||
{{link-to "add" "contributions.new" title="Submit a contribution" class="button small green"}}
|
class="small {{if this.showQuickFilterUnconfirmed "active"}}">
|
||||||
|
filter
|
||||||
|
</button>
|
||||||
|
{{#if this.kredits.hasAccounts}}
|
||||||
|
<LinkTo @route="contributions.new"
|
||||||
|
@title="Submit a contribution"
|
||||||
|
class="button small green">add</LinkTo>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{!-- TODO: We need a better naming for kredits.hasAccounts --}}
|
{{!-- TODO: We need a better naming for kredits.hasAccounts --}}
|
||||||
{{contribution-list contributions=contributionsUnconfirmedSorted
|
<ContributionList @contributions={{this.contributionsUnconfirmedSorted}}
|
||||||
vetoContribution=(action "vetoContribution")
|
@vetoContribution={{action "vetoContribution"}}
|
||||||
contractInteractionEnabled=kredits.hasAccounts
|
@contractInteractionEnabled={{this.kredits.hasAccounts}}
|
||||||
selectedContributionId=selectedContributionId
|
@selectedContributionId={{this.selectedContributionId}}
|
||||||
showQuickFilter=showQuickFilterUnconfirmed}}
|
@showQuickFilter={{this.showQuickFilterUnconfirmed}}
|
||||||
|
@loading={{this.kredits.syncContributions.isRunning}}/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -79,14 +91,18 @@
|
|||||||
<header class="with-nav">
|
<header class="with-nav">
|
||||||
<h2>Confirmed Contributions</h2>
|
<h2>Confirmed Contributions</h2>
|
||||||
<nav>
|
<nav>
|
||||||
<button {{action "toggleQuickFilterConfirmed"}} class="small {{if showQuickFilterConfirmed "active"}}">filter</button>
|
<button type="button"
|
||||||
|
onclick={{action "toggleQuickFilterConfirmed"}}
|
||||||
|
class="small {{if this.showQuickFilterConfirmed "active"}}">
|
||||||
|
filter
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{contribution-list contributions=contributionsConfirmedSorted
|
<ContributionList @contributions={{this.contributionsConfirmedSorted}}
|
||||||
vetoContribution=(action "vetoContribution")
|
@vetoContribution={{action "vetoContribution"}}
|
||||||
selectedContributionId=selectedContributionId
|
@selectedContributionId={{this.selectedContributionId}}
|
||||||
showQuickFilter=showQuickFilterConfirmed}}
|
@showQuickFilter={{this.showQuickFilterConfirmed}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<section id="contribution-details" class={{if model.vetoed "vetoed"}}>
|
<section id="contribution-details" class={{if this.model.vetoed "vetoed"}}>
|
||||||
<header class="with-nav">
|
<header class="with-nav">
|
||||||
<h2>Contribution #{{model.id}}</h2>
|
<h2>Contribution #{{this.model.id}}</h2>
|
||||||
<nav>
|
<nav>
|
||||||
<span class="amount">{{model.amount}}</span>
|
<span class="amount">{{this.model.amount}}</span>
|
||||||
<span class="symbol">₭S</span>
|
<span class="symbol">₭S</span>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -10,39 +10,39 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<p class="who-what-when">
|
<p class="who-what-when">
|
||||||
<span class="contributor">
|
<span class="contributor">
|
||||||
{{user-avatar contributor=model.contributor}}
|
<UserAvatar @contributor={{this.model.contributor}} />
|
||||||
{{link-to model.contributor.name "dashboard.contributors.show" model.contributor}}
|
<LinkTo @route="dashboard.contributors.show" @model={{this.model.contributor}}>{{this.model.contributor.name}}</LinkTo>
|
||||||
</span>
|
</span>
|
||||||
contributed
|
contributed
|
||||||
<span class="date" title={{model.iso8601Date}}>{{moment-from model.iso8601Date}}</span>:
|
<span class="date" title={{this.model.iso8601Date}}>{{moment-from this.model.iso8601Date}}</span>:
|
||||||
</p>
|
</p>
|
||||||
<h3>{{model.description}}</h3>
|
<h3>{{this.model.description}}</h3>
|
||||||
<p>
|
<p>
|
||||||
Kind: {{model.kind}}
|
Kind: {{this.model.kind}}
|
||||||
<br>Status: {{contribution-status model}}
|
<br>Status: {{item-status this.model}}
|
||||||
</p>
|
</p>
|
||||||
{{#if model.url}}
|
{{#if this.model.url}}
|
||||||
<p>
|
<p>
|
||||||
<a href={{model.url}}
|
<a href={{this.model.url}}
|
||||||
title={{model.description}}
|
title={{this.model.description}}
|
||||||
class="button"
|
class="button"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener">
|
rel="noopener noreferrer">
|
||||||
Open URL
|
Open URL
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if model.vetoed}}
|
{{#if this.model.vetoed}}
|
||||||
<div class="hint vetoed">
|
<div class="hint vetoed">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
{{icon-warning}}
|
<IconWarning />
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
This contribution has been vetoed, meaning no
|
This contribution has been vetoed, meaning no
|
||||||
kredits will be issued.
|
kredits will be issued.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{{link-to "Re-submit contribution …" "contributions.resubmit" model class="button small green"}}.
|
<LinkTo @route="contributions.resubmit" @model={{this.model}} class="button small green">Re-submit contribution …</LinkTo>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@@ -50,10 +50,10 @@
|
|||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<p>
|
<p>
|
||||||
{{link-to "Copy & edit as new" "contributions.resubmit" model class="button small"}}.
|
<LinkTo @route="contributions.resubmit" @model={{this.model}} class="button small">Copy & edit as new</LinkTo>
|
||||||
{{#if model.ipfsHash}}
|
{{#if this.model.ipfsHash}}
|
||||||
<a href="{{ipfsGatewayUrl}}/{{model.ipfsHash}}"
|
<a href="{{this.ipfsGatewayUrl}}/{{this.model.ipfsHash}}"
|
||||||
class="button small" target="_blank" rel="noopener">
|
class="button small" target="_blank" rel="noopener noreferrer">
|
||||||
Inspect IPFS data
|
Inspect IPFS data
|
||||||
</a>
|
</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -1,36 +1,41 @@
|
|||||||
<section id="contributor-profile">
|
<section id="contributor-profile">
|
||||||
<header>
|
<header>
|
||||||
{{user-avatar contributor=model size="medium"}}
|
<UserAvatar @contributor={{this.model}} @size="medium" />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h2>{{model.name}}</h2>
|
<h2>{{this.model.name}}</h2>
|
||||||
<p class="role">{{roleName}}</p>
|
<p class="role">{{this.roleName}}</p>
|
||||||
|
|
||||||
<ul class="external-accounts">
|
<ul class="external-accounts">
|
||||||
{{#each model.accounts as |account|}}
|
{{#each this.model.accounts as |account|}}
|
||||||
<li>
|
<li>
|
||||||
{{external-account-link account=account}}
|
<ExternalAccountLink @account={{account}} />
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if model.url}}
|
{{#if this.model.url}}
|
||||||
<li>
|
<li>
|
||||||
<a href={{model.url}} target="_blank" rel="noopener" title="Website">
|
<a href={{this.model.url}} target="_blank" rel="noopener noreferrer" title="Website">
|
||||||
<span class="site">Web</span>
|
<span class="site">Web</span>
|
||||||
{{icon-web-globe}}
|
<IconWebGlobe />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
|
<div class="actions">
|
||||||
|
<p>
|
||||||
|
<LinkTo @route="contributions.new" @query={{hash contributorId=this.model.id}} class="button green">♥ Give kredits</LinkTo>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<p>
|
<p>
|
||||||
<a href="https://rinkeby.etherscan.io/address/{{model.account}}" class="button small" target="_blank" rel="noopener">Inspect Ethereum transactions</a>
|
<a href="https://rinkeby.etherscan.io/address/{{this.model.account}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect Ethereum transactions</a>
|
||||||
{{#if model.ipfsHash}}
|
{{#if this.model.ipfsHash}}
|
||||||
<a href="{{ipfsGatewayUrl}}/{{model.ipfsHash}}" class="button small" target="_blank" rel="noopener">Inspect IPFS profile</a>
|
<a href="{{this.ipfsGatewayUrl}}/{{this.model.ipfsHash}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect IPFS profile</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{link-to "Edit profile" "contributors.edit" model class="button small"}}
|
<LinkTo @route="contributors.edit" @model={{this.model}} class="button small">Edit profile</LinkTo>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<section id="add-proposal">
|
|
||||||
<header>
|
|
||||||
<h2>Add Proposal</h2>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
{{add-proposal contributors=minedContributors save=(action "save")}}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<main class="center-column">
|
||||||
|
|
||||||
|
<section id="add-item">
|
||||||
|
<header>
|
||||||
|
<h2>Submit a Reimbursement</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<AddReimbursement @attributes={{this.model.params}} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
@@ -10,6 +10,6 @@
|
|||||||
<a href="https://wiki.kosmos.org/Main_Page#Community_.2F_Getting_in_touch_.2F_Getting_involved">in one of our chat rooms</a>?.
|
<a href="https://wiki.kosmos.org/Main_Page#Community_.2F_Getting_in_touch_.2F_Getting_involved">in one of our chat rooms</a>?.
|
||||||
</p>
|
</p>
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
{{#link-to "dashboard" class="button small"}}Return to dashboard{{/link-to}}
|
<LinkTo @route="dashboard" class="button small">Return to dashboard</LinkTo>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||