Compare commits
112 Commits
v1.16.0
...
v2.0.0-beta.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
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 |
+18
-4
@@ -2,13 +2,21 @@
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
parser: 'babel-eslint',
|
parser: '@babel/eslint-parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2018,
|
requireConfigFile: false,
|
||||||
|
ecmaVersion: 2020,
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
legacyDecorators: true
|
legacyDecorators: true
|
||||||
}
|
},
|
||||||
|
babelOptions: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
plugins: [
|
||||||
|
['@babel/plugin-proposal-decorators', { legacy: true }],
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
console: true
|
console: true
|
||||||
@@ -28,7 +36,13 @@ module.exports = {
|
|||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
'ember/no-jquery': 'error',
|
'ember/no-jquery': 'error',
|
||||||
'ember/require-computed-property-dependencies': 'warn',
|
'ember/require-computed-property-dependencies': 'warn',
|
||||||
'ember/no-observers': '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
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@ module.exports = {
|
|||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
'simple-unless': false,
|
'simple-unless': false,
|
||||||
'no-nested-interactive': false
|
'no-nested-interactive': false,
|
||||||
|
'no-html-comments': false
|
||||||
},
|
},
|
||||||
|
|
||||||
ignore: [
|
ignore: [
|
||||||
|
|||||||
+5
-15
@@ -1,31 +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:
|
|
||||||
global:
|
|
||||||
# See https://git.io/vdao3 for details.
|
|
||||||
- JOBS=1
|
|
||||||
|
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
script:
|
|
||||||
- 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"]'
|
||||||
|
|||||||
@@ -3,11 +3,21 @@ import { computed } from '@ember/object';
|
|||||||
import { and, notEmpty } from '@ember/object/computed';
|
import { and, notEmpty } from '@ember/object/computed';
|
||||||
import { assign } from '@ember/polyfills';
|
import { 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'),
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<select required onchange={{action (mut this.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 this.contributorId contributor.id}}>{{contributor.name}}</option>
|
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
@@ -15,11 +15,14 @@
|
|||||||
<p>
|
<p>
|
||||||
<select required onchange={{action (mut this.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="bureaucracy" selected={{eq this.kind "bureaucracy"}}>Bureaucracy</option>
|
||||||
<option value="community" selected={{eq this.kind "community"}}>Community</option>
|
<option value="community" selected={{eq this.kind "community"}}>Community</option>
|
||||||
<option value="design" selected={{eq this.kind "design"}}>Design</option>
|
<option value="design" selected={{eq this.kind "design"}}>Design</option>
|
||||||
<option value="dev" selected={{eq this.kind "dev"}}>Development</option>
|
<option value="dev" selected={{eq this.kind "dev"}}>Development</option>
|
||||||
<option value="docs" selected={{eq this.kind "docs"}}>Documentation</option>
|
<option value="docs" selected={{eq this.kind "docs"}}>Documentation</option>
|
||||||
<option value="ops" selected={{eq this.kind "ops"}}>IT Operations</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>
|
<option value="special" selected={{eq this.kind "special"}}>Special</option>
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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 ] = dateInput.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());
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,89 @@
|
|||||||
|
<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>
|
||||||
@@ -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>
|
||||||
@@ -24,10 +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() {
|
||||||
const activeIds = new Set(this.contributions.mapBy('contributorId')
|
const activeIds = new Set(this.contributions.mapBy('contributorId'));
|
||||||
.map(id => id.toString()));
|
|
||||||
|
|
||||||
return this.contributors.filter(c => activeIds.has(c.id.toString()));
|
return this.contributors.filter(c => activeIds.has(c.id));
|
||||||
}),
|
}),
|
||||||
|
|
||||||
contributionKinds: computed('contributions.[]', function() {
|
contributionKinds: computed('contributions.[]', function() {
|
||||||
@@ -42,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; }
|
||||||
|
|||||||
@@ -29,11 +29,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<ul class="contribution-list">
|
<ul class="item-list contribution-list {{if @loading 'loading'}}">
|
||||||
{{#each this.contributionsFiltered as |contribution|}}
|
{{#each this.contributionsFiltered as |contribution|}}
|
||||||
<li role="button" data-contribution-id={{contribution.id}}
|
<li role="button" data-contribution-id={{contribution.id}}
|
||||||
{{action "openContributionDetails" contribution}}
|
{{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"><UserAvatar @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>
|
||||||
|
|||||||
@@ -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,21 +1,25 @@
|
|||||||
<tbody>
|
<table class="contributor-list {{if @loading 'loading'}}">
|
||||||
{{#each @contributorList as |c|}}
|
<thead>
|
||||||
<tr role="button"
|
</thead>
|
||||||
onclick={{action "openContributorDetails" c.contributor}}
|
<tbody>
|
||||||
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id @selectedContributorId) "selected"}}">
|
{{#each @contributorList as |c|}}
|
||||||
<td class="person">
|
<tr role="button"
|
||||||
<UserAvatar @contributor={{c.contributor}} /> {{c.contributor.name}}
|
onclick={{action "openContributorDetails" c.contributor}}
|
||||||
</td>
|
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id @selectedContributorId) "selected"}}">
|
||||||
<td class="kredits">
|
<td class="person">
|
||||||
<span class="amount">
|
<UserAvatar @contributor={{c.contributor}} /> {{c.contributor.name}}
|
||||||
{{#if @showUnconfirmedKredits}}
|
</td>
|
||||||
{{c.amountTotal}}
|
<td class="kredits">
|
||||||
{{else}}
|
<span class="amount">
|
||||||
{{c.amountConfirmed}}
|
{{#if @showUnconfirmedKredits}}
|
||||||
{{/if}}
|
{{c.amountTotal}}
|
||||||
</span>
|
{{else}}
|
||||||
<span class="symbol">₭S</span>
|
{{c.amountConfirmed}}
|
||||||
</td>
|
{{/if}}
|
||||||
</tr>
|
</span>
|
||||||
{{/each}}
|
<span class="symbol">₭S</span>
|
||||||
</tbody>
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { sort } from '@ember/object/computed';
|
||||||
|
|
||||||
|
export default class ReimbursementListComponent extends Component {
|
||||||
|
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
|
||||||
|
@sort('args.items', 'itemSorting') itemsSorted;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<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="recipient"><UserAvatar @contributor={{reimbursement.contributor}} /></span>
|
||||||
|
<span class="title">Expenses covered by {{reimbursement.contributor.name}}</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>
|
||||||
|
</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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -41,7 +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);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +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('hasPendingChanges', this, this.triggerRecompute);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
triggerRecompute () {
|
|
||||||
once(this, 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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -29,4 +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');
|
||||||
|
|
||||||
|
this.route('reimbursements', function() {});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route('reimbursements', function() {
|
||||||
|
this.route('new');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+23
-14
@@ -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 {
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import Route from '@ember/routing/route';
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
export default Route.extend({
|
export default class ContributionsNewRoute extends Route {
|
||||||
|
|
||||||
model (params) {
|
model (params) {
|
||||||
return { 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+512
-119
@@ -1,81 +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 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,
|
||||||
|
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('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
|
||||||
@@ -84,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}`);
|
||||||
@@ -96,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,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);
|
||||||
@@ -165,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) {
|
||||||
@@ -205,40 +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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getContributions () {
|
fetchContributions (options = { page: { size: 200 } }) {
|
||||||
return this.kredits.Contribution.all({page: {size: 200}})
|
console.debug(`[kredits] Fetching contributions from the network`);
|
||||||
|
return this.kredits.Contribution.all(options)
|
||||||
.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`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loadContributionFromData(data) {
|
||||||
|
const contribution = Contribution.create(processContributionData(data));
|
||||||
|
contribution.set('contributor', this.contributors.findBy('id', data.contributorId));
|
||||||
|
const loadedContribution = this.contributions.findBy('id', contribution.id);
|
||||||
|
if (loadedContribution) { this.contributions.removeObject(loadedContribution); }
|
||||||
|
this.contributions.pushObject(contribution);
|
||||||
|
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);
|
||||||
@@ -251,23 +442,185 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getCurrentUser: computed('kredits.provider', 'currentUserAccounts.[]', function() {
|
//
|
||||||
if (isEmpty(this.currentUserAccounts)) {
|
// Generic data handling (for objects that can be vetoed)
|
||||||
return RSVP.resolve();
|
//
|
||||||
}
|
|
||||||
return this.kredits.Contributor
|
fetchObjects(objectClass, options = { page: { size: 200 } }) {
|
||||||
.functions.getContributorIdByAddress(this.currentUserAccounts.firstObject)
|
const objectClassLowerCase = objectClass.toLowerCase();
|
||||||
.then((id) => {
|
console.debug(`[kredits] Fetching ${objectClassLowerCase}s from the network`);
|
||||||
// check if the user is a contributor or not
|
return this.kredits[objectClass].all(options)
|
||||||
if (id === 0) {
|
.then(objects => {
|
||||||
return RSVP.resolve();
|
return objects.map(data => {
|
||||||
} else {
|
const classInstance = this[`load${objectClass}FromData`](data);
|
||||||
return this.kredits.Contributor.getById(id);
|
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'),
|
||||||
|
|
||||||
|
//
|
||||||
// Contract events
|
// Contract events
|
||||||
|
//
|
||||||
|
|
||||||
addContractEventHandlers () {
|
addContractEventHandlers () {
|
||||||
this.kredits.Contributor
|
this.kredits.Contributor
|
||||||
.on('ContributorProfileUpdated', this.handleContributorChange.bind(this))
|
.on('ContributorProfileUpdated', this.handleContributorChange.bind(this))
|
||||||
@@ -278,6 +631,10 @@ 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.Reimbursement
|
||||||
|
.on('ReimbursementAdded', this.handleReimbursementAdded.bind(this))
|
||||||
|
.on('ReimbursementVetoed', this.handleReimbursementVetoed.bind(this))
|
||||||
|
|
||||||
this.kredits.Token
|
this.kredits.Token
|
||||||
.on('Transfer', this.handleTransfer.bind(this));
|
.on('Transfer', this.handleTransfer.bind(this));
|
||||||
},
|
},
|
||||||
@@ -288,13 +645,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);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -303,27 +662,61 @@ 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());
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleContributionVetoed (contributionId) {
|
||||||
|
console.debug('[kredits] ContributionVetoed event received for ', contributionId);
|
||||||
|
const c = this.contributions.findBy('id', contributionId);
|
||||||
|
|
||||||
|
if (c) {
|
||||||
|
console.debug('[kredits] Updating contribution', c);
|
||||||
|
c.set('vetoed', true);
|
||||||
|
c.set('pendingTx', null);
|
||||||
|
await this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleContributionVetoed (contributionId) {
|
//
|
||||||
console.debug('[kredits] ContributionVetoed event received for ', contributionId);
|
// TODO test when reimbursement txs are successful
|
||||||
const contribution = this.contributions.findBy('id', contributionId);
|
//
|
||||||
console.debug('[kredits] contribution', contribution);
|
async handleReimbursementAdded (id, addedByAccount, amount) {
|
||||||
|
console.debug('[kredits] ReimbursementAdded event received', { id, addedByAccount, amount });
|
||||||
|
|
||||||
if (contribution) {
|
const pendingReimbursement = this.reimbursementsPending.find(r => {
|
||||||
contribution.set('vetoed', true);
|
return r.amount.toString() === amount.toString();
|
||||||
contribution.set('pendingTx', null);
|
});
|
||||||
|
|
||||||
|
if (pendingReimbursement) {
|
||||||
|
console.debug('[kredits] Found a pending reimbursement matching the event. Replacing it with the final record...');
|
||||||
|
this.reimbursements.removeObject(pendingReimbursement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await this.kredits.Reimbursement.getById(id);
|
||||||
|
this.loadReimbursementFromData(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// TODO test when reimbursement txs are successful and veto is implemented
|
||||||
|
//
|
||||||
|
handleReimbursementVetoed (id) {
|
||||||
|
console.debug('[kredits] ReimbursementVetoed received for ', id);
|
||||||
|
const reimbursement = this.reimbursements.findBy('id', id);
|
||||||
|
console.debug('[kredits] reimbursement', this.reimbursement);
|
||||||
|
|
||||||
|
if (reimbursement) {
|
||||||
|
reimbursement.set('vetoed', true);
|
||||||
|
reimbursement.set('pendingTx', null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -20,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,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);
|
||||||
|
|||||||
+46
-6
@@ -1,21 +1,31 @@
|
|||||||
section#add-contributor,
|
section#add-contributor,
|
||||||
section#add-contribution,
|
section#add-contribution,
|
||||||
|
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 {
|
||||||
@@ -29,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;
|
||||||
@@ -39,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 {
|
||||||
@@ -109,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-1
@@ -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%}
|
||||||
|
}
|
||||||
+7
-2
@@ -47,7 +47,7 @@ a {
|
|||||||
|
|
||||||
section {
|
section {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.5rem;
|
||||||
color: $primary-color;
|
color: $primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +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/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 {
|
||||||
|
|||||||
@@ -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,5 +100,4 @@ ul.contribution-list {
|
|||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
ul.reimbursement-list {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 12rem;
|
||||||
|
grid-row-gap: 0.5rem;
|
||||||
|
padding-top: 1.6rem;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}}
|
||||||
@@ -6,8 +6,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<AddContribution @contributors={{this.sortedContributors}}
|
<AddContribution @attributes={{this.model.params}}
|
||||||
@attributes={{this.model.params}}
|
|
||||||
@save={{action "save"}} />
|
@save={{action "save"}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<ContributorList @contributorList={{this.kreditsToplist}}
|
<ContributorList @contributorList={{this.kreditsToplist}}
|
||||||
@showUnconfirmedKredits={{this.showUnconfirmedKredits}}
|
@showUnconfirmedKredits={{this.showUnconfirmedKredits}}
|
||||||
@selectedContributorId={{this.selectedContributorId}} />
|
@selectedContributorId={{this.selectedContributorId}}
|
||||||
|
@loading={{this.kredits.syncContributors.isRunning}} />
|
||||||
|
|
||||||
<p class="stats">
|
<p class="stats">
|
||||||
<span class="number">{{await this.kredits.totalKreditsEarned}}</span> kredits confirmed and issued to
|
<span class="number">{{await this.kredits.totalKreditsEarned}}</span> kredits confirmed and issued to
|
||||||
@@ -58,7 +59,9 @@
|
|||||||
{{#if this.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 type="button"
|
<button type="button"
|
||||||
onclick={{action "toggleQuickFilterUnconfirmed"}}
|
onclick={{action "toggleQuickFilterUnconfirmed"}}
|
||||||
@@ -78,7 +81,8 @@
|
|||||||
@vetoContribution={{action "vetoContribution"}}
|
@vetoContribution={{action "vetoContribution"}}
|
||||||
@contractInteractionEnabled={{this.kredits.hasAccounts}}
|
@contractInteractionEnabled={{this.kredits.hasAccounts}}
|
||||||
@selectedContributionId={{this.selectedContributionId}}
|
@selectedContributionId={{this.selectedContributionId}}
|
||||||
@showQuickFilter={{this.showQuickFilterUnconfirmed}} />
|
@showQuickFilter={{this.showQuickFilterUnconfirmed}}
|
||||||
|
@loading={{this.kredits.syncContributions.isRunning}}/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<h3>{{this.model.description}}</h3>
|
<h3>{{this.model.description}}</h3>
|
||||||
<p>
|
<p>
|
||||||
Kind: {{this.model.kind}}
|
Kind: {{this.model.kind}}
|
||||||
<br>Status: {{contribution-status this.model}}
|
<br>Status: {{item-status this.model}}
|
||||||
</p>
|
</p>
|
||||||
{{#if this.model.url}}
|
{{#if this.model.url}}
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { computed } from '@ember/object';
|
|
||||||
import ethers from 'ethers';
|
|
||||||
|
|
||||||
export default function(dependentKey, converterMethod) {
|
|
||||||
return computed(dependentKey, {
|
|
||||||
get () {
|
|
||||||
let value = this.get(dependentKey);
|
|
||||||
if (value && ethers.utils.BigNumber.isBigNumber(value)) {
|
|
||||||
return value[converterMethod]();
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
set (key, value) {
|
|
||||||
const bnValue = ethers.utils.bigNumberify(value);
|
|
||||||
this.set(dependentKey, bnValue);
|
|
||||||
return bnValue[converterMethod]();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { computed } from '@ember/object';
|
|
||||||
import ethers from 'ethers';
|
|
||||||
import formatKredits from 'kredits-web/utils/format-kredits';
|
|
||||||
|
|
||||||
export default function(dependentKey, options = {}) {
|
|
||||||
return computed(dependentKey, {
|
|
||||||
get () {
|
|
||||||
const value = this.get(dependentKey);
|
|
||||||
return formatKredits(value, options);
|
|
||||||
},
|
|
||||||
set (key, value) {
|
|
||||||
const bnValue = ethers.utils.bigNumberify(value);
|
|
||||||
this.set(dependentKey, bnValue);
|
|
||||||
return formatKredits(bnValue, options);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
// Code from https://github.com/HeroicEric/ember-group-by (MIT licensed)
|
// Code from https://github.com/HeroicEric/ember-group-by (MIT licensed)
|
||||||
//
|
//
|
||||||
import { A } from '@ember/array';
|
import { A } from '@ember/array';
|
||||||
import { get } from '@ember/object';
|
|
||||||
import { isPresent } from '@ember/utils';
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
export default function groupBy (collection, property) {
|
export default function groupBy (collection, property) {
|
||||||
@@ -11,11 +10,11 @@ export default function groupBy (collection, property) {
|
|||||||
|
|
||||||
if (items) {
|
if (items) {
|
||||||
items.forEach(function(item) {
|
items.forEach(function(item) {
|
||||||
let value = get(item, property);
|
let value = item[property];
|
||||||
let group = groups.findBy('value', value);
|
let group = groups.findBy('value', value);
|
||||||
|
|
||||||
if (isPresent(group)) {
|
if (isPresent(group)) {
|
||||||
get(group, 'items').push(item);
|
group.items.push(item);
|
||||||
} else {
|
} else {
|
||||||
group = { property: property, value: value, items: [item] };
|
group = { property: property, value: value, items: [item] };
|
||||||
groups.push(group);
|
groups.push(group);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default function isValidAmount(inputAmount) {
|
||||||
|
const amount = parseFloat(inputAmount);
|
||||||
|
if (Number.isNaN(amount)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return amount > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export default function processContributionData(data, options={}) {
|
||||||
|
const processed = {}
|
||||||
|
|
||||||
|
if (data.confirmedAtBlock && (typeof data.confirmedAtBlock.toNumber === 'function')) {
|
||||||
|
processed.confirmedAt = data.confirmedAtBlock.toNumber();
|
||||||
|
} else if (data.confirmedAt !== 'undefined') {
|
||||||
|
processed.confirmedAt = data.confirmedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherProperties = [
|
||||||
|
'id', 'contributorId', 'amount', 'vetoed', 'ipfsHash', 'kind',
|
||||||
|
'description', 'url', 'date', 'time', 'pendingTx'
|
||||||
|
];
|
||||||
|
|
||||||
|
otherProperties.forEach(prop => {
|
||||||
|
processed[prop] = data[prop];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.includeDetails) {
|
||||||
|
processed.details = data.details;
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default function processContributorData(data) {
|
||||||
|
const processed = {
|
||||||
|
balance: data.balanceInt,
|
||||||
|
totalKreditsEarned: data.totalKreditsEarned,
|
||||||
|
contributionsCount: data.contributionsCount?.toNumber()
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherProperties = [
|
||||||
|
'id', 'account', 'accounts', 'ipfsHash', 'isCore', 'kind', 'name', 'url',
|
||||||
|
'github_username', 'github_uid', 'wiki_username', 'zoom_display_name'
|
||||||
|
];
|
||||||
|
|
||||||
|
otherProperties.forEach(prop => {
|
||||||
|
processed[prop] = data[prop];
|
||||||
|
});
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export default function processReimbursementData(data) {
|
||||||
|
const processed = {
|
||||||
|
amount: data.amount.toNumber()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.confirmedAtBlock && (typeof data.confirmedAtBlock.toNumber === 'function')) {
|
||||||
|
processed.confirmedAt = data.confirmedAtBlock.toNumber();
|
||||||
|
} else if (data.confirmedAt !== 'undefined') {
|
||||||
|
processed.confirmedAt = data.confirmedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherProperties = [
|
||||||
|
'id', 'recipientId', 'token', 'vetoed', 'ipfsHash',
|
||||||
|
'expenses', 'pendingTx'
|
||||||
|
];
|
||||||
|
|
||||||
|
otherProperties.forEach(prop => {
|
||||||
|
processed[prop] = data[prop];
|
||||||
|
});
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
+15
-21
@@ -25,18 +25,9 @@ module.exports = function(environment) {
|
|||||||
// when it is created
|
// when it is created
|
||||||
},
|
},
|
||||||
|
|
||||||
browserify: {
|
web3ProviderUrl: 'https://rsk-testnet.kosmos.org',
|
||||||
tests: true,
|
web3RequiredChainId: 31,
|
||||||
transform: [
|
web3RequiredNetworkName: 'RSK Testnet',
|
||||||
["babelify", {
|
|
||||||
presets: ["es2015"],
|
|
||||||
global: true
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
web3ProviderUrl: 'https://rinkeby.infura.io/v3/d4f788b7a6584f7db2fc3c268d4d09e9',
|
|
||||||
web3RequiredNetwork: 'rinkeby',
|
|
||||||
|
|
||||||
githubConnectUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/connect/github',
|
githubConnectUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/connect/github',
|
||||||
githubSignupUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/github',
|
githubSignupUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/github',
|
||||||
@@ -46,7 +37,17 @@ module.exports = function(environment) {
|
|||||||
port: '5444',
|
port: '5444',
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
gatewayUrl: 'https://ipfs.kosmos.org/ipfs'
|
gatewayUrl: 'https://ipfs.kosmos.org/ipfs'
|
||||||
}
|
},
|
||||||
|
|
||||||
|
tokens: {
|
||||||
|
// TODO this is still the WBTC address, since contracts currently
|
||||||
|
// requires a token address for reimbursements
|
||||||
|
'BTC': '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'
|
||||||
|
},
|
||||||
|
|
||||||
|
btcBalanceAPI: 'https://api.kosmos.org/kredits/onchain_btc_balance',
|
||||||
|
|
||||||
|
corsProxy: 'https://cors.5apps.com/?uri='
|
||||||
};
|
};
|
||||||
|
|
||||||
if (environment === 'development') {
|
if (environment === 'development') {
|
||||||
@@ -55,7 +56,6 @@ module.exports = function(environment) {
|
|||||||
// ENV.APP.LOG_TRANSITIONS = true;
|
// ENV.APP.LOG_TRANSITIONS = true;
|
||||||
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
|
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
|
||||||
// ENV.APP.LOG_VIEW_LOOKUPS = true;
|
// ENV.APP.LOG_VIEW_LOOKUPS = true;
|
||||||
ENV.web3ProviderUrl = 'https://rinkeby.infura.io/v3/d4f788b7a6584f7db2fc3c268d4d09e9';
|
|
||||||
|
|
||||||
ENV.githubConnectUrl = 'http://localhost:8888/kredits/signup/connect/github';
|
ENV.githubConnectUrl = 'http://localhost:8888/kredits/signup/connect/github';
|
||||||
ENV.githubSignupUrl = 'http://localhost:8888/kredits/signup/github';
|
ENV.githubSignupUrl = 'http://localhost:8888/kredits/signup/github';
|
||||||
@@ -82,13 +82,7 @@ module.exports = function(environment) {
|
|||||||
|
|
||||||
if (process.env.WEB3_PROVIDER_URL) {
|
if (process.env.WEB3_PROVIDER_URL) {
|
||||||
ENV.web3ProviderUrl = process.env.WEB3_PROVIDER_URL;
|
ENV.web3ProviderUrl = process.env.WEB3_PROVIDER_URL;
|
||||||
ENV.web3RequiredNetwork = null;
|
ENV.web3RequiredChainId = null;
|
||||||
}
|
|
||||||
if (process.env.KREDITS_DAO_ADDRESS) {
|
|
||||||
ENV.kreditsKernelAddress = process.env.KREDITS_DAO_ADDRESS;
|
|
||||||
}
|
|
||||||
if (process.env.KREDITS_APM_DOMAIN) {
|
|
||||||
ENV.kreditsApmDomain = process.env.KREDITS_APM_DOMAIN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ENV;
|
return ENV;
|
||||||
|
|||||||
+6
-6
@@ -17,12 +17,12 @@ module.exports = function(defaults) {
|
|||||||
webpack: {
|
webpack: {
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
// {
|
||||||
// inline json files instead of using fs.readFileSync
|
// // inline json files instead of using fs.readFileSync
|
||||||
enforce: 'post',
|
// enforce: 'post',
|
||||||
test: /kosmos-schemas\/index\.js$/,
|
// test: /kosmos-schemas\/index\.js$/,
|
||||||
loader: 'transform-loader?brfs'
|
// loader: 'transform-loader?brfs'
|
||||||
}
|
// }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
node: {
|
node: {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+32569
-14949
File diff suppressed because it is too large
Load Diff
+22
-20
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kredits-web",
|
"name": "kredits-web",
|
||||||
"version": "1.16.0",
|
"version": "2.0.0-beta.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Contribution dashboard of the Kosmos project",
|
"description": "Contribution dashboard of the Kosmos project",
|
||||||
"repository": "https://github.com/67P/kredits-web",
|
"repository": "https://github.com/67P/kredits-web",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"start": "ember serve",
|
"start": "ember serve",
|
||||||
"test": "npm-run-all lint:* test:*",
|
"test": "npm-run-all lint:* test:*",
|
||||||
"test:ember": "ember test",
|
"test:ember": "ember test",
|
||||||
"start:local": "WEB3_PROVIDER_URL=http://localhost:7545 ember serve",
|
"start:local": "WEB3_PROVIDER_URL=http://localhost:8545 ember serve",
|
||||||
"build": "ember build --environment=production",
|
"build": "ember build --environment=production",
|
||||||
"build-prod": "rm -rf release/* && ember build -prod --output-path release",
|
"build-prod": "rm -rf release/* && ember build -prod --output-path release",
|
||||||
"preversion": "npm test",
|
"preversion": "npm test",
|
||||||
@@ -26,57 +26,59 @@
|
|||||||
"deploy": "git push 5apps master"
|
"deploy": "git push 5apps master"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
|
"@babel/plugin-proposal-decorators": "^7.19.3",
|
||||||
"@ember/optional-features": "^1.3.0",
|
"@ember/optional-features": "^1.3.0",
|
||||||
"@glimmer/component": "^1.0.0",
|
"@glimmer/component": "^1.0.0",
|
||||||
"@glimmer/tracking": "^1.0.0",
|
"@glimmer/tracking": "^1.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"@kredits/contracts": "^7.0.1",
|
||||||
"babel-preset-es2015": "^6.22.0",
|
"babel-preset-es2015": "^6.22.0",
|
||||||
"babelify": "^7.3.0",
|
|
||||||
"brfs": "^2.0.2",
|
|
||||||
"broccoli-asset-rev": "^3.0.0",
|
"broccoli-asset-rev": "^3.0.0",
|
||||||
"ember-auto-import": "^1.5.3",
|
"ember-auto-import": "^1.12.1",
|
||||||
"ember-awesome-macros": "0.41.0",
|
"ember-awesome-macros": "0.41.0",
|
||||||
"ember-cli": "~3.18.0",
|
"ember-cli": "~3.18.0",
|
||||||
"ember-cli-app-version": "^3.2.0",
|
"ember-cli-app-version": "^3.2.0",
|
||||||
"ember-cli-babel": "^7.19.0",
|
"ember-cli-babel": "^7.20.4",
|
||||||
"ember-cli-chart": "^3.6.0",
|
"ember-cli-chart": "^3.6.0",
|
||||||
"ember-cli-dependency-checker": "^3.2.0",
|
"ember-cli-dependency-checker": "^3.2.0",
|
||||||
"ember-cli-htmlbars": "^4.3.1",
|
"ember-cli-htmlbars": "^5.1.2",
|
||||||
"ember-cli-inject-live-reload": "^2.0.2",
|
"ember-cli-inject-live-reload": "^2.0.2",
|
||||||
"ember-cli-moment-shim": "^3.7.1",
|
"ember-cli-moment-shim": "^3.8.0",
|
||||||
"ember-cli-sass": "^10.0.1",
|
"ember-cli-sass": "^10.0.1",
|
||||||
"ember-cli-sri": "^2.1.1",
|
"ember-cli-sri": "^2.1.1",
|
||||||
"ember-cli-uglify": "^3.0.0",
|
"ember-cli-uglify": "^3.0.0",
|
||||||
|
"ember-concurrency": "^1.1.7",
|
||||||
|
"ember-concurrency-decorators": "^2.0.1",
|
||||||
"ember-export-application-global": "^2.0.1",
|
"ember-export-application-global": "^2.0.1",
|
||||||
"ember-fetch": "^8.0.1",
|
"ember-fetch": "^8.0.1",
|
||||||
"ember-flatpickr": "^2.16.2",
|
"ember-flatpickr": "^2.16.2",
|
||||||
"ember-load-initializers": "^2.1.1",
|
"ember-load-initializers": "^2.1.1",
|
||||||
"ember-macro-helpers": "0.17.0",
|
"ember-macro-helpers": "0.17.0",
|
||||||
"ember-maybe-import-regenerator": "^0.1.6",
|
"ember-maybe-import-regenerator": "^0.1.6",
|
||||||
"ember-moment": "^7.8.1",
|
"ember-moment": "^8.0.0",
|
||||||
"ember-promise-helpers": "^1.0.9",
|
"ember-promise-helpers": "^1.0.9",
|
||||||
"ember-qunit": "^4.6.0",
|
"ember-qunit": "^4.6.0",
|
||||||
"ember-resolver": "^8.0.0",
|
"ember-resolver": "^8.0.0",
|
||||||
"ember-source": "~3.18.0",
|
"ember-source": "~3.18.0",
|
||||||
"ember-template-lint": "^2.6.0",
|
"ember-template-lint": "^2.8.0",
|
||||||
"ember-truth-helpers": "github:jmurphyau/ember-truth-helpers#31a14373a31f1f82c77537720549b47a95c28e5f",
|
"ember-truth-helpers": "^3.0.0",
|
||||||
"eslint": "^7.0.0",
|
"eslint": "^8.14.0",
|
||||||
"eslint-plugin-ember": "^8.4.0",
|
"eslint-plugin-ember": "^11.0.6",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"ethers": "^4.0.47",
|
"ethers": "^5.4.7",
|
||||||
"kosmos-schemas": "^2.1.0",
|
"fetch-mock": "^9.10.7",
|
||||||
"kredits-contracts": "^5.5.0",
|
|
||||||
"loader.js": "^4.7.0",
|
"loader.js": "^4.7.0",
|
||||||
|
"localforage": "^1.7.3",
|
||||||
"ndjson": "github:hugomrdias/ndjson#feat/readable-stream3",
|
"ndjson": "github:hugomrdias/ndjson#feat/readable-stream3",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"qunit-dom": "^1.2.0",
|
"qunit-dom": "^1.2.0",
|
||||||
"sass": "^1.26.5",
|
"sass": "^1.26.7",
|
||||||
"transform-loader": "^0.2.4",
|
"transform-loader": "^0.2.4",
|
||||||
"tv4": "^1.3.0",
|
"tv4": "^1.3.0",
|
||||||
"web3-utils": "^1.2.7"
|
"web3-utils": "^1.7.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "10.* || >= 12"
|
"node": ">= 14"
|
||||||
},
|
},
|
||||||
"ember": {
|
"ember": {
|
||||||
"edition": "octane"
|
"edition": "octane"
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<g transform="translate(0.00630876,-0.00301984)">
|
||||||
|
<path fill="#f7931a" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
|
||||||
|
<path fill="#FFF" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":[],"names":[],"mappings":"","file":"auto-import-fastboot.js"}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
//# sourceMappingURL=auto-import-fastboot-81f919f3d82c38703c7b0de8c4c4a874.map
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
|||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<g transform="translate(0.00630876,-0.00301984)">
|
||||||
|
<path fill="#f7931a" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
|
||||||
|
<path fill="#FFF" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
+4
-4
@@ -8,10 +8,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||||
|
|
||||||
|
|
||||||
<meta name="kredits-web/config/environment" content="%7B%22modulePrefix%22%3A%22kredits-web%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22auto%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_DEFAULT_ASYNC_OBSERVERS%22%3Atrue%2C%22_JQUERY_INTEGRATION%22%3Afalse%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22name%22%3A%22kredits-web%22%2C%22version%22%3A%221.16.0%2B41cc37d3%22%7D%2C%22browserify%22%3A%7B%22tests%22%3Atrue%2C%22transform%22%3A%5B%5B%22babelify%22%2C%7B%22presets%22%3A%5B%22es2015%22%5D%2C%22global%22%3Atrue%7D%5D%5D%7D%2C%22web3ProviderUrl%22%3A%22https%3A%2F%2Frinkeby.infura.io%2Fv3%2Fd4f788b7a6584f7db2fc3c268d4d09e9%22%2C%22web3RequiredNetwork%22%3A%22rinkeby%22%2C%22githubConnectUrl%22%3A%22https%3A%2F%2Fhal8000.chat.kosmos.org%2Fkredits%2Fsignup%2Fconnect%2Fgithub%22%2C%22githubSignupUrl%22%3A%22https%3A%2F%2Fhal8000.chat.kosmos.org%2Fkredits%2Fsignup%2Fgithub%22%2C%22ipfs%22%3A%7B%22host%22%3A%22ipfs.kosmos.org%22%2C%22port%22%3A%225444%22%2C%22protocol%22%3A%22https%22%2C%22gatewayUrl%22%3A%22https%3A%2F%2Fipfs.kosmos.org%2Fipfs%22%7D%2C%22exportApplicationGlobal%22%3Afalse%7D" />
|
<meta name="kredits-web/config/environment" content="%7B%22modulePrefix%22%3A%22kredits-web%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22auto%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_DEFAULT_ASYNC_OBSERVERS%22%3Atrue%2C%22_JQUERY_INTEGRATION%22%3Afalse%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22name%22%3A%22kredits-web%22%2C%22version%22%3A%222.0.0-0%2B7c8ca45b%22%7D%2C%22web3ProviderUrl%22%3A%22https%3A%2F%2Frsk-testnet.kosmos.org%22%2C%22web3RequiredChainId%22%3A31%2C%22web3RequiredNetworkName%22%3A%22RSK%20Testnet%22%2C%22githubConnectUrl%22%3A%22https%3A%2F%2Fhal8000.chat.kosmos.org%2Fkredits%2Fsignup%2Fconnect%2Fgithub%22%2C%22githubSignupUrl%22%3A%22https%3A%2F%2Fhal8000.chat.kosmos.org%2Fkredits%2Fsignup%2Fgithub%22%2C%22ipfs%22%3A%7B%22host%22%3A%22ipfs.kosmos.org%22%2C%22port%22%3A%225444%22%2C%22protocol%22%3A%22https%22%2C%22gatewayUrl%22%3A%22https%3A%2F%2Fipfs.kosmos.org%2Fipfs%22%7D%2C%22tokens%22%3A%7B%22BTC%22%3A%220x2260fac5e5542a773aa44fbcfedf7c193bc2c599%22%7D%2C%22btcBalanceAPI%22%3A%22https%3A%2F%2Fapi.kosmos.org%2Fkredits%2Fonchain_btc_balance%22%2C%22corsProxy%22%3A%22https%3A%2F%2Fcors.5apps.com%2F%3Furi%3D%22%2C%22exportApplicationGlobal%22%3Afalse%7D" />
|
||||||
|
|
||||||
<link integrity="" rel="stylesheet" href="/assets/vendor-ca96b2e19fc9f356e3dbad90c9f3f323.css">
|
<link integrity="" rel="stylesheet" href="/assets/vendor-ca96b2e19fc9f356e3dbad90c9f3f323.css">
|
||||||
<link integrity="" rel="stylesheet" href="/assets/kredits-web-8440190dda6de1748075524fe140b4ee.css">
|
<link integrity="" rel="stylesheet" href="/assets/kredits-web-bdbc10028be8d48749a246964068e561.css">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
||||||
<script src="/assets/vendor-5ebca0a713676bf484d729ec58e9d74b.js" integrity="sha256-K9BPrQzg0Nv83HSPUwk2n1hU8IOaceDf03EaU5LgblQ= sha512-stOJYOdJfiuzleqChwK9pNLCVMQwg9sZXUSboTsvtIixnRoQ285yS080rRX/GfcRd/eI0TEpPI0XKqvN56kzLA==" ></script>
|
<script src="/assets/vendor-4ee536ded971436a83d35253df25f2cb.js" integrity="sha256-yb+pC9iuZ8WcQwk9VuDNhSlWguL7BEABsEOEhBpwUC0= sha512-50LApx5XqeKHu/FMaljzqhAMPgkqSiy2rDhUcViYBcXWBBYmilFirup/iWvLiNCdfr5ct9wk7+tU9iShdSgDww==" ></script>
|
||||||
<script src="/assets/kredits-web-d376614e43d3edec456b90f7efdfedf4.js" integrity="sha256-ZUj9gy+521ioE0n+gKl53UrRqVALwY7YTOonD2SjZfg= sha512-0G4MUqxkZ+yazboR03+jfA9BRKoyKh2/IgG9iAVttTfVZRyMTHqiFnGlG00KY72H0y2jVamQtgY6UpLgAohGLg==" ></script>
|
<script src="/assets/kredits-web-597bd2c84f295cea39c51678944cb39f.js" integrity="sha256-Ctb96mli0/Rkuej7AJB6wLegTJElHqGYpBOiKkB3spw= sha512-pwHfdUGsokaPO3vAZV4qYG+ljL+5ceExlHARAoyTsd15+yCDCbKsmJzuwLXnagNn/O1IzkDlEN0wed5l3FbuhQ==" ></script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Vendored
+391
File diff suppressed because one or more lines are too long
Vendored
+13
-11
@@ -1,23 +1,25 @@
|
|||||||
import Model from 'kredits-web/models/contribution';
|
import Model from 'kredits-web/models/contribution';
|
||||||
import contributors from '../../tests/fixtures/contributors';
|
import contributors from '../../tests/fixtures/contributors';
|
||||||
|
import processContributionData from 'kredits-web/utils/process-contribution-data';
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
{ id: 1, contributorId: 1, confirmedAtBlock: 1000, claimed: false, vetoed: false, amount: 1500, kind: 'dev' },
|
{ id: 1, contributorId: 1, confirmedAt: 1000, claimed: false, vetoed: false, amount: 1500, kind: 'dev' },
|
||||||
{ id: 2, contributorId: 1, confirmedAtBlock: 1000, claimed: false, vetoed: false, amount: 5000, kind: 'ops' },
|
{ id: 2, contributorId: 1, confirmedAt: 1000, claimed: false, vetoed: false, amount: 5000, kind: 'ops' },
|
||||||
{ id: 3, contributorId: 2, confirmedAtBlock: 1000, claimed: false, vetoed: false, amount: 1500, kind: 'ops' },
|
{ id: 3, contributorId: 2, confirmedAt: 1000, claimed: false, vetoed: false, amount: 1500, kind: 'ops' },
|
||||||
{ id: 4, contributorId: 2, confirmedAtBlock: 1000, claimed: false, vetoed: false, amount: 1500, kind: 'docs' },
|
{ id: 4, contributorId: 2, confirmedAt: 1000, claimed: false, vetoed: false, amount: 1500, kind: 'docs' },
|
||||||
{ id: 5, contributorId: 1, confirmedAtBlock: 1000, claimed: false, vetoed: false, amount: 5000, kind: 'design' },
|
{ id: 5, contributorId: 1, confirmedAt: 1000, claimed: false, vetoed: false, amount: 5000, kind: 'design' },
|
||||||
{ id: 6, contributorId: 1, confirmedAtBlock: 1000, claimed: false, vetoed: true, amount: 500, kind: 'dev' },
|
{ id: 6, contributorId: 1, confirmedAt: 1000, claimed: false, vetoed: true, amount: 500, kind: 'dev' },
|
||||||
{ id: 7, contributorId: 3, confirmedAtBlock: 2000, claimed: false, vetoed: false, amount: 5000, kind: 'dev' },
|
{ id: 7, contributorId: 3, confirmedAt: 2000, claimed: false, vetoed: false, amount: 5000, kind: 'dev' },
|
||||||
{ id: 8, contributorId: 1, confirmedAtBlock: 2000, claimed: false, vetoed: false, amount: 1500, kind: 'community' },
|
{ id: 8, contributorId: 1, confirmedAt: 2000, claimed: false, vetoed: false, amount: 1500, kind: 'community' },
|
||||||
{ id: 9, contributorId: 3, confirmedAtBlock: 2000, claimed: false, vetoed: true, amount: 1500, kind: 'docs' },
|
{ id: 9, contributorId: 3, confirmedAt: 2000, claimed: false, vetoed: true, amount: 1500, kind: 'docs' },
|
||||||
];
|
];
|
||||||
|
|
||||||
data.forEach(attrs => {
|
data.forEach(attrs => {
|
||||||
attrs.contributor = contributors.findBy('id', attrs.contributorId.toString());
|
const c = Model.create(processContributionData(attrs));
|
||||||
items.push(Model.create(attrs))
|
c.set('contributor', contributors.findBy('id', attrs.contributorId));
|
||||||
|
items.push(c);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default items;
|
export default items;
|
||||||
|
|||||||
Vendored
+64
@@ -0,0 +1,64 @@
|
|||||||
|
export default {
|
||||||
|
"0": 1,
|
||||||
|
"1": "0x7E8f313C56F809188313aa274Fa67EE58c31515d",
|
||||||
|
"2": "0x99b8afd7b266e19990924a8be9099e81054b70c36b20937228a77a5cf75723b8",
|
||||||
|
"3": 18,
|
||||||
|
"4": 32,
|
||||||
|
"5": true,
|
||||||
|
"6": {
|
||||||
|
"_hex": "0x09979c0838e8fc880000"
|
||||||
|
},
|
||||||
|
"7": 53500,
|
||||||
|
"8": {
|
||||||
|
"_hex": "0x49"
|
||||||
|
},
|
||||||
|
"9": true,
|
||||||
|
"id": 1,
|
||||||
|
"account": "0x7E8f313C56F809188313aa274Fa67EE58c31515d",
|
||||||
|
"hashDigest": "0x99b8afd7b266e19990924a8be9099e81054b70c36b20937228a77a5cf75723b8",
|
||||||
|
"hashFunction": 18,
|
||||||
|
"hashSize": 32,
|
||||||
|
"isCore": true,
|
||||||
|
"balance": {
|
||||||
|
"_hex": "0x09979c0838e8fc880000"
|
||||||
|
},
|
||||||
|
"totalKreditsEarned": 53500,
|
||||||
|
"contributionsCount": {
|
||||||
|
"_hex": "0x49",
|
||||||
|
"toNumber": function() { return 73; }
|
||||||
|
},
|
||||||
|
"exists": true,
|
||||||
|
"balanceInt": 45298,
|
||||||
|
"ipfsHash": "QmYgiRd1FZ7JjDGBwkmLQNF6XQuyF8AWoFDRvUWhhmxiEj",
|
||||||
|
"name": "Bumi",
|
||||||
|
"kind": "person",
|
||||||
|
"url": "https://michaelbumann.com",
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"site": "github.com",
|
||||||
|
"uid": 318,
|
||||||
|
"username": "bumi",
|
||||||
|
"url": "https://github.com/bumi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"site": "gitea.kosmos.org",
|
||||||
|
"username": "bumi",
|
||||||
|
"url": "https://gitea.kosmos.org/bumi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"site": "wiki.kosmos.org",
|
||||||
|
"username": "Bumi",
|
||||||
|
"url": "https://wiki.kosmos.org/User:Bumi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"site": "zoom.us",
|
||||||
|
"username": "bumi"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"github_uid": 318,
|
||||||
|
"github_username": "bumi",
|
||||||
|
"gitea_username": "bumi",
|
||||||
|
"wiki_username": "Bumi",
|
||||||
|
"zoom_display_name": "bumi",
|
||||||
|
"ipfsData": "{\n \"@context\": \"https://schema.kosmos.org\",\n \"@type\": \"Contributor\",\n \"kind\": \"person\",\n \"name\": \"Bumi\",\n \"accounts\": [\n {\n \"site\": \"github.com\",\n \"uid\": 318,\n \"username\": \"bumi\",\n \"url\": \"https://github.com/bumi\"\n },\n {\n \"site\": \"gitea.kosmos.org\",\n \"username\": \"bumi\",\n \"url\": \"https://gitea.kosmos.org/bumi\"\n },\n {\n \"site\": \"wiki.kosmos.org\",\n \"username\": \"Bumi\",\n \"url\": \"https://wiki.kosmos.org/User:Bumi\"\n },\n {\n \"site\": \"zoom.us\",\n \"username\": \"bumi\"\n }\n ],\n \"url\": \"https://michaelbumann.com\"\n}"
|
||||||
|
}
|
||||||
Vendored
+5
-1
@@ -1,4 +1,5 @@
|
|||||||
import Contributor from 'kredits-web/models/contributor';
|
import Contributor from 'kredits-web/models/contributor';
|
||||||
|
import processContributorData from 'kredits-web/utils/process-contributor-data';
|
||||||
|
|
||||||
const contributors = [];
|
const contributors = [];
|
||||||
|
|
||||||
@@ -8,6 +9,9 @@ const data = [
|
|||||||
{ id: 3, name: 'Manuel', totalKreditsEarned: 0, github_uid: 54812 }
|
{ id: 3, name: 'Manuel', totalKreditsEarned: 0, github_uid: 54812 }
|
||||||
];
|
];
|
||||||
|
|
||||||
data.forEach(attrs => contributors.push(Contributor.create(attrs)));
|
data.forEach(attrs => {
|
||||||
|
const c = Contributor.create(processContributorData(attrs));
|
||||||
|
contributors.push(c);
|
||||||
|
});
|
||||||
|
|
||||||
export default contributors;
|
export default contributors;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { getContext } from '@ember/test-helpers';
|
||||||
|
|
||||||
|
export default function createComponent(lookupPath, named = {}) {
|
||||||
|
let { owner } = getContext();
|
||||||
|
let componentManager = owner.lookup('component-manager:glimmer');
|
||||||
|
let { class: componentClass } = owner.factoryFor(lookupPath);
|
||||||
|
return componentManager.createComponent(componentClass, { named });
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// import { module, test } from 'qunit';
|
||||||
|
// import { setupRenderingTest } from 'ember-qunit';
|
||||||
|
// import { render } from '@ember/test-helpers';
|
||||||
|
// import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
//
|
||||||
|
// module('Integration | Component | add-expense-item', function(hooks) {
|
||||||
|
// setupRenderingTest(hooks);
|
||||||
|
//
|
||||||
|
// test('it renders', async function(assert) {
|
||||||
|
// Set any properties with this.set('myProperty', 'value');
|
||||||
|
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||||
|
// await render(hbs`<AddExpenseItem />`);
|
||||||
|
// assert.equal(this.element.textContent.trim(), '');
|
||||||
|
// });
|
||||||
|
// });
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
|
import { render } from '@ember/test-helpers';
|
||||||
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
|
||||||
|
module('Integration | Component | add-reimbursement', function(hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
test('it works', async function(assert) {
|
||||||
|
await render(hbs`<AddReimbursement />`);
|
||||||
|
assert.ok(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// import { module, test } from 'qunit';
|
||||||
|
// import { setupRenderingTest } from 'ember-qunit';
|
||||||
|
// import { render } from '@ember/test-helpers';
|
||||||
|
// import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
//
|
||||||
|
// module('Integration | Component | budget-balances', function(hooks) {
|
||||||
|
// setupRenderingTest(hooks);
|
||||||
|
//
|
||||||
|
// test('it renders', async function(assert) {
|
||||||
|
// await render(hbs`<BudgetBalances />`);
|
||||||
|
// assert.equal(this.element.textContent.trim(), '');
|
||||||
|
// });
|
||||||
|
// });
|
||||||
@@ -16,8 +16,8 @@ module('Integration | Component | contribution-list', function(hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('it renders filtered contributions', async function(assert) {
|
test('it renders filtered contributions', async function(assert) {
|
||||||
let service = this.owner.lookup('service:kredits');
|
let kredits = this.owner.lookup('service:kredits');
|
||||||
service.set('contributors', contributors);
|
kredits.set('contributors', contributors);
|
||||||
|
|
||||||
this.set('fixtures', contributions);
|
this.set('fixtures', contributions);
|
||||||
await render(hbs`{{contribution-list contributions=fixtures showQuickFilter=true}}`);
|
await render(hbs`{{contribution-list contributions=fixtures showQuickFilter=true}}`);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
|
import { render } from '@ember/test-helpers';
|
||||||
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
|
||||||
|
module('Integration | Component | reimbursement-list', function(hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
test('it renders', async function(assert) {
|
||||||
|
// Set any properties with this.set('myProperty', 'value');
|
||||||
|
// Handle any actions with this.set('myAction', function(val) { ... });
|
||||||
|
|
||||||
|
await render(hbs`<ReimbursementList />`);
|
||||||
|
|
||||||
|
assert.equal(this.element.textContent.trim(), '');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,14 +8,14 @@ module('Integration | Component | user-avatar', function(hooks) {
|
|||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
test('it renders', async function(assert) {
|
test('it renders', async function(assert) {
|
||||||
this.set('bumi', contributors.findBy('id', '1'));
|
this.set('bumi', contributors.findBy('id', 1));
|
||||||
await render(hbs`{{user-avatar contributor=bumi}}`);
|
await render(hbs`{{user-avatar contributor=bumi}}`);
|
||||||
|
|
||||||
assert.dom(this.element).hasText('');
|
assert.dom(this.element).hasText('');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('default image source URL', async function(assert) {
|
test('default image source URL', async function(assert) {
|
||||||
this.set('bumi', contributors.findBy('id', '1'));
|
this.set('bumi', contributors.findBy('id', 1));
|
||||||
await render(hbs`{{user-avatar contributor=bumi}}`);
|
await render(hbs`{{user-avatar contributor=bumi}}`);
|
||||||
|
|
||||||
assert.equal(this.element.querySelector('img').src,
|
assert.equal(this.element.querySelector('img').src,
|
||||||
@@ -23,7 +23,7 @@ module('Integration | Component | user-avatar', function(hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('size-specific image source URLs', async function(assert) {
|
test('size-specific image source URLs', async function(assert) {
|
||||||
this.set('bumi', contributors.findBy('id', '1'));
|
this.set('bumi', contributors.findBy('id', 1));
|
||||||
this.set('size', 'medium');
|
this.set('size', 'medium');
|
||||||
|
|
||||||
await render(hbs`{{user-avatar contributor=bumi size=size}}`);
|
await render(hbs`{{user-avatar contributor=bumi size=size}}`);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user