Compare commits
88 Commits
v2.0.0-beta.2
...
v2.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
251e61891f
|
|||
| 66e1adc719 | |||
|
62388a0488
|
|||
| d7fe860353 | |||
|
14da36b806
|
|||
|
636c79ecc7
|
|||
|
d67bfd59da
|
|||
|
67538a9437
|
|||
|
473522986d
|
|||
|
0dd5f6c5ad
|
|||
|
731e01f2c5
|
|||
|
f02e5572ba
|
|||
|
aad2aaecd1
|
|||
|
7a61d1c569
|
|||
|
697ace35b5
|
|||
|
89ddde28b8
|
|||
|
50b0f2cd7a
|
|||
|
1b78cb4cb4
|
|||
| b32f805cbd | |||
|
3110fd347e
|
|||
|
5612ac4e95
|
|||
|
69ab4913aa
|
|||
|
39e01e4558
|
|||
|
7afc75dbff
|
|||
|
f5f74ae27f
|
|||
|
55f7baecf6
|
|||
|
bd199fcf26
|
|||
| 0777c213ba | |||
|
071922d979
|
|||
|
fc044df9fd
|
|||
|
dd2bdd5332
|
|||
|
0cd05d14bc
|
|||
|
3ee140723c
|
|||
|
49b3825af4
|
|||
|
af309ff682
|
|||
|
53f13e4a63
|
|||
|
bcdd033f34
|
|||
|
b9e7737589
|
|||
|
57b9bccb4a
|
|||
|
5c9fbf5043
|
|||
|
205d2d2afc
|
|||
| 895bf569ff | |||
|
dbedf1dbe8
|
|||
| 26f2f2afe9 | |||
|
6c456b1184
|
|||
|
03e7f14d1c
|
|||
|
e94e747ba1
|
|||
|
3679412b3c
|
|||
|
ea15e69d79
|
|||
|
8cc3da40b7
|
|||
|
bdeab512b6
|
|||
| 97ebebe28c | |||
|
d5edb93ec5
|
|||
|
0ecc4079c2
|
|||
|
3f080cf553
|
|||
|
0bdb4d2021
|
|||
|
1e7d8491f9
|
|||
|
4356288497
|
|||
|
87bf8ff2df
|
|||
|
26975636a0
|
|||
| dad56a7b31 | |||
|
d069fa02ab
|
|||
|
d219abc358
|
|||
|
13989bf973
|
|||
|
b3ab69d599
|
|||
|
fe3816501c
|
|||
| 854402aa73 | |||
|
abc19c715b
|
|||
|
b6434ac8d2
|
|||
|
4788b8c51c
|
|||
|
6a72a5b418
|
|||
|
d90ddd464a
|
|||
|
2626543f3e
|
|||
| 088db24534 | |||
| f7b7daa024 | |||
| bb11e2267b | |||
| 7cd023a21b | |||
|
1095bf0218
|
|||
|
138cec0389
|
|||
|
a93be41e08
|
|||
|
89f6fa0b5c
|
|||
|
e2a80eafd1
|
|||
|
662e76979b
|
|||
|
6c7de97e38
|
|||
|
fe27b010da
|
|||
|
ff716c68ea
|
|||
|
84dded2f10
|
|||
|
f86190030f
|
@@ -0,0 +1,30 @@
|
|||||||
|
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [16.x]
|
||||||
|
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
- run: npm install
|
||||||
|
- run: npm test
|
||||||
@@ -48,6 +48,12 @@ module.exports = {
|
|||||||
"no-action"
|
"no-action"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"moduleId": "app/components/expense-list/template",
|
||||||
|
"only": [
|
||||||
|
"no-invalid-role"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"moduleId": "app/components/topbar-account-panel/template",
|
"moduleId": "app/components/topbar-account-panel/template",
|
||||||
"only": [
|
"only": [
|
||||||
|
|||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "14"
|
|
||||||
- "16"
|
|
||||||
|
|
||||||
sudo: false
|
|
||||||
dist: precise
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
|
|
||||||
addons:
|
|
||||||
chrome: stable
|
|
||||||
|
|
||||||
cache:
|
|
||||||
npm: false
|
|
||||||
|
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
[](https://travis-ci.org/67P/kredits-web)
|
[](https://github.com/67P/kredits-web/actions/workflows/ci.yml?query=branch%3Amaster)
|
||||||
|
|
||||||
# kredits-web
|
# Kredits Web
|
||||||
|
|
||||||
This README outlines the details of collaborating on this Ember application.
|
This is the main Web UI for Kosmos [Kredits](https://wiki.kosmos.org/Kredits).
|
||||||
A short introduction of this app could easily go here.
|
It provides an overview of contributors and contributions, as well as the
|
||||||
|
community's budget, expenses, and reimbursements.
|
||||||
|
|
||||||
|
It is an unhosted Web app, fetching all data from
|
||||||
|
[Rootstock](https://rootstock.io/) and [IPFS](https://ipfs.tech/), and caching
|
||||||
|
it in the browser's local database. As such, you can run the app directly from
|
||||||
|
the `/release` directory on any Web server that can serve static assets.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -24,9 +30,9 @@ You will need the following things properly installed on your computer.
|
|||||||
|
|
||||||
### Building/running for development
|
### Building/running for development
|
||||||
|
|
||||||
* `ember serve` - by default kredits-web connects to the Ethreum Kovan network
|
* `npm start` - by default Kredits Web connects to the Rootstock testnet network
|
||||||
* Visit your app at [http://localhost:4200](http://localhost:4200).
|
* Visit the app at [http://localhost:4200](http://localhost:4200).
|
||||||
* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
|
* Visit the tests at [http://localhost:4200/tests](http://localhost:4200/tests).
|
||||||
|
|
||||||
See [working with locally deployed contracts](https://github.com/67P/kredits-web#working-with-locally-deployed-contracts) for details on how to develop with locally deployed contracts.
|
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.
|
||||||
|
|
||||||
@@ -48,7 +54,7 @@ Make use of the many generators for code, try `ember help generate` for more det
|
|||||||
### Building
|
### Building
|
||||||
|
|
||||||
* `ember build` (development)
|
* `ember build` (development)
|
||||||
* `ember build --environment production` (production)
|
* `npm run build` (production)
|
||||||
|
|
||||||
### Deploying
|
### Deploying
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<form onsubmit={{action "submit"}}>
|
<form onsubmit={{action "submit"}}>
|
||||||
<p>
|
<p>
|
||||||
<label for="c-account">Ethereum account</label>
|
<label for="c-account">Rootstock account</label>
|
||||||
<Input @type="text"
|
<Input @type="text"
|
||||||
@value={{this.account}}
|
@value={{this.account}}
|
||||||
name="account" id="c-account"
|
name="account" id="c-account"
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ export default class AddExpenseItemComponent extends Component {
|
|||||||
|
|
||||||
currencies = [
|
currencies = [
|
||||||
{ code: 'EUR' },
|
{ code: 'EUR' },
|
||||||
{ code: 'USD' }
|
{ code: 'USD' },
|
||||||
|
{ code: 'BTC' }
|
||||||
];
|
];
|
||||||
|
|
||||||
get isValidAmount () {
|
get isValidAmount () {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { inject as service } from '@ember/service';
|
|||||||
import { action } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { A } from '@ember/array';
|
import { A } from '@ember/array';
|
||||||
import { scheduleOnce } from '@ember/runloop';
|
import { scheduleOnce } from '@ember/runloop';
|
||||||
|
import { btcToSats, satsToBtc } from 'kredits-web/utils/btc-conversions';
|
||||||
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
import isValidAmount from 'kredits-web/utils/is-valid-amount';
|
||||||
|
import isoDateIsToday from 'kredits-web/utils/iso-date-is-today';
|
||||||
import readFileContent from 'kredits-web/utils/read-file-content';
|
import readFileContent from 'kredits-web/utils/read-file-content';
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
@@ -27,6 +29,10 @@ export default class AddReimbursementComponent extends Component {
|
|||||||
this.exchangeRates.fetchRates();
|
this.exchangeRates.fetchRates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get contributorId () {
|
||||||
|
return this.recipientId || this.kredits.currentUser?.id;
|
||||||
|
}
|
||||||
|
|
||||||
get isValidTotal () {
|
get isValidTotal () {
|
||||||
return isValidAmount(this.total);
|
return isValidAmount(this.total);
|
||||||
}
|
}
|
||||||
@@ -69,22 +75,6 @@ export default class AddReimbursementComponent extends Component {
|
|||||||
anchor.scrollIntoView();
|
anchor.scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTotalAmountFromFiat() {
|
|
||||||
let btcAmount = parseFloat(this.total);
|
|
||||||
|
|
||||||
if (this.exchangeRates.btceur > 0 && this.totalEUR > 0) {
|
|
||||||
btcAmount += (this.totalEUR / this.exchangeRates.btceur);
|
|
||||||
}
|
|
||||||
if (this.exchangeRates.btcusd > 0 && this.totalUSD > 0) {
|
|
||||||
btcAmount += (this.totalUSD / this.exchangeRates.btcusd);
|
|
||||||
}
|
|
||||||
if (this.totalUSD === 0 && this.totalEUR === 0) {
|
|
||||||
btcAmount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.total = btcAmount.toFixed(8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO use ember-concurrency here
|
// TODO use ember-concurrency here
|
||||||
// https://github.com/67P/kredits-web/pull/209#discussion_r1064234421
|
// https://github.com/67P/kredits-web/pull/209#discussion_r1064234421
|
||||||
@action
|
@action
|
||||||
@@ -104,7 +94,7 @@ export default class AddReimbursementComponent extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
updateContributor (event) {
|
updateContributor (event) {
|
||||||
this.recipientId = event.target.value;
|
this.recipientId = parseInt(event.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@@ -114,16 +104,38 @@ export default class AddReimbursementComponent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addExpenseItem (expenseItem) {
|
async addExpenseItem (expense) {
|
||||||
this.expenses.pushObject(expenseItem);
|
let totalBTC = parseFloat(this.total);
|
||||||
this.updateTotalAmountFromFiat();
|
|
||||||
|
if (expense.currency === "BTC") {
|
||||||
|
expense.amountSats = btcToSats(expense.amount);
|
||||||
|
totalBTC += expense.amount;
|
||||||
|
} else {
|
||||||
|
let amountSats;
|
||||||
|
if (isoDateIsToday(expense.date)) {
|
||||||
|
amountSats = btcToSats(expense.amount / this.exchangeRates[expense.currency]);
|
||||||
|
} else {
|
||||||
|
const rates = await this.exchangeRates.fetchHistoricRates(expense.date);
|
||||||
|
amountSats = btcToSats(expense.amount / rates[expense.currency]);
|
||||||
|
}
|
||||||
|
expense.amountSats = amountSats;
|
||||||
|
totalBTC += satsToBtc(amountSats);
|
||||||
|
}
|
||||||
|
console.debug("Adding expense:", expense);
|
||||||
|
|
||||||
|
this.total = totalBTC.toFixed(8);
|
||||||
|
this.expenses.pushObject(expense);
|
||||||
this.expenseFormVisible = false;
|
this.expenseFormVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
removeExpenseItem (expenseItem) {
|
async removeExpenseItem (expense) {
|
||||||
this.expenses.removeObject(expenseItem);
|
let totalBTC = parseFloat(this.total);
|
||||||
this.updateTotalAmountFromFiat();
|
let amountBTC = satsToBtc(expense.amountSats);
|
||||||
|
totalBTC = totalBTC - amountBTC;
|
||||||
|
this.total = totalBTC.toFixed(8);
|
||||||
|
|
||||||
|
this.expenses.removeObject(expense);
|
||||||
|
|
||||||
if (this.expenses.length === 0) {
|
if (this.expenses.length === 0) {
|
||||||
this.expenseFormVisible = true;
|
this.expenseFormVisible = true;
|
||||||
@@ -136,12 +148,12 @@ export default class AddReimbursementComponent extends Component {
|
|||||||
if (!this.kredits.currentUser) { window.alert('You need to connect your RSK account first.'); return false }
|
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 }
|
if (!this.kredits.currentUserIsCore) { window.alert('Only core contributors can submit reimbursements.'); return false }
|
||||||
|
|
||||||
const contributor = this.contributors.findBy('id', parseInt(this.recipientId));
|
const contributor = this.contributors.findBy('id', this.contributorId);
|
||||||
|
|
||||||
const attributes = {
|
const attributes = {
|
||||||
amount: parseInt(parseFloat(this.total) * 100000000), // convert to sats
|
amount: btcToSats(this.total),
|
||||||
token: config.tokens['BTC'],
|
token: config.tokens['BTC'],
|
||||||
recipientId: parseInt(this.recipientId),
|
recipientId: this.contributorId,
|
||||||
title: `Expenses covered by ${contributor.name}`,
|
title: `Expenses covered by ${contributor.name}`,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
<label>
|
<label>
|
||||||
<p class="label">Contributor:</p>
|
<p class="label">Contributor:</p>
|
||||||
<p>
|
<p>
|
||||||
<select required {{on "change" this.updateContributor}}>
|
<select id="contributor" required {{on "change" this.updateContributor}}>
|
||||||
<option value="" selected disabled hidden></option>
|
|
||||||
{{#each this.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}}
|
||||||
@@ -15,6 +14,7 @@
|
|||||||
<p class="label">Total amount (BTC):</p>
|
<p class="label">Total amount (BTC):</p>
|
||||||
<p>
|
<p>
|
||||||
<Input @type="text"
|
<Input @type="text"
|
||||||
|
@name="total-btc"
|
||||||
@placeholder="0.0015"
|
@placeholder="0.0015"
|
||||||
@value={{this.total}}
|
@value={{this.total}}
|
||||||
@required={{true}}
|
@required={{true}}
|
||||||
@@ -44,29 +44,15 @@
|
|||||||
|
|
||||||
<h3>Expense items</h3>
|
<h3>Expense items</h3>
|
||||||
{{#if this.expenses}}
|
{{#if this.expenses}}
|
||||||
<ul class="expense-list">
|
<ExpenseList @expenses={{this.expenses}}
|
||||||
{{#each this.expenses as |expense|}}
|
@removeExpenseItem={{this.removeExpenseItem}}
|
||||||
<li>
|
@deletable={{true}} />
|
||||||
<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">
|
<p class="actions">
|
||||||
<button {{on "click" this.showExpenseForm}}
|
<button {{on "click" this.showExpenseForm}}
|
||||||
class="green small" type="button">+ Add another item</button>
|
id="add-another-item" class="green small" type="button">
|
||||||
|
+ Add another item
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p>No line items yet.</p>
|
<p>No line items yet.</p>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { alias } from '@ember/object/computed';
|
|
||||||
|
|
||||||
export default class BudgetBalancesComponent extends Component {
|
export default class BudgetBalancesComponent extends Component {
|
||||||
@service communityFunds;
|
@service communityFunds;
|
||||||
@alias('communityFunds.balances') balances;
|
|
||||||
|
get balancesSorted () {
|
||||||
|
return this.communityFunds.balances
|
||||||
|
.sortBy('confirmed_balance').reverse();
|
||||||
|
}
|
||||||
|
|
||||||
get loading () {
|
get loading () {
|
||||||
return !this.communityFunds.balancesLoaded;
|
return !this.communityFunds.balancesLoaded;
|
||||||
|
|||||||
@@ -7,11 +7,19 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{#each this.balances as |balance|}}
|
{{#each this.balancesSorted as |balance|}}
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{balance.token.symbol}}</th>
|
<th>
|
||||||
<td class="amount">{{balance.confirmed_balance}}</td>
|
<img src={{balance.token.icon}}
|
||||||
<td class="fiat-amount">~{{balance.balanceUSD}} USD</td>
|
alt={{balance.token.description}}
|
||||||
|
title={{balance.token.description}} />
|
||||||
|
</th>
|
||||||
|
<td class="amount">
|
||||||
|
{{fmt-number balance.confirmed_balance}} <span class="unit">sats</span>
|
||||||
|
</td>
|
||||||
|
<td class="fiat-amount">
|
||||||
|
~{{balance.balanceUSD}} USD
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export default class ConfirmedInComponent extends Component {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
get confirmedInBlocks () {
|
||||||
|
return this.args.confirmedAtBlock - this.kredits.currentBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
get confirmedInSeconds () {
|
||||||
|
// A new block is mined every 30 seconds on average
|
||||||
|
return this.confirmedInBlocks * 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
get confirmedInHumanTime () {
|
||||||
|
return moment.duration(this.confirmedInSeconds, "seconds").humanize();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConfirmed () {
|
||||||
|
return this.confirmedInBlocks <= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{{#if this.isConfirmed}}
|
||||||
|
Confirmed at block <strong>{{@confirmedAtBlock}}</strong> (~ {{this.confirmedInHumanTime}} ago)
|
||||||
|
{{else}}
|
||||||
|
Confirming in <strong>{{this.confirmedInBlocks}}</strong> blocks (~ {{this.confirmedInHumanTime}})
|
||||||
|
{{/if}}
|
||||||
@@ -1,18 +1,43 @@
|
|||||||
import Component from '@ember/component';
|
import Component from '@glimmer/component';
|
||||||
|
import { action } from '@ember/object';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
|
||||||
export default Component.extend({
|
export default class ContributorComponent extends Component {
|
||||||
tagName: '',
|
@service router;
|
||||||
|
|
||||||
router: service(),
|
@tracked selectedContributorId = null;
|
||||||
|
@tracked showToplistOnly = true;
|
||||||
|
|
||||||
selectedContributorId: null,
|
get contributorList () {
|
||||||
|
return this.args.contributorList;
|
||||||
|
}
|
||||||
|
|
||||||
actions: {
|
get contributorTop10 () {
|
||||||
|
return this.contributorList ?
|
||||||
|
this.contributorList.slice(0, 10) : [];
|
||||||
|
}
|
||||||
|
|
||||||
openContributorDetails(contributor) {
|
get contributors () {
|
||||||
|
return this.showToplistOnly ?
|
||||||
|
this.contributorTop10 : this.contributorList;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hiddenContributorsAmount () {
|
||||||
|
return this.contributorList.length - 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showAllButtonText () {
|
||||||
|
return `Show ${this.hiddenContributorsAmount} more contributors`;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
openContributorDetails (contributor) {
|
||||||
this.router.transitionTo('dashboard.contributors.show', contributor);
|
this.router.transitionTo('dashboard.contributors.show', contributor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
showAllContributors () {
|
||||||
|
this.showToplistOnly = false;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<thead>
|
<thead>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{#each @contributorList as |c|}}
|
{{#each this.contributors as |c|}}
|
||||||
<tr role="button"
|
<tr role="button"
|
||||||
onclick={{action "openContributorDetails" c.contributor}}
|
onclick={{action "openContributorDetails" c.contributor}}
|
||||||
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id @selectedContributorId) "selected"}}">
|
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id @selectedContributorId) "selected"}}">
|
||||||
@@ -21,5 +21,12 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
{{#if this.showToplistOnly}}
|
||||||
|
<tr role="button" onclick={{action "showAllContributors"}}>
|
||||||
|
<td colspan="2">
|
||||||
|
{{this.showAllButtonText}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/if}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
|
||||||
|
export default class ExpenseListComponent extends Component {
|
||||||
|
|
||||||
|
get showDeleteButton () {
|
||||||
|
return !!this.args.deletable;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<ul class="expense-list">
|
||||||
|
{{#each @expenses as |expense|}}
|
||||||
|
<li class="expense-item">
|
||||||
|
<h4>
|
||||||
|
<span class="date">{{fmt-date-localized expense.date}}:</span>
|
||||||
|
<span class="title">{{expense.title}}</span>
|
||||||
|
</h4>
|
||||||
|
<div class="amount" title="{{fmt-amount-sats-title expense.amountSats}}">
|
||||||
|
{{fmt-fiat-currency expense.amount expense.currency}}
|
||||||
|
</div>
|
||||||
|
<p class="description">
|
||||||
|
{{expense.description}}
|
||||||
|
</p>
|
||||||
|
<p class="tags">
|
||||||
|
{{#each expense.tags as |tag|}}
|
||||||
|
<button type="button" class="small yellow" role="none">
|
||||||
|
<IconTag />{{tag}}
|
||||||
|
</button>
|
||||||
|
{{/each}}
|
||||||
|
</p>
|
||||||
|
{{#if this.showDeleteButton}}
|
||||||
|
<div class="actions">
|
||||||
|
<button {{on "click" (fn @removeExpenseItem expense)}}
|
||||||
|
class="danger small" type="button">delete</button>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<nav id="main-menu">
|
||||||
|
<ul>
|
||||||
|
<li><LinkTo @route="dashboard">Dashboard</LinkTo></li>
|
||||||
|
<li><LinkTo @route="budget">Budget</LinkTo></li>
|
||||||
|
<li><LinkTo @route="about">About</LinkTo></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
import fmtDateLocalized from 'kredits-web/helpers/fmt-date-localized';
|
||||||
|
|
||||||
|
export default class ReimbursementItemComponent extends Component {
|
||||||
|
@service kredits;
|
||||||
|
@tracked showExpenseDetails = false;
|
||||||
|
|
||||||
|
constructor(owner, args) {
|
||||||
|
super(owner, args);
|
||||||
|
if (this.isUnconfirmed && !this.isVetoed) {
|
||||||
|
this.showExpenseDetails = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get ipfsGatewayUrl () {
|
||||||
|
return config.ipfs.gatewayUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConfirmed () {
|
||||||
|
return (this.args.reimbursement.confirmedAt - this.kredits.currentBlock) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isUnconfirmed () {
|
||||||
|
return !this.isConfirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isVetoed () {
|
||||||
|
return this.args.reimbursement.vetoed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showVetoButton () {
|
||||||
|
return this.isUnconfirmed && this.kredits.currentUserIsCore;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showConfirmedIn () {
|
||||||
|
return !this.isVetoed &&
|
||||||
|
(this.showExpenseDetails || this.isUnconfirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
get expenses () {
|
||||||
|
return this.args.reimbursement.expenses;
|
||||||
|
}
|
||||||
|
|
||||||
|
get expensesDateRange () {
|
||||||
|
const dates = this.expenses.map(e => e.date).uniq().sort();
|
||||||
|
let out = fmtDateLocalized.compute(dates.firstObject)
|
||||||
|
if (dates.length > 1) {
|
||||||
|
out += ' - ' + fmtDateLocalized.compute(dates.lastObject)
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleExpenseDetails () {
|
||||||
|
this.showExpenseDetails = !this.showExpenseDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
veto (id) {
|
||||||
|
this.kredits.vetoReimbursement(id).then(transaction => {
|
||||||
|
console.debug('[controllers:budget] Veto submitted to chain: '+transaction.hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<li data-reimbursement-id={{@reimbursement.id}}
|
||||||
|
class="{{item-status @reimbursement}}">
|
||||||
|
<p class="meta cursor-pointer" role="button"
|
||||||
|
{{on "click" this.toggleExpenseDetails}}>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{{#if this.showExpenseDetails}}
|
||||||
|
<ExpenseList @expenses={{@reimbursement.expenses}} />
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<p>
|
||||||
|
{{#if this.showConfirmedIn}}
|
||||||
|
<ConfirmedIn @confirmedAtBlock={{@reimbursement.confirmedAt}} />
|
||||||
|
{{else}}
|
||||||
|
{{#unless this.isVetoed}}{{this.expensesDateRange}}{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
<p class="actions">
|
||||||
|
<a href="{{this.ipfsGatewayUrl}}/{{@reimbursement.ipfsHash}}"
|
||||||
|
class="button small" target="_blank" rel="noopener noreferrer">
|
||||||
|
Inspect IPFS data
|
||||||
|
</a>
|
||||||
|
{{#if this.showVetoButton}}
|
||||||
|
<button {{on "click" (fn this.veto @reimbursement.id)}}
|
||||||
|
disabled={{@reimbursement.vetoed}}
|
||||||
|
class="button small danger" type="button">veto</button>
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
@@ -1,23 +1,10 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { sort } from '@ember/object/computed';
|
import { sort } from '@ember/object/computed';
|
||||||
import { action } from '@ember/object';
|
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import config from 'kredits-web/config/environment';
|
|
||||||
|
|
||||||
export default class ReimbursementListComponent extends Component {
|
export default class ReimbursementListComponent extends Component {
|
||||||
@service kredits;
|
@service kredits;
|
||||||
|
|
||||||
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
|
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
|
||||||
@sort('args.items', 'itemSorting') itemsSorted;
|
@sort('args.items', 'itemSorting') itemsSorted;
|
||||||
|
|
||||||
get ipfsGatewayUrl () {
|
|
||||||
return config.ipfs.gatewayUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
veto (id) {
|
|
||||||
this.kredits.vetoReimbursement(id).then(transaction => {
|
|
||||||
console.debug('[controllers:budget] Veto submitted to chain: '+transaction.hash);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,5 @@
|
|||||||
<ul class="item-list spaced reimbursement-list {{if @loading 'loading'}}">
|
<ul class="item-list collapsible spaced reimbursement-list {{if @loading 'loading'}}">
|
||||||
{{#each this.itemsSorted as |reimbursement|}}
|
{{#each this.itemsSorted as |item|}}
|
||||||
<li data-reimbursement-id={{reimbursement.id}}
|
<ReimbursementItem @reimbursement={{item}} />
|
||||||
class="{{item-status reimbursement}}">
|
|
||||||
<p class="meta">
|
|
||||||
<span class="title">
|
|
||||||
Expenses covered by {{reimbursement.contributor.name}}
|
|
||||||
</span>
|
|
||||||
<span class="recipient">
|
|
||||||
<UserAvatar @contributor={{reimbursement.contributor}} />
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p class="token-amount">
|
|
||||||
<span class="amount">
|
|
||||||
{{sats-to-btc reimbursement.amount}}</span> <span class="symbol">BTC</span>
|
|
||||||
</p>
|
|
||||||
<ul class="expense-list">
|
|
||||||
{{#each reimbursement.expenses as |expense|}}
|
|
||||||
<li>
|
|
||||||
<div class="description" rowspan="2">
|
|
||||||
<h4>
|
|
||||||
<span class="date">{{expense.date}}:</span>
|
|
||||||
<span class="title">{{expense.title}}</span>
|
|
||||||
</h4>
|
|
||||||
<p class="description">{{expense.description}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="amount">
|
|
||||||
{{fmt-fiat-currency expense.amount expense.currency}}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
{{#if this.kredits.currentUserIsCore}}
|
|
||||||
<p>
|
|
||||||
<a href="{{this.ipfsGatewayUrl}}/{{reimbursement.ipfsHash}}"
|
|
||||||
class="button small" target="_blank" rel="noopener noreferrer">
|
|
||||||
Inspect IPFS data
|
|
||||||
</a>
|
|
||||||
<button {{on "click" (fn this.veto reimbursement.id)}}
|
|
||||||
disabled={{reimbursement.vetoed}}
|
|
||||||
class="button small danger" type="button">veto</button>
|
|
||||||
</p>
|
|
||||||
{{/if}}
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -1,45 +1,37 @@
|
|||||||
import Component from '@ember/component';
|
import Component from '@glimmer/component';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { computed } from '@ember/object';
|
import { action } from '@ember/object';
|
||||||
import { isPresent } from '@ember/utils';
|
import { isPresent } from '@ember/utils';
|
||||||
|
|
||||||
export default Component.extend({
|
export default class TopbarAccountPanelComponent extends Component {
|
||||||
|
@service router;
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
tagName: '',
|
@tracked setupInProgress = false;
|
||||||
|
|
||||||
kredits: service(),
|
get userHasWallet () {
|
||||||
router: service(),
|
|
||||||
|
|
||||||
setupInProgress: false,
|
|
||||||
|
|
||||||
userHasEthereumWallet: computed(function() {
|
|
||||||
return isPresent(window.ethereum);
|
return isPresent(window.ethereum);
|
||||||
}),
|
}
|
||||||
|
|
||||||
showConnectButton: computed('userHasEthereumWallet',
|
get walletConnected () {
|
||||||
'kredits.hasAccounts', function() {
|
return this.userHasWallet && this.kredits.hasAccounts;
|
||||||
return this.userHasEthereumWallet &&
|
}
|
||||||
!this.kredits.hasAccounts;
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
get walletDisconnected () {
|
||||||
|
return this.userHasWallet && !this.kredits.hasAccounts;
|
||||||
|
}
|
||||||
|
|
||||||
signup() {
|
@action
|
||||||
|
signup () {
|
||||||
this.router.transitionTo('signup');
|
this.router.transitionTo('signup');
|
||||||
},
|
|
||||||
|
|
||||||
async connectAccount() {
|
|
||||||
try {
|
|
||||||
await window.ethereum.enable();
|
|
||||||
this.set('setupInProgress', true);
|
|
||||||
await this.kredits.setup();
|
|
||||||
this.set('setupInProgress', false);
|
|
||||||
} catch (error) {
|
|
||||||
this.set('setupInProgress', false);
|
|
||||||
console.log('Opening Ethereum wallet failed:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async connectWallet () {
|
||||||
|
this.setupInProgress = true;
|
||||||
|
await this.kredits.connectWallet();
|
||||||
|
this.setupInProgress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -2,16 +2,15 @@
|
|||||||
{{#if this.setupInProgress}}
|
{{#if this.setupInProgress}}
|
||||||
Connecting account...
|
Connecting account...
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if (and this.kredits.hasAccounts this.kredits.currentUser)}}
|
{{#if (and this.walletConnected this.kredits.currentUser)}}
|
||||||
{{this.kredits.currentUser.name}}
|
{{this.kredits.currentUser.name}}
|
||||||
{{#if this.kredits.currentUserIsCore}}
|
|
||||||
<span class="core-flag">(core)</span>
|
|
||||||
{{/if}}
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="anonymous">Anonymous</span>
|
<span class="anonymous">Anonymous</span>
|
||||||
<button onclick={{action "signup"}} class="small" type="button">Sign up</button>
|
<button onclick={{action "signup"}} id="signup"
|
||||||
{{#if this.showConnectButton}}
|
class="small" type="button">Sign up</button>
|
||||||
<button onclick={{action "connectAccount"}} class="small green" type="button">Connect account</button>
|
{{#if this.walletDisconnected}}
|
||||||
|
<button onclick={{action "connectWallet"}} id="connect"
|
||||||
|
class="small green" type="button">Connect wallet</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default class ApplicationController extends Controller {
|
||||||
kredits: service(),
|
@service kredits;
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { alias } from '@ember/object/computed';
|
import { alias } from '@ember/object/computed';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
|
||||||
export default class BudgetController extends Controller {
|
export default class BudgetController extends Controller {
|
||||||
@service kredits;
|
@service kredits;
|
||||||
|
@service router;
|
||||||
|
|
||||||
@alias('kredits.reimbursementsUnconfirmed') reimbursementsUnconfirmed;
|
@alias('kredits.reimbursementsUnconfirmed') reimbursementsUnconfirmed;
|
||||||
@alias('kredits.reimbursementsConfirmed') reimbursementsConfirmed;
|
@alias('kredits.reimbursementsConfirmed') reimbursementsConfirmed;
|
||||||
|
@alias('kredits.currentUserIsCore') currentUserIsCore;
|
||||||
|
|
||||||
|
@action
|
||||||
|
addReimbursement () {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
this.router.transitionTo('reimbursements.new');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
import { computed } from '@ember/object';
|
import { computed } from '@ember/object';
|
||||||
import { alias, not, sort } from '@ember/object/computed';
|
import { alias, gt, not, sort } from '@ember/object/computed';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
@@ -25,7 +25,10 @@ export default Controller.extend({
|
|||||||
kreditsToplistSorting: computed('showUnconfirmedKredits', function(){
|
kreditsToplistSorting: computed('showUnconfirmedKredits', function(){
|
||||||
return this.showUnconfirmedKredits ? ['amountTotal:desc'] : ['amountConfirmed:desc'];
|
return this.showUnconfirmedKredits ? ['amountTotal:desc'] : ['amountConfirmed:desc'];
|
||||||
}),
|
}),
|
||||||
kreditsToplist: sort('kreditsByContributor', 'kreditsToplistSorting'),
|
kreditsToplist: computed('kreditsByContributor', function(){
|
||||||
|
return this.kreditsByContributor.filter(c => c.amountTotal > 0);
|
||||||
|
}),
|
||||||
|
kreditsToplistSorted: sort('kreditsToplist', 'kreditsToplistSorting'),
|
||||||
|
|
||||||
showUnconfirmedKredits: true,
|
showUnconfirmedKredits: true,
|
||||||
hideUnconfirmedKredits: not('showUnconfirmedKredits'),
|
hideUnconfirmedKredits: not('showUnconfirmedKredits'),
|
||||||
@@ -33,9 +36,7 @@ export default Controller.extend({
|
|||||||
showQuickFilterUnconfirmed: false,
|
showQuickFilterUnconfirmed: false,
|
||||||
showQuickFilterConfirmed: false,
|
showQuickFilterConfirmed: false,
|
||||||
|
|
||||||
showIntroText: computed('kredits.{hasAccounts,currentUser}', function(){
|
showFullContributionSync: gt('kredits.missingHistoricContributionsCount', 0),
|
||||||
return (!this.kredits.hasAccounts || !this.kredits.currentUser);
|
|
||||||
}),
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import Controller from '@ember/controller';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
import { isAddress } from 'web3-utils';
|
||||||
|
|
||||||
|
export default class AccountController extends Controller {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
@tracked accountAddress = null;
|
||||||
|
|
||||||
|
get isValidEthAccount () {
|
||||||
|
return isAddress(this.accountAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
get signupButtonDisabled () {
|
||||||
|
return !this.isValidEthAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
completeSignup () {
|
||||||
|
const payload = {
|
||||||
|
accessToken: this.kredits.githubAccessToken,
|
||||||
|
account: this.accountAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(config.githubSignupUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
alert('Creating profile failed. We have been notified about this error and will take a look soon. Sorry!');
|
||||||
|
console.warn('Creating contributor profile failed:',
|
||||||
|
JSON.parse(data.error.body).error.message)
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.log('[signup/account] Created contributor:', data);
|
||||||
|
|
||||||
|
this.kredits.githubAccessToken = null;
|
||||||
|
this.accountAddress = null;
|
||||||
|
|
||||||
|
this.transitionToRoute('signup.complete');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import Controller from '@ember/controller';
|
|
||||||
import { computed } from '@ember/object';
|
|
||||||
import { alias, not } from '@ember/object/computed';
|
|
||||||
import { isAddress } from 'web3-utils';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import config from 'kredits-web/config/environment';
|
|
||||||
|
|
||||||
export default Controller.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
ethAddress: null,
|
|
||||||
githubAccessToken: alias('kredits.githubAccessToken'),
|
|
||||||
|
|
||||||
isValidEthAccount: computed('ethAddress', function() {
|
|
||||||
return isAddress(this.ethAddress);
|
|
||||||
}),
|
|
||||||
|
|
||||||
signupButtonDisabled: not('isValidEthAccount'),
|
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
completeSignup () {
|
|
||||||
const payload = {
|
|
||||||
accessToken: this.githubAccessToken,
|
|
||||||
account: this.ethAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(config.githubSignupUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(data => {
|
|
||||||
console.log('Created contributor:', data);
|
|
||||||
|
|
||||||
this.setProperties({
|
|
||||||
githubAccessToken: null,
|
|
||||||
ethAddress: null
|
|
||||||
});
|
|
||||||
|
|
||||||
this.transitionToRoute('signup.complete');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import Controller from '@ember/controller';
|
import Controller from '@ember/controller';
|
||||||
|
import { action } from '@ember/object';
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
export default Controller.extend({
|
export default class IndexController extends Controller {
|
||||||
|
|
||||||
actions: {
|
|
||||||
|
|
||||||
|
@action
|
||||||
connectGithub () {
|
connectGithub () {
|
||||||
window.location = config.githubConnectUrl;
|
window.location = config.githubConnectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function fmtAmountSatsTitle(params) {
|
||||||
|
const amountSats = params[0];
|
||||||
|
|
||||||
|
if (typeof amountSats === 'number' && amountSats > 0) {
|
||||||
|
return `${amountSats} sats`;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
import getLocale from 'kredits-web/utils/get-locale';
|
||||||
|
|
||||||
|
export default helper(function fmtDateLocalized(dateStr) {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const locale = getLocale();
|
||||||
|
return new Intl.DateTimeFormat(locale).format(date);
|
||||||
|
});
|
||||||
@@ -2,10 +2,14 @@ import { helper } from '@ember/component/helper';
|
|||||||
|
|
||||||
export default helper(function fmtFiatCurrency(params) {
|
export default helper(function fmtFiatCurrency(params) {
|
||||||
const lang = navigator.language || navigator.userLanguage;
|
const lang = navigator.language || navigator.userLanguage;
|
||||||
|
const value = params[0];
|
||||||
|
const currency = params[1] || 'EUR';
|
||||||
|
|
||||||
|
if (currency === 'BTC') return `BTC ${value}`;
|
||||||
|
|
||||||
const formatter = new Intl.NumberFormat(lang, {
|
const formatter = new Intl.NumberFormat(lang, {
|
||||||
style: 'currency',
|
style: 'currency', currency, currencyDisplay: 'code'
|
||||||
currency: params[1] || 'EUR',
|
|
||||||
currencyDisplay: 'code'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return formatter.format(params[0]);
|
return formatter.format(params[0]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function fmtNumber(number) {
|
||||||
|
const lang = navigator.language || navigator.userLanguage;
|
||||||
|
return number.toLocaleString(lang);
|
||||||
|
});
|
||||||
+2
-4
@@ -11,7 +11,6 @@ Router.map(function() {
|
|||||||
this.route('contributors', function() {
|
this.route('contributors', function() {
|
||||||
this.route('show', { path: ':id' });
|
this.route('show', { path: ':id' });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('contributions', function() {
|
this.route('contributions', function() {
|
||||||
this.route('show', { path: ':id' });
|
this.route('show', { path: ':id' });
|
||||||
});
|
});
|
||||||
@@ -26,16 +25,15 @@ Router.map(function() {
|
|||||||
});
|
});
|
||||||
this.route('signup', function() {
|
this.route('signup', function() {
|
||||||
this.route('github');
|
this.route('github');
|
||||||
this.route('eth-account');
|
this.route('account');
|
||||||
this.route('complete');
|
this.route('complete');
|
||||||
});
|
});
|
||||||
this.route('budget', function() {
|
this.route('budget', function() {
|
||||||
this.route('expenses');
|
this.route('expenses');
|
||||||
|
|
||||||
this.route('reimbursements', function() {});
|
this.route('reimbursements', function() {});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('reimbursements', function() {
|
this.route('reimbursements', function() {
|
||||||
this.route('new');
|
this.route('new');
|
||||||
});
|
});
|
||||||
|
this.route('about');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
|
||||||
|
export default class AboutRoute extends Route {
|
||||||
|
}
|
||||||
@@ -11,8 +11,11 @@ export default class DashboardRoute extends Route {
|
|||||||
schedule('afterRender', this.kredits.syncContributions,
|
schedule('afterRender', this.kredits.syncContributions,
|
||||||
this.kredits.syncContributions.perform);
|
this.kredits.syncContributions.perform);
|
||||||
}
|
}
|
||||||
schedule('afterRender', this.kredits.fetchMissingContributions,
|
// TODO fetch automatically under a certain threshold
|
||||||
this.kredits.fetchMissingContributions.perform);
|
// The browser might delete cached data and we don't need manual re-syncs
|
||||||
|
// depending on how little is missing
|
||||||
|
// schedule('afterRender', this.kredits.fetchMissingContributions,
|
||||||
|
// this.kredits.fetchMissingContributions.perform);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ export default Route.extend({
|
|||||||
contributors: alias('kredits.contributors'),
|
contributors: alias('kredits.contributors'),
|
||||||
|
|
||||||
model (params) {
|
model (params) {
|
||||||
return this.contributors.findBy('id', params.id);
|
const contributor = this.contributors.findBy('id', params.id);
|
||||||
|
|
||||||
|
if (contributor) {
|
||||||
|
return contributor;
|
||||||
|
} else {
|
||||||
|
return this.kredits.fetchContributor(params.id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController (controller, model) {
|
setupController (controller, model) {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import Route from '@ember/routing/route';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { isEmpty } from '@ember/utils';
|
||||||
|
|
||||||
|
export default class SignupAccountRoute extends Route {
|
||||||
|
@service kredits;
|
||||||
|
|
||||||
|
async setupController (controller) {
|
||||||
|
if (!window.ethereum) return;
|
||||||
|
|
||||||
|
if (this.kredits.hasAccounts) {
|
||||||
|
controller.accountAddress = this.kredits.currentUserAccounts.firstObject;
|
||||||
|
} else {
|
||||||
|
return this.kredits.connectWallet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect () {
|
||||||
|
if (isEmpty(this.kredits.githubAccessToken)) {
|
||||||
|
this.transitionTo('signup.index');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import Route from '@ember/routing/route';
|
|
||||||
import { inject as service } from '@ember/service';
|
|
||||||
import { isEmpty } from '@ember/utils';
|
|
||||||
|
|
||||||
export default Route.extend({
|
|
||||||
|
|
||||||
kredits: service(),
|
|
||||||
|
|
||||||
redirect () {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
if (isEmpty(this.kredits.githubAccessToken)) {
|
|
||||||
this.transitionTo('signup.index');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -22,7 +22,7 @@ export default Route.extend({
|
|||||||
|
|
||||||
this.kredits.set('githubAccessToken', accessToken);
|
this.kredits.set('githubAccessToken', accessToken);
|
||||||
|
|
||||||
this.transitionTo('signup.eth-account');
|
this.transitionTo('signup.account');
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import config from 'kredits-web/config/environment';
|
|||||||
|
|
||||||
function createStore(name) {
|
function createStore(name) {
|
||||||
let networkName;
|
let networkName;
|
||||||
if (config.web3RequiredNetworkName) {
|
if (config.web3NetworkName) {
|
||||||
networkName = config.web3RequiredNetworkName.toLocaleLowerCase().replace(' ', '-');
|
networkName = config.web3NetworkName.toLocaleLowerCase().replace(' ', '-');
|
||||||
} else {
|
} else {
|
||||||
networkName = 'custom';
|
networkName = 'custom';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Service, { inject as service } from '@ember/service';
|
|
||||||
import { tracked } from '@glimmer/tracking';
|
|
||||||
import { A } from '@ember/array';
|
import { A } from '@ember/array';
|
||||||
import { task } from 'ember-concurrency-decorators';
|
import { task } from 'ember-concurrency-decorators';
|
||||||
|
import { tracked } from '@glimmer/tracking';
|
||||||
|
import Service, { inject as service } from '@ember/service';
|
||||||
import config from 'kredits-web/config/environment';
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
export default class CommunityFundsService extends Service {
|
export default class CommunityFundsService extends Service {
|
||||||
@@ -12,9 +12,20 @@ export default class CommunityFundsService extends Service {
|
|||||||
|
|
||||||
@task
|
@task
|
||||||
*fetchBalances () {
|
*fetchBalances () {
|
||||||
yield fetch(config.btcBalanceAPI).then(res => res.json())
|
const promises = [];
|
||||||
.then(res => {
|
const balances = config.communityFundsAPI.balances;
|
||||||
return this.processBalances(res);
|
|
||||||
|
for (const item of Object.keys(balances)) {
|
||||||
|
const c = balances[item];
|
||||||
|
promises.push(
|
||||||
|
this.fetchBalance(c.url)
|
||||||
|
.then(res => { return this.processBalance(res, c) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
yield Promise.all(promises)
|
||||||
|
.then(() => {
|
||||||
|
this.balancesLoaded = true;
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.log(`[community-funds] Fetching balances failed:`);
|
console.log(`[community-funds] Fetching balances failed:`);
|
||||||
@@ -22,18 +33,21 @@ export default class CommunityFundsService extends Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async processBalances (res) {
|
async fetchBalance(url) {
|
||||||
|
return fetch(url).then(res => res.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
async processBalance (res, config) {
|
||||||
await this.exchangeRates.fetchRates();
|
await this.exchangeRates.fetchRates();
|
||||||
|
|
||||||
// Format and round the approximate USD value
|
// Format and round the approximate USD value
|
||||||
const lang = navigator.language || navigator.userLanguage;
|
const lang = navigator.language || navigator.userLanguage;
|
||||||
const balanceUSD = res.confirmed_balance * this.exchangeRates.btcusd;
|
const balanceUSD = (res.confirmed_balance / 100000000) * this.exchangeRates.USD;
|
||||||
res.balanceUSD = Math.round(balanceUSD).toLocaleString(lang);
|
res.balanceUSD = Math.round(balanceUSD).toLocaleString(lang);
|
||||||
|
|
||||||
this.balances.pushObject({
|
this.balances.pushObject({
|
||||||
...res,
|
...res,
|
||||||
...{ token: { name: 'BTC', symbol: 'BTC'} }
|
...{ token: { icon: `/img/${config.icon}`, symbol: config.symbol, description: config.description } }
|
||||||
});
|
});
|
||||||
|
|
||||||
this.balancesLoaded = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import config from 'kredits-web/config/environment';
|
|||||||
|
|
||||||
// Need to go through proxy for CORS headers
|
// Need to go through proxy for CORS headers
|
||||||
const bitstampBaseUrl = `${config.corsProxy}https://www.bitstamp.net/api/v2`;
|
const bitstampBaseUrl = `${config.corsProxy}https://www.bitstamp.net/api/v2`;
|
||||||
|
const kosmosBtcPriceBaseUrl = "https://storage.kosmos.org/kosmos/public/btc-price";
|
||||||
|
|
||||||
async function fetchFromBitstamp(currencyPair) {
|
async function fetchFromBitstamp(currencyPair) {
|
||||||
try {
|
try {
|
||||||
@@ -16,14 +17,37 @@ async function fetchFromBitstamp(currencyPair) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class ExchangeRatesService extends Service {
|
export default class ExchangeRatesService extends Service {
|
||||||
@tracked btceur = 0;
|
@tracked EUR = 0;
|
||||||
@tracked btcusd = 0;
|
@tracked USD = 0;
|
||||||
|
@tracked historic = {};
|
||||||
|
|
||||||
|
get exchangeRatesLoaded () {
|
||||||
|
return (this.EUR !== 0) && (this.USD !== 0);
|
||||||
|
}
|
||||||
|
|
||||||
async fetchRates (source='bitstamp') {
|
async fetchRates (source='bitstamp') {
|
||||||
|
if (this.exchangeRatesLoaded) return;
|
||||||
|
|
||||||
switch(source) {
|
switch(source) {
|
||||||
case 'bitstamp':
|
case 'bitstamp':
|
||||||
this.btceur = await fetchFromBitstamp('btceur');
|
this.EUR = await fetchFromBitstamp('btceur');
|
||||||
this.btcusd = await fetchFromBitstamp('btcusd');
|
this.USD = await fetchFromBitstamp('btcusd');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchHistoricRates (isoDate) {
|
||||||
|
if (typeof this.historic[isoDate] === "object") {
|
||||||
|
return this.historic[isoDate];
|
||||||
|
} else {
|
||||||
|
const url = `${kosmosBtcPriceBaseUrl}/${isoDate}`;
|
||||||
|
try {
|
||||||
|
const rates = await fetch(url).then(res => res.json());
|
||||||
|
this.historic[isoDate] = rates;
|
||||||
|
return rates;
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
Promise.reject(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-29
@@ -5,7 +5,7 @@ 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, filterBy, notEmpty, sort } from '@ember/object/computed';
|
import { alias, filterBy, notEmpty, sort } from '@ember/object/computed';
|
||||||
import { isEmpty, isPresent } from '@ember/utils';
|
import { isEmpty } from '@ember/utils';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
import { task, taskGroup } from 'ember-concurrency';
|
import { task, taskGroup } from 'ember-concurrency';
|
||||||
@@ -15,6 +15,7 @@ import processContributorData from 'kredits-web/utils/process-contributor-data';
|
|||||||
import processContributionData from 'kredits-web/utils/process-contribution-data';
|
import processContributionData from 'kredits-web/utils/process-contribution-data';
|
||||||
import processReimbursementData from 'kredits-web/utils/process-reimbursement-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 switchNetwork from 'kredits-web/utils/switch-network';
|
||||||
|
|
||||||
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';
|
||||||
@@ -28,7 +29,7 @@ export default Service.extend({
|
|||||||
browserCache: service(),
|
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 when web3 is loaded.
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
contributors: null,
|
contributors: null,
|
||||||
contributions: null,
|
contributions: null,
|
||||||
@@ -48,11 +49,28 @@ export default Service.extend({
|
|||||||
contributionsNeedSync: false,
|
contributionsNeedSync: false,
|
||||||
reimbursementsNeedSync: false,
|
reimbursementsNeedSync: false,
|
||||||
|
|
||||||
|
missingHistoricContributionsCount: 0,
|
||||||
|
|
||||||
init () {
|
init () {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.set('contributors', []);
|
this.set('contributors', []);
|
||||||
this.set('contributions', []);
|
this.set('contributions', []);
|
||||||
this.set('reimbursements', []);
|
this.set('reimbursements', []);
|
||||||
|
|
||||||
|
if (window.ethereum) {
|
||||||
|
window.ethereum.on('chainChanged', this.handleUserChainChanged);
|
||||||
|
window.ethereum.on('accountsChanged', this.handleAccountsChanged);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleUserChainChanged (chainId) {
|
||||||
|
console.log('User-provided chain ID changed to', chainId);
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleAccountsChanged (accounts) {
|
||||||
|
console.log('User-provided accounts changed to', accounts);
|
||||||
|
window.location.reload();
|
||||||
},
|
},
|
||||||
|
|
||||||
// This is called in the application route's beforeModel(). So it is
|
// This is called in the application route's beforeModel(). So it is
|
||||||
@@ -62,7 +80,7 @@ export default Service.extend({
|
|||||||
let ethProvider;
|
let ethProvider;
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
function instantiateWithoutAccount () {
|
function instantiateWithoutWallet () {
|
||||||
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}`);
|
||||||
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
|
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
|
||||||
@@ -72,44 +90,50 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function instantiateWithAccount (web3Provider, context) {
|
async function instantiateWithWallet (web3Provider, context) {
|
||||||
console.debug('[kredits] Using user-provided Web3 instance, e.g. from 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.web3RequiredChainId) &&
|
const accounts = await ethProvider.listAccounts();
|
||||||
network.chainId !== config.web3RequiredChainId) {
|
const chainId = config.web3ChainId;
|
||||||
return instantiateWithoutAccount();
|
|
||||||
}
|
|
||||||
|
|
||||||
ethProvider.listAccounts().then(accounts => {
|
if (isEmpty(accounts)) return instantiateWithoutWallet();
|
||||||
|
|
||||||
|
if (network.chainId !== chainId) {
|
||||||
|
return switchNetwork();
|
||||||
|
} else {
|
||||||
context.set('currentUserAccounts', accounts);
|
context.set('currentUserAccounts', accounts);
|
||||||
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
|
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
|
||||||
resolve({
|
resolve({
|
||||||
ethProvider,
|
ethProvider,
|
||||||
ethSigner
|
ethSigner
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.ethereum) {
|
if (window.ethereum) {
|
||||||
if (window.ethereum.isConnected()) {
|
instantiateWithWallet(window.ethereum, this);
|
||||||
instantiateWithAccount(window.ethereum, this);
|
|
||||||
} else {
|
} else {
|
||||||
instantiateWithoutAccount();
|
instantiateWithoutWallet();
|
||||||
}
|
|
||||||
}
|
|
||||||
// Legacy dapp browsers...
|
|
||||||
else if (window.web3) {
|
|
||||||
instantiateWithAccount(window.web3.currentProvider, this);
|
|
||||||
}
|
|
||||||
// Non-dapp browsers...
|
|
||||||
else {
|
|
||||||
instantiateWithoutAccount();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async connectWallet () {
|
||||||
|
const provider = new ethers.providers.Web3Provider(window.ethereum);
|
||||||
|
const network = await provider.getNetwork();
|
||||||
|
const chainId = config.web3ChainId;
|
||||||
|
const chainIdHex = `0x${Number(chainId).toString(16)}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({ method: 'eth_requestAccounts' });
|
||||||
|
if (network.chainId !== chainId) await switchNetwork(chainIdHex);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Connecting wallet failed:', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async setup () {
|
async setup () {
|
||||||
const kredits = await this.getEthProvider().then(providerAndSigner => {
|
const kredits = await this.getEthProvider().then(providerAndSigner => {
|
||||||
return new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
|
return new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
|
||||||
@@ -118,9 +142,12 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await kredits.init();
|
await kredits.init();
|
||||||
|
|
||||||
this.set('kredits', kredits);
|
this.set('kredits', kredits);
|
||||||
this.set('currentBlock', await kredits.provider.getBlockNumber());
|
this.set('currentBlock', await this.kredits.provider.getBlockNumber());
|
||||||
|
this.kredits.provider.on('block', blockNumber => {
|
||||||
|
console.debug('[kredits] New block mined:', blockNumber);
|
||||||
|
this.set('currentBlock', blockNumber)
|
||||||
|
});
|
||||||
|
|
||||||
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
|
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
|
||||||
this.getCurrentUser.then(contributorData => {
|
this.getCurrentUser.then(contributorData => {
|
||||||
@@ -227,12 +254,20 @@ export default Service.extend({
|
|||||||
await this.loadObjectsFromCache('Contribution');
|
await this.loadObjectsFromCache('Contribution');
|
||||||
this.set('contributionsNeedSync', true);
|
this.set('contributionsNeedSync', true);
|
||||||
} else {
|
} else {
|
||||||
await this.fetchContributions({ page: { size: 30 } });
|
await this.fetchContributions({ page: { size: 40 } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.updateMissingHistoricContributionsCount();
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async updateMissingHistoricContributionsCount () {
|
||||||
|
const contributionsCount = await this.kredits.Contribution.count;
|
||||||
|
this.set('missingHistoricContributionsCount', contributionsCount - this.contributions.length);
|
||||||
|
console.debug(`Missing ${this.missingHistoricContributionsCount} historic contributions (out of ${contributionsCount} overall)`)
|
||||||
|
},
|
||||||
|
|
||||||
addContributor (attributes) {
|
addContributor (attributes) {
|
||||||
if (attributes.github_uid) {
|
if (attributes.github_uid) {
|
||||||
const uidInt = parseInt(attributes.github_uid);
|
const uidInt = parseInt(attributes.github_uid);
|
||||||
@@ -261,6 +296,12 @@ export default Service.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetchContributor (id) {
|
||||||
|
console.debug(`[kredits] Fetching contributor from the network`);
|
||||||
|
return this.kredits.Contributor.getById(id)
|
||||||
|
.then(data => this.loadContributorFromData(data))
|
||||||
|
},
|
||||||
|
|
||||||
fetchContributors () {
|
fetchContributors () {
|
||||||
console.debug(`[kredits] Fetching all contributors from the network`);
|
console.debug(`[kredits] Fetching all contributors from the network`);
|
||||||
return this.kredits.Contributor.all()
|
return this.kredits.Contributor.all()
|
||||||
@@ -368,11 +409,12 @@ export default Service.extend({
|
|||||||
syncContributions: task(function * () {
|
syncContributions: task(function * () {
|
||||||
yield this.fetchNewContributions.perform();
|
yield this.fetchNewContributions.perform();
|
||||||
yield this.syncUnconfirmedContributions.perform();
|
yield this.syncUnconfirmedContributions.perform();
|
||||||
|
yield this.updateMissingHistoricContributionsCount();
|
||||||
this.set('contributionsNeedSync', false);
|
this.set('contributionsNeedSync', false);
|
||||||
}).group('contributionTasks'),
|
}).group('contributionTasks'),
|
||||||
|
|
||||||
fetchNewContributions: task(function * () {
|
fetchNewContributions: task(function * () {
|
||||||
const count = yield this.kredits.Contribution.functions.contributionsCount();
|
const count = yield this.kredits.Contribution.count;
|
||||||
const lastKnownContributionId = Math.max.apply(null, this.contributions.mapBy('id'));
|
const lastKnownContributionId = Math.max.apply(null, this.contributions.mapBy('id'));
|
||||||
const toFetch = count - lastKnownContributionId;
|
const toFetch = count - lastKnownContributionId;
|
||||||
|
|
||||||
@@ -389,7 +431,7 @@ export default Service.extend({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
fetchMissingContributions: task(function * () {
|
fetchMissingContributions: task(function * () {
|
||||||
const count = yield this.kredits.Contribution.functions.contributionsCount();
|
const count = yield this.kredits.Contribution.count;
|
||||||
const allIds = [...Array(count+1).keys()];
|
const allIds = [...Array(count+1).keys()];
|
||||||
allIds.shift(); // remove first item, which is 0
|
allIds.shift(); // remove first item, which is 0
|
||||||
const loadedContributions = new Set(this.contributions.mapBy('id'));
|
const loadedContributions = new Set(this.contributions.mapBy('id'));
|
||||||
@@ -738,5 +780,5 @@ export default Service.extend({
|
|||||||
this.contributors
|
this.contributors
|
||||||
.findBy('address', to)
|
.findBy('address', to)
|
||||||
.incrementProperty('balance', value);
|
.incrementProperty('balance', value);
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+32
-11
@@ -31,32 +31,53 @@ button, input[type=submit], .button {
|
|||||||
&.small {
|
&.small {
|
||||||
font-size: 0.86rem;
|
font-size: 0.86rem;
|
||||||
padding: 0.2rem 0.8rem;
|
padding: 0.2rem 0.8rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.danger:not(:disabled) {
|
&.danger:not(:disabled) {
|
||||||
color: $red;
|
color: $red;
|
||||||
background-color: rgba(40, 21, 21, 0.6);
|
background-color: rgba(40, 21, 21, 0.6);
|
||||||
border-color: rgba(40, 21, 21, 1);
|
border-color: rgba(40, 21, 21, 1);
|
||||||
|
&:hover { background-color: rgba(40, 21, 21, 0.8); }
|
||||||
&:hover {
|
&:focus, &:active, &.active { border-color: $red; }
|
||||||
background-color: rgba(40, 21, 21, 0.8);
|
|
||||||
}
|
|
||||||
&:focus, &:active, &.active {
|
|
||||||
border-color: $red;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.green:not(:disabled) {
|
&.green:not(:disabled) {
|
||||||
color: $green;
|
color: $green;
|
||||||
background-color: rgba(21, 40, 21, 0.6);
|
background-color: rgba(21, 40, 21, 0.6);
|
||||||
border-color: rgba(21, 40, 21, 1);
|
border-color: rgba(21, 40, 21, 1);
|
||||||
|
&:hover { background-color: rgba(21, 40, 21, 0.8); }
|
||||||
|
&:focus, &:active, &.active { border-color: $green; }
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&.pink:not(:disabled) {
|
||||||
background-color: rgba(21, 40, 21, 0.8);
|
color: $pink;
|
||||||
|
background-color: rgba(40, 21, 40, 0.6);
|
||||||
|
border-color: rgba(40, 21, 40, 1);
|
||||||
|
&:hover { background-color: rgba(40, 21, 40, 0.8); }
|
||||||
|
&:focus, &:active, &.active { border-color: $pink; }
|
||||||
}
|
}
|
||||||
&:focus, &:active, &.active {
|
|
||||||
border-color: $green;
|
&.purple:not(:disabled) {
|
||||||
|
color: $purple;
|
||||||
|
background-color: rgba(24, 21, 40, 0.6);
|
||||||
|
border-color: rgba(24, 21, 40, 1);
|
||||||
|
&:hover { background-color: rgba(24, 21, 40, 0.8); }
|
||||||
|
&:focus, &:active, &.active { border-color: $purple; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.yellow:not(:disabled) {
|
||||||
|
color: $yellow;
|
||||||
|
background-color: rgba(40, 40, 21, 0.6);
|
||||||
|
border-color: rgba(40, 40, 21, 1);
|
||||||
|
&:hover { background-color: rgba(40, 40, 21, 0.8); }
|
||||||
|
&:focus, &:active, &.active { border-color: $yellow; }
|
||||||
}
|
}
|
||||||
|
|
||||||
&.icon {
|
&.icon {
|
||||||
|
|||||||
@@ -14,10 +14,6 @@ section#signup {
|
|||||||
p {
|
p {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
|
||||||
&.mg-bottom-md {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.label {
|
&.label {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ ul.item-list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.collapsible {
|
||||||
|
> li {
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-left: 1px solid $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
@include loading-border-top;
|
@include loading-border-top;
|
||||||
|
|
||||||
|
|||||||
+23
-35
@@ -1,3 +1,7 @@
|
|||||||
|
body {
|
||||||
|
margin-bottom: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
|
|
||||||
@@ -14,7 +18,7 @@ main {
|
|||||||
"contributions";
|
"contributions";
|
||||||
}
|
}
|
||||||
|
|
||||||
&#budget {
|
&#budget, &#about {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-row-gap: 2rem;
|
grid-row-gap: 2rem;
|
||||||
@@ -56,9 +60,9 @@ main {
|
|||||||
|
|
||||||
&.text-lg {
|
&.text-lg {
|
||||||
p {
|
p {
|
||||||
font-size: 1.2rem;
|
font-size: 1.35rem;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 2rem;
|
||||||
line-height: 1.5em;
|
line-height: 150%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,50 +71,29 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
&.mg-bottom-md {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.actions {
|
&.actions {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#intro {
|
|
||||||
padding: 2rem;
|
|
||||||
background-color: rgba(0,0,0,.2);
|
|
||||||
font-size: 1.6rem;
|
|
||||||
|
|
||||||
@include media-max(small) {
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
&.text {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: inherit;
|
margin: 4rem 0 2rem 0;
|
||||||
margin-bottom: 1em;
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 1em;
|
font-size: 1.35rem;
|
||||||
line-height: 1.5em;
|
margin-bottom: 2rem;
|
||||||
font-size: 1.2rem;
|
line-height: 150%;
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
&:hover, &:active {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +114,7 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&#budget {
|
&#budget, &#about {
|
||||||
grid-column-gap: 3rem;
|
grid-column-gap: 3rem;
|
||||||
grid-template-columns: 2fr 4fr 2fr;
|
grid-template-columns: 2fr 4fr 2fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
@@ -177,4 +160,9 @@ main section {
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Remove after switch to Tailwind CSS
|
||||||
|
.mb-4 { margin-bottom: 1rem; }
|
||||||
|
.mb-8 { margin-bottom: 2rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ a {
|
|||||||
color: $primary-color;
|
color: $primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cursor-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@@ -98,6 +102,7 @@ section {
|
|||||||
@import "components/contributor-profile";
|
@import "components/contributor-profile";
|
||||||
@import "components/expense-list";
|
@import "components/expense-list";
|
||||||
@import "components/external-account-link";
|
@import "components/external-account-link";
|
||||||
|
@import "components/main-navigation-menu";
|
||||||
@import "components/loading-spinner";
|
@import "components/loading-spinner";
|
||||||
@import "components/reimbursement-list";
|
@import "components/reimbursement-list";
|
||||||
@import "components/topbar";
|
@import "components/topbar";
|
||||||
|
|||||||
@@ -16,8 +16,12 @@ section#funds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
font-size: 1.2rem;
|
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-height: 1.5rem;
|
||||||
|
max-width: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
@@ -35,8 +39,13 @@ section#funds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.fiat-amount {
|
&.fiat-amount {
|
||||||
|
font-size: 1.2rem;
|
||||||
color: rgba(255,255,255,0.8);
|
color: rgba(255,255,255,0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.unit {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,32 +17,30 @@ ul.expense-list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
|
||||||
grid-row-start: 1;
|
|
||||||
grid-row-end: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount {
|
|
||||||
justify-self: end;
|
|
||||||
// font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
justify-self: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.amount {
|
||||||
line-height: 2rem;
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
&.description {
|
.description {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
grid-column-start: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
button {
|
||||||
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
nav#main-menu {
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: rgba(0,0,0,.2);
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
|
||||||
|
@include media-max(small) {
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&.active, &:hover {
|
||||||
|
color: $yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,4 +31,21 @@ ul.reimbursement-list {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.meta {
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-column-end: 3;
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
border-top: 1px solid $item-border-color;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
p {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.6rem 0 1rem 0;
|
||||||
|
|
||||||
|
&.actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
h1 {
|
h1 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<main id="about">
|
||||||
|
<div id="aside">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<section class="text">
|
||||||
|
<h2>What is this?</h2>
|
||||||
|
<p>
|
||||||
|
You have found the contribution dashboard and budget management app of the
|
||||||
|
<a href="https://kosmos.org" target="_blank" rel="noreferrer noopener">Kosmos</a>
|
||||||
|
open-source co-operative.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We use this app to track what people contribute to all parts of our
|
||||||
|
projects (not just code), as well as for managing our community's budget.
|
||||||
|
</p>
|
||||||
|
<h2>Why?</h2>
|
||||||
|
<p>
|
||||||
|
We are trying out a new form of co-operative, native to the Internet,
|
||||||
|
creating and sharing digital resources the same way traditional
|
||||||
|
co-operatives share physical ones.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
By knowing roughly how much people contribute, we are able to reward
|
||||||
|
co-operative members who contribute their time with money contributed
|
||||||
|
by the ones who don't.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In addition to open-source grant payouts, all contributors can also propose
|
||||||
|
reimbursements for specific expenses they covered on behalf of the
|
||||||
|
community.
|
||||||
|
</p>
|
||||||
|
<h2>How?</h2>
|
||||||
|
<p>
|
||||||
|
Instead of notaries, lawyers, courts, or banks, we record data and manage
|
||||||
|
certain decisions on a decentralized ledger called
|
||||||
|
<a href="https://rootstock.io/" target="_blank" rel="noreferrer noopener">Rootstock</a>,
|
||||||
|
which is cryptographically tied to the bitcoin timechain.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
By doing this, we can ensure community control over the budget, as well as
|
||||||
|
full transparency and verifiability of everything that happens. It also
|
||||||
|
allows us to quickly, cheaply, and reliably send our open-source grants to
|
||||||
|
anyone who's contributing, no matter where they are on this beautiful
|
||||||
|
planet.
|
||||||
|
</p>
|
||||||
|
<h2>Kredits</h2>
|
||||||
|
<p>
|
||||||
|
All contributions are rewarded with so-called kredits. They are both
|
||||||
|
credits in the traditional sense of public attribution, as well as a
|
||||||
|
measure of how much and how regularly someone has added value to the
|
||||||
|
community and its products and services.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Kredits are considered for example for grant payouts, as well as
|
||||||
|
permissions for certain actions like vetos or votes. They may also be used
|
||||||
|
to access <a href="https://kosmos.org/services/">hosted services</a> for free,
|
||||||
|
or to unlock additional features on otherwise free services.
|
||||||
|
</p>
|
||||||
|
<h2>Getting started</h2>
|
||||||
|
<p>
|
||||||
|
We'd be delighted to welcome you as a new contributor!
|
||||||
|
If you'd like to start collecting kredits for your contributions, you can
|
||||||
|
<LinkTo @route="signup">create a contributor profile</LinkTo> now.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Next, you could learn more about
|
||||||
|
<a href="https://community.kosmos.org/t/how-kredits-for-contributions-are-proposed-and-confirmed/176">how kredits are proposed and issued</a>.
|
||||||
|
If you want to dive deeper into how this all works, head over to the
|
||||||
|
<a href="https://wiki.kosmos.org/Kredits">Kredits documentation</a>
|
||||||
|
page on our (soon to be phased out) wiki.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="empty">
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<header id="topbar">
|
<header id="topbar">
|
||||||
<h1><LinkTo @route="dashboard">Kosmos Kredits</LinkTo></h1>
|
<h1><LinkTo @route="dashboard">Kredits</LinkTo></h1>
|
||||||
<TopbarAccountPanel />
|
<TopbarAccountPanel />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<MainNavigationMenu />
|
||||||
|
|
||||||
{{outlet}}
|
{{outlet}}
|
||||||
|
|||||||
+11
-19
@@ -1,22 +1,6 @@
|
|||||||
<main id="budget">
|
<main id="budget">
|
||||||
|
|
||||||
<div id="aside">
|
<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">
|
<section id="funds">
|
||||||
<header>
|
<header>
|
||||||
<h2>Community funds</h2>
|
<h2>Community funds</h2>
|
||||||
@@ -33,7 +17,11 @@
|
|||||||
<header class="with-nav">
|
<header class="with-nav">
|
||||||
<h2>Proposed Reimbursements</h2>
|
<h2>Proposed Reimbursements</h2>
|
||||||
<nav>
|
<nav>
|
||||||
<LinkTo @route="reimbursements.new" @title="Submit a reimbursement" class="button small green">add</LinkTo>
|
<button type="button" class="button small green"
|
||||||
|
title="Submit a reimbursement"
|
||||||
|
{{on "click" this.addReimbursement}}>
|
||||||
|
add
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -45,9 +33,13 @@
|
|||||||
{{#if this.reimbursementsConfirmed}}
|
{{#if this.reimbursementsConfirmed}}
|
||||||
<section id="expenses-confirmed">
|
<section id="expenses-confirmed">
|
||||||
<header class="with-nav">
|
<header class="with-nav">
|
||||||
<h2>Confirmed Expenses</h2>
|
<h2>Confirmed Reimbursements</h2>
|
||||||
<nav>
|
<nav>
|
||||||
<LinkTo @route="reimbursements.new" @title="Submit a reimbursement" class="button small green">add</LinkTo>
|
<button type="button" class="button small green"
|
||||||
|
title="Submit a reimbursement"
|
||||||
|
{{on "click" this.addReimbursement}}>
|
||||||
|
add
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tag"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>
|
||||||
|
After Width: | Height: | Size: 355 B |
+31
-17
@@ -1,19 +1,3 @@
|
|||||||
{{#if this.showIntroText}}
|
|
||||||
<div id="intro" class={{if this.showDetailsPane "with-details"}}>
|
|
||||||
<h2>
|
|
||||||
Welcome to the contribution dashboard of the
|
|
||||||
<a href="https://kosmos.org" target="_blank" rel="noreferrer noopener">Kosmos</a> project!
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
If you want to learn more about what the numbers mean and how this works,
|
|
||||||
check out the
|
|
||||||
<a href="https://wiki.kosmos.org/Kredits" target="_blank" rel="noreferrer noopener">Kredits documentation</a>.
|
|
||||||
If you want to start earning kredits for your contributions,
|
|
||||||
<LinkTo @route="signup">create a contributor profile</LinkTo>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<main id="dashboard" class={{if this.showDetailsPane "with-details"}}>
|
<main id="dashboard" class={{if this.showDetailsPane "with-details"}}>
|
||||||
|
|
||||||
<div id="stats">
|
<div id="stats">
|
||||||
@@ -27,7 +11,7 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</header>
|
</header>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ContributorList @contributorList={{this.kreditsToplist}}
|
<ContributorList @contributorList={{this.kreditsToplistSorted}}
|
||||||
@showUnconfirmedKredits={{this.showUnconfirmedKredits}}
|
@showUnconfirmedKredits={{this.showUnconfirmedKredits}}
|
||||||
@selectedContributorId={{this.selectedContributorId}}
|
@selectedContributorId={{this.selectedContributorId}}
|
||||||
@loading={{this.kredits.syncContributors.isRunning}} />
|
@loading={{this.kredits.syncContributors.isRunning}} />
|
||||||
@@ -45,6 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{#if this.contributionsConfirmed}}
|
||||||
<section id="contributions-by-type">
|
<section id="contributions-by-type">
|
||||||
<header>
|
<header>
|
||||||
<h2>Contributions by type</h2>
|
<h2>Contributions by type</h2>
|
||||||
@@ -53,6 +38,7 @@
|
|||||||
<ChartContributionsByType @contributions={{this.contributions}} />
|
<ChartContributionsByType @contributions={{this.contributions}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="contributions">
|
<div id="contributions">
|
||||||
@@ -87,6 +73,7 @@
|
|||||||
</section>
|
</section>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.contributionsConfirmed}}
|
||||||
<section id="contributions-confirmed">
|
<section id="contributions-confirmed">
|
||||||
<header class="with-nav">
|
<header class="with-nav">
|
||||||
<h2>Confirmed Contributions</h2>
|
<h2>Confirmed Contributions</h2>
|
||||||
@@ -105,6 +92,33 @@
|
|||||||
@showQuickFilter={{this.showQuickFilterConfirmed}} />
|
@showQuickFilter={{this.showQuickFilterConfirmed}} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.showFullContributionSync}}
|
||||||
|
<section id="sync-all-contributions">
|
||||||
|
{{#if this.kredits.fetchMissingContributions.isIdle}}
|
||||||
|
<p class="mb-4">
|
||||||
|
There are
|
||||||
|
<strong>{{this.kredits.missingHistoricContributionsCount}}</strong>
|
||||||
|
earlier contributions, which are not currently loaded/displayed.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can fetch all historic data in one go, and have it stored locally in
|
||||||
|
your browser:
|
||||||
|
<button type="button" {{on "click" (perform this.kredits.fetchMissingContributions)}} class="small">
|
||||||
|
fetch all data
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="mb-4">
|
||||||
|
Syncing data. Please be patient...
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
(You can leave this website anytime and sync missing data when you come back.)
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="details">
|
<div id="details">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<p>
|
<p>
|
||||||
<a href="https://rinkeby.etherscan.io/address/{{this.model.account}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect Ethereum transactions</a>
|
<a href="https://explorer.testnet.rsk.co/address/{{this.model.account}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect Rootstock transactions</a>
|
||||||
{{#if this.model.ipfsHash}}
|
{{#if this.model.ipfsHash}}
|
||||||
<a href="{{this.ipfsGatewayUrl}}/{{this.model.ipfsHash}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect IPFS profile</a>
|
<a href="{{this.ipfsGatewayUrl}}/{{this.model.ipfsHash}}" class="button small" target="_blank" rel="noopener noreferrer">Inspect IPFS profile</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<header>
|
||||||
|
<h2>Complete your contributor profile</h2>
|
||||||
|
</header>
|
||||||
|
<div class="content text-lg">
|
||||||
|
<p class="mb-8">
|
||||||
|
Kredits allow you to to earn rewards for your contributions, in the form of
|
||||||
|
dynamic open-source grants. As a regular contributor, you can also take
|
||||||
|
part in the community's project governance and finances.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In order to interact with the system you will need a
|
||||||
|
<a href="https://rootstock.io" target="_blank" rel="noopener noreferrer">Rootstock</a>
|
||||||
|
wallet/account.
|
||||||
|
</p>
|
||||||
|
<form>
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
Rootstock address:<br>
|
||||||
|
<Input @type="text" @value={{this.accountAddress}} placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" class={{if this.isValidEthAccount "valid" ""}} />
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
<p class="actions">
|
||||||
|
<button {{on "click" (fn this.completeSignup)}}
|
||||||
|
disabled={{this.signupButtonDisabled}}
|
||||||
|
type="button">
|
||||||
|
Complete my profile
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Why not say hi to your fellow contributors
|
Why not say hi to your fellow contributors
|
||||||
<a href="https://wiki.kosmos.org/Main_Page#Community_.2F_Getting_in_touch_.2F_Getting_involved">in one of our chat rooms</a>?.
|
<a href="https://wiki.kosmos.org/Main_Page#Community_.2F_Getting_in_touch_.2F_Getting_involved"
|
||||||
|
target="_blank" rel="noopener noreferrer">in one of our chat rooms</a>?.
|
||||||
</p>
|
</p>
|
||||||
<p class="actions">
|
<p class="actions">
|
||||||
<LinkTo @route="dashboard" class="button small">Return to dashboard</LinkTo>
|
<LinkTo @route="dashboard" class="button small">Return to dashboard</LinkTo>
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
<header>
|
|
||||||
<h2>Complete your contributor profile</h2>
|
|
||||||
</header>
|
|
||||||
<div class="content text-lg">
|
|
||||||
<p class="mg-bottom-md">
|
|
||||||
Kredits allow you to take part in project governance, and to earn rewards for
|
|
||||||
your contributions. For both, you will need an Ethereum wallet/account.
|
|
||||||
</p>
|
|
||||||
<form>
|
|
||||||
<p>
|
|
||||||
<label>
|
|
||||||
Ethereum account:<br>
|
|
||||||
<Input @type="text" @value={{this.ethAddress}} placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4" class={{if this.isValidEthAccount "valid" ""}} />
|
|
||||||
</label>
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
<p class="actions">
|
|
||||||
<button disabled={{this.signupButtonDisabled}}
|
|
||||||
onclick={{action "completeSignup"}}
|
|
||||||
type="button">
|
|
||||||
Complete my profile
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export function floatToBtc(number) {
|
||||||
|
return Number(number.toFixed(8))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function btcToSats(btc) {
|
||||||
|
return Math.round(btc * 100_000_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function satsToBtc(sats) {
|
||||||
|
return Math.round((sats / 100_000_000) * 100_000_000) / 100_000_000;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default function() {
|
||||||
|
return (navigator.languages && navigator.languages.length) ?
|
||||||
|
navigator.languages[0] :
|
||||||
|
navigator.language;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export default function(value) {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
return value === today;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import config from 'kredits-web/config/environment';
|
||||||
|
|
||||||
|
export default async function () {
|
||||||
|
const networkName = config.web3NetworkName;
|
||||||
|
const chainId = config.web3ChainId;
|
||||||
|
const chainIdHex = `0x${Number(chainId).toString(16)}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_switchEthereumChain',
|
||||||
|
params: [{ chainId: chainIdHex }]
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// This error code indicates that the chain has not been added to MetaMask
|
||||||
|
if (err.code === 4902) {
|
||||||
|
await window.ethereum.request({
|
||||||
|
method: 'wallet_addEthereumChain',
|
||||||
|
params: [{
|
||||||
|
chainId: chainIdHex,
|
||||||
|
chainName: networkName,
|
||||||
|
rpcUrls: [ config.web3ProviderUrl ],
|
||||||
|
nativeCurrency: { name: 'tRBTC', symbol: 'tRBTC', decimals: 18 }
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to switch chains:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
-8
@@ -26,11 +26,11 @@ module.exports = function(environment) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
web3ProviderUrl: 'https://rsk-testnet.kosmos.org',
|
web3ProviderUrl: 'https://rsk-testnet.kosmos.org',
|
||||||
web3RequiredChainId: 31,
|
web3ChainId: 31,
|
||||||
web3RequiredNetworkName: 'RSK Testnet',
|
web3NetworkName: 'RSK Testnet',
|
||||||
|
|
||||||
githubConnectUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/connect/github',
|
githubConnectUrl: 'https://hal8000.kosmos.chat/kredits/signup/connect/github',
|
||||||
githubSignupUrl: 'https://hal8000.chat.kosmos.org/kredits/signup/github',
|
githubSignupUrl: 'https://hal8000.kosmos.chat/kredits/signup/github',
|
||||||
|
|
||||||
ipfs: {
|
ipfs: {
|
||||||
host: 'ipfs.kosmos.org',
|
host: 'ipfs.kosmos.org',
|
||||||
@@ -45,7 +45,22 @@ module.exports = function(environment) {
|
|||||||
'BTC': '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'
|
'BTC': '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599'
|
||||||
},
|
},
|
||||||
|
|
||||||
btcBalanceAPI: 'https://api.kosmos.org/kredits/onchain_btc_balance',
|
communityFundsAPI: {
|
||||||
|
balances: {
|
||||||
|
onchain: {
|
||||||
|
icon: 'icon-btc.png',
|
||||||
|
symbol: 'BTC',
|
||||||
|
description: 'BTC on chain',
|
||||||
|
url: 'https://api.kosmos.org/btcpay/onchain_btc_balance'
|
||||||
|
},
|
||||||
|
lightning: {
|
||||||
|
icon: 'icon-btc-lightning.png',
|
||||||
|
symbol: 'BTC',
|
||||||
|
description: 'BTC on Lightning Network',
|
||||||
|
url: 'https://api.kosmos.org/btcpay/lightning_btc_balance'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
corsProxy: 'https://cors.5apps.com/?uri='
|
corsProxy: 'https://cors.5apps.com/?uri='
|
||||||
};
|
};
|
||||||
@@ -61,11 +76,15 @@ module.exports = function(environment) {
|
|||||||
ENV.githubSignupUrl = 'http://localhost:8888/kredits/signup/github';
|
ENV.githubSignupUrl = 'http://localhost:8888/kredits/signup/github';
|
||||||
|
|
||||||
ENV.ipfs = {
|
ENV.ipfs = {
|
||||||
host: 'localhost',
|
host: '10.1.1.198',
|
||||||
port: '5001',
|
port: '5001',
|
||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
gatewayUrl: 'http://localhost:8080/ipfs'
|
gatewayUrl: 'http://10.1.1.198:9090/ipfs'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Uncomment if you want to use local akkounts
|
||||||
|
// ENV.communityFundsAPI.balances.onchain.url = 'http://localhost:3000/api/btcpay/onchain_btc_balance';
|
||||||
|
// ENV.communityFundsAPI.balances.lightning.url = 'http://localhost:3000/api/btcpay/lightning_btc_balance';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (environment === 'test') {
|
if (environment === 'test') {
|
||||||
@@ -82,7 +101,12 @@ 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.web3RequiredChainId = null;
|
}
|
||||||
|
if (process.env.WEB3_CHAIN_ID) {
|
||||||
|
ENV.web3ChainId = parseInt(process.env.WEB3_CHAIN_ID);
|
||||||
|
}
|
||||||
|
if (process.env.WEB3_NETWORK_NAME) {
|
||||||
|
ENV.web3NetworkName = process.env.WEB3_NETWORK_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ENV;
|
return ENV;
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ module.exports = function(defaults) {
|
|||||||
path: "empty" // needed for kosmos-schemas dependency
|
path: "empty" // needed for kosmos-schemas dependency
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
fingerprint: {
|
||||||
|
exclude: [ 'img/icon-btc' ]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Generated
+29
-19
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "kredits-web",
|
"name": "kredits-web",
|
||||||
"version": "2.0.0-beta.2",
|
"version": "2.3.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "kredits-web",
|
"name": "kredits-web",
|
||||||
"version": "2.0.0-beta.2",
|
"version": "2.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.19.1",
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"@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",
|
||||||
"@kredits/contracts": "^7.0.1",
|
"@kredits/contracts": "^7.5.0",
|
||||||
"babel-preset-es2015": "^6.22.0",
|
"babel-preset-es2015": "^6.22.0",
|
||||||
"broccoli-asset-rev": "^3.0.0",
|
"broccoli-asset-rev": "^3.0.0",
|
||||||
"ember-auto-import": "^1.12.1",
|
"ember-auto-import": "^1.12.1",
|
||||||
@@ -3062,17 +3062,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@kosmos/schemas": {
|
"node_modules/@kosmos/schemas": {
|
||||||
"version": "3.1.0",
|
"version": "3.2.0",
|
||||||
"dev": true,
|
"resolved": "https://registry.npmjs.org/@kosmos/schemas/-/schemas-3.2.0.tgz",
|
||||||
"license": "MIT"
|
"integrity": "sha512-GyWXmWcATcakmEdPOpafdckBgWFqcpSh+tB8nm11VGgtgA94pfoFhKZ3t88WELItKfuP/GFHgMiZmdMB3bSkdQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@kredits/contracts": {
|
"node_modules/@kredits/contracts": {
|
||||||
"version": "7.0.1",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.5.0.tgz",
|
||||||
"integrity": "sha512-4IhNQoPc1BCT63NKyDOmV+ryb82FKNbIqfDyTdant6WTI9lfkIAnI8Et+ynQmqxx3Efez5hqoDZtEhCijcuIng==",
|
"integrity": "sha512-4Jw6lXn9DfZiTnMtWHwGLS5zhxEJZ2ANfAgcr7tZxqRl7Y0ijpoWvPCA+LkSGqRgKspyiw9+7vSVTtyvAyQbPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kosmos/schemas": "^3.1.0",
|
"@kosmos/schemas": "^3.2.0",
|
||||||
"ethers": "^5.4.7",
|
"ethers": "^5.4.7",
|
||||||
"ipfs-http-client": "^56.0.3",
|
"ipfs-http-client": "^56.0.3",
|
||||||
"multihashes": "^4.0.3",
|
"multihashes": "^4.0.3",
|
||||||
@@ -6816,7 +6817,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001332",
|
"version": "1.0.30001599",
|
||||||
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
|
||||||
|
"integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -6826,9 +6829,12 @@
|
|||||||
{
|
{
|
||||||
"type": "tidelift",
|
"type": "tidelift",
|
||||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"license": "CC-BY-4.0"
|
|
||||||
},
|
},
|
||||||
"node_modules/capture-exit": {
|
"node_modules/capture-exit": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -26062,16 +26068,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@kosmos/schemas": {
|
"@kosmos/schemas": {
|
||||||
"version": "3.1.0",
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kosmos/schemas/-/schemas-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-GyWXmWcATcakmEdPOpafdckBgWFqcpSh+tB8nm11VGgtgA94pfoFhKZ3t88WELItKfuP/GFHgMiZmdMB3bSkdQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@kredits/contracts": {
|
"@kredits/contracts": {
|
||||||
"version": "7.0.1",
|
"version": "7.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@kredits/contracts/-/contracts-7.5.0.tgz",
|
||||||
"integrity": "sha512-4IhNQoPc1BCT63NKyDOmV+ryb82FKNbIqfDyTdant6WTI9lfkIAnI8Et+ynQmqxx3Efez5hqoDZtEhCijcuIng==",
|
"integrity": "sha512-4Jw6lXn9DfZiTnMtWHwGLS5zhxEJZ2ANfAgcr7tZxqRl7Y0ijpoWvPCA+LkSGqRgKspyiw9+7vSVTtyvAyQbPQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@kosmos/schemas": "^3.1.0",
|
"@kosmos/schemas": "^3.2.0",
|
||||||
"ethers": "^5.4.7",
|
"ethers": "^5.4.7",
|
||||||
"ipfs-http-client": "^56.0.3",
|
"ipfs-http-client": "^56.0.3",
|
||||||
"multihashes": "^4.0.3",
|
"multihashes": "^4.0.3",
|
||||||
@@ -29050,7 +29058,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001332",
|
"version": "1.0.30001599",
|
||||||
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
|
||||||
|
"integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"capture-exit": {
|
"capture-exit": {
|
||||||
|
|||||||
+3
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kredits-web",
|
"name": "kredits-web",
|
||||||
"version": "2.0.0-beta.2",
|
"version": "2.3.0",
|
||||||
"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,12 +17,11 @@
|
|||||||
"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:8545 ember serve",
|
"start:local": "WEB3_PROVIDER_URL=http://localhost:8545 WEB3_CHAIN_ID=1337 WEB3_NETWORK_NAME='Hardhat Devchain' 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",
|
||||||
"version": "npm run build-prod && git add release/",
|
"version": "npm run build-prod && git add release/",
|
||||||
"postversion": "git push origin master",
|
|
||||||
"deploy": "git push 5apps master"
|
"deploy": "git push 5apps master"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -31,7 +30,7 @@
|
|||||||
"@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",
|
||||||
"@kredits/contracts": "^7.0.1",
|
"@kredits/contracts": "^7.5.0",
|
||||||
"babel-preset-es2015": "^6.22.0",
|
"babel-preset-es2015": "^6.22.0",
|
||||||
"broccoli-asset-rev": "^3.0.0",
|
"broccoli-asset-rev": "^3.0.0",
|
||||||
"ember-auto-import": "^1.12.1",
|
"ember-auto-import": "^1.12.1",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
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
+569
-310
File diff suppressed because one or more lines are too long
+9767
-5870
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.0 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%222.0.0-beta.2%2B62b618b6%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" />
|
<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.3.0%2B66e1adc7%22%7D%2C%22web3ProviderUrl%22%3A%22https%3A%2F%2Frsk-testnet.kosmos.org%22%2C%22web3ChainId%22%3A31%2C%22web3NetworkName%22%3A%22RSK%20Testnet%22%2C%22githubConnectUrl%22%3A%22https%3A%2F%2Fhal8000.kosmos.chat%2Fkredits%2Fsignup%2Fconnect%2Fgithub%22%2C%22githubSignupUrl%22%3A%22https%3A%2F%2Fhal8000.kosmos.chat%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%22communityFundsAPI%22%3A%7B%22balances%22%3A%7B%22onchain%22%3A%7B%22icon%22%3A%22icon-btc.png%22%2C%22symbol%22%3A%22BTC%22%2C%22description%22%3A%22BTC%20on%20chain%22%2C%22url%22%3A%22https%3A%2F%2Fapi.kosmos.org%2Fbtcpay%2Fonchain_btc_balance%22%7D%2C%22lightning%22%3A%7B%22icon%22%3A%22icon-btc-lightning.png%22%2C%22symbol%22%3A%22BTC%22%2C%22description%22%3A%22BTC%20on%20Lightning%20Network%22%2C%22url%22%3A%22https%3A%2F%2Fapi.kosmos.org%2Fbtcpay%2Flightning_btc_balance%22%7D%7D%7D%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-429511daa3257050f23812f202a534ee.css">
|
<link integrity="" rel="stylesheet" href="/assets/kredits-web-273a3e2096140c611fc6c314653c5122.css">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
||||||
<script src="/assets/vendor-4ee536ded971436a83d35253df25f2cb.js" integrity="sha256-yb+pC9iuZ8WcQwk9VuDNhSlWguL7BEABsEOEhBpwUC0= sha512-50LApx5XqeKHu/FMaljzqhAMPgkqSiy2rDhUcViYBcXWBBYmilFirup/iWvLiNCdfr5ct9wk7+tU9iShdSgDww==" ></script>
|
<script src="/assets/vendor-fe68375ecc84328a68fb6e2dc6c2ffd0.js" integrity="sha256-R9zoGCHpj2nktDrCaA5bsKl5UIzBz3pi8OxcjZUcUXg= sha512-KgK1Q5VBzvVFUa5DEMaexc2VLg337DOyWnvp/3HvNzcjSFwCRG/dCn3x6EMuxULl1lY/rCVJ2ZgZN0K5NwZDkA==" ></script>
|
||||||
<script src="/assets/kredits-web-0a433c90d05ea3903308867e0d178f7f.js" integrity="sha256-kU8qbqT24o1tZj4xdYJzarfPFSfEmZr3JLu0g8pAmqk= sha512-gD6YmV3xDnwHJ2WU6yXNkUEiPScjdfM2S5eYwCrsfcsI4iJUG81z7IjUbwzstuZocKkCS5zt8Y84Hh84SdhSuw==" ></script>
|
<script src="/assets/kredits-web-ff25bacd2246c053f7629aa0ac30852d.js" integrity="sha256-ZMtyPPTI2FdCZOW2AhZKoSc3V1F8p7eb5paSnXfUUVE= sha512-lbKnfjHisBjgh4AVaSvDb+7zrtfrMd9PhVj3kUW+UREmA9kl3qLaWECHtIfWz4VNPfzQV9xg5P9hQFmcrPe+ig==" ></script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,13 +1,58 @@
|
|||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
import { render } from '@ember/test-helpers';
|
import { click, fillIn, render } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
import contributors from '../../../fixtures/contributors';
|
||||||
|
|
||||||
module('Integration | Component | add-reimbursement', function(hooks) {
|
module('Integration | Component | add-reimbursement', function(hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
test('it works', async function(assert) {
|
hooks.beforeEach(function() {
|
||||||
|
let kredits = this.owner.lookup('service:kredits');
|
||||||
|
kredits.set('contributors', contributors);
|
||||||
|
kredits.set('currentUser', contributors.findBy('id', 3));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Contributor select menu', async function(assert) {
|
||||||
await render(hbs`<AddReimbursement />`);
|
await render(hbs`<AddReimbursement />`);
|
||||||
assert.ok(true);
|
|
||||||
|
assert.equal(this.element.querySelectorAll('select#contributor option').length, contributors.length,
|
||||||
|
'contains correct amount of items');
|
||||||
|
|
||||||
|
assert.equal(this.element.querySelector('select#contributor option:checked').value, "3",
|
||||||
|
'preselects the connected contributor account');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Adding expense items', async function(assert) {
|
||||||
|
await render(hbs`<AddReimbursement />`);
|
||||||
|
|
||||||
|
assert.dom(this.element).includesText('New expense item');
|
||||||
|
|
||||||
|
await fillIn('form input[name="expense-amount"]', '49');
|
||||||
|
await fillIn('form input[name="expense-title"]', 'Domain kosmos.org (yearly fee)');
|
||||||
|
await click('form#add-expense-item input[type="submit"]');
|
||||||
|
|
||||||
|
assert.equal(this.element.querySelector('input[name="total-eur"]').value, '49',
|
||||||
|
'updates the total EUR amount');
|
||||||
|
assert.equal(this.element.querySelector('input[name="total-usd"]').value, '0',
|
||||||
|
'does not update the total USD amount');
|
||||||
|
assert.equal(this.element.querySelector('input[name="total-btc"]').value, '0.00534493',
|
||||||
|
'updates the total BTC amount');
|
||||||
|
|
||||||
|
assert.dom(this.element).doesNotIncludeText('New expense item');
|
||||||
|
|
||||||
|
await click('button#add-another-item');
|
||||||
|
|
||||||
|
await fillIn('form input[name="expense-amount"]', '29');
|
||||||
|
await fillIn('select[name="expense-currency"]', 'USD');
|
||||||
|
await fillIn('form input[name="expense-title"]', 'Domain kosmos.social (yearly fee)');
|
||||||
|
await click('form#add-expense-item input[type="submit"]');
|
||||||
|
|
||||||
|
assert.equal(this.element.querySelector('input[name="total-usd"]').value, '29',
|
||||||
|
'updates the total USD amount');
|
||||||
|
assert.equal(this.element.querySelector('input[name="total-eur"]').value, '49',
|
||||||
|
'does not update the total EUR amount');
|
||||||
|
assert.equal(this.element.querySelector('input[name="total-btc"]').value, '0.00804268',
|
||||||
|
'updates the total BTC amount');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,37 +3,90 @@ import { setupRenderingTest } from 'ember-qunit';
|
|||||||
import { render } from '@ember/test-helpers';
|
import { render } from '@ember/test-helpers';
|
||||||
import hbs from 'htmlbars-inline-precompile';
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
|
||||||
|
function stubWalletApi () {
|
||||||
|
window.ethereum = {
|
||||||
|
on: function() { return true; },
|
||||||
|
request: function() { return Promise.resolve(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module('Integration | Component | topbar-account-panel', function(hooks) {
|
module('Integration | Component | topbar-account-panel', function(hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
test('unknown user without wallet (or no permission to get wallet/account info)', async function(assert) {
|
test('Unknown user without wallet', async function(assert) {
|
||||||
await render(hbs`<TopbarAccountPanel />`);
|
await render(hbs`<TopbarAccountPanel />`);
|
||||||
|
|
||||||
assert.ok(this.element.textContent.trim().match(/^Anonymous/));
|
assert.ok(
|
||||||
|
this.element.textContent.trim().match(/^Anonymous/),
|
||||||
|
'shows current user as anonymous'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#signup').length, 1,
|
||||||
|
'signup button is visible'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#connect').length, 0,
|
||||||
|
'connect button is not visible'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('unknown user with Ethereum wallet', async function(assert) {
|
test('Unknown user with disconnected wallet', async function(assert) {
|
||||||
|
stubWalletApi();
|
||||||
|
let service = this.owner.lookup('service:kredits');
|
||||||
|
service.set('currentUserAccounts', []);
|
||||||
|
await render(hbs`<TopbarAccountPanel />`);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
this.element.textContent.trim().match(/^Anonymous/),
|
||||||
|
'shows current user as anonymous'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#signup').length, 1,
|
||||||
|
'signup button is visible'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#connect').length, 1,
|
||||||
|
'connect button is visible'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unknown user with connected wallet', async function(assert) {
|
||||||
|
stubWalletApi();
|
||||||
let service = this.owner.lookup('service:kredits');
|
let service = this.owner.lookup('service:kredits');
|
||||||
service.set('currentUserAccounts', [{ foo: 'bar' }]);
|
service.set('currentUserAccounts', [{ foo: 'bar' }]);
|
||||||
await render(hbs`<TopbarAccountPanel />`);
|
await render(hbs`<TopbarAccountPanel />`);
|
||||||
|
|
||||||
assert.ok(this.element.textContent.trim().match(/^Anonymous/));
|
assert.ok(
|
||||||
|
this.element.textContent.trim().match(/^Anonymous/),
|
||||||
|
'shows current user as anonymous'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#signup').length, 1,
|
||||||
|
'signup button is visible'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#connect').length, 0,
|
||||||
|
'connect button is not visible'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('known contributor', async function(assert) {
|
test('Known contributor', async function(assert) {
|
||||||
|
stubWalletApi();
|
||||||
let service = this.owner.lookup('service:kredits');
|
let service = this.owner.lookup('service:kredits');
|
||||||
service.set('currentUserAccounts', [{ foo: 'bar' }]);
|
service.set('currentUserAccounts', [{ foo: 'bar' }]);
|
||||||
service.set('currentUser', {
|
service.set('currentUser', { name: 'Dorian Nakamoto', isCore: false });
|
||||||
name: 'Dorian Nakamoto',
|
|
||||||
isCore: false
|
|
||||||
});
|
|
||||||
await render(hbs`<TopbarAccountPanel />`);
|
await render(hbs`<TopbarAccountPanel />`);
|
||||||
|
|
||||||
assert.dom(this.element).hasText('Dorian Nakamoto');
|
assert.dom(this.element).hasText(
|
||||||
|
'Dorian Nakamoto', 'shows current user\'s name'
|
||||||
service.set('currentUser.isCore', true);
|
);
|
||||||
await render(hbs`<TopbarAccountPanel />`);
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#signup').length, 0,
|
||||||
assert.equal(this.element.querySelectorAll('span.core-flag').length, 1);
|
'signup button is not visible'
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
this.element.querySelectorAll('button#connect').length, 0,
|
||||||
|
'connect button is not visible'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,5 +18,12 @@ module('Integration | Helper | fmt-fiat-currency', function(hooks) {
|
|||||||
await render(hbs`{{fmt-fiat-currency this.amount 'USD'}}`);
|
await render(hbs`{{fmt-fiat-currency this.amount 'USD'}}`);
|
||||||
assert.ok(this.element.textContent.trim().match(/USD/),
|
assert.ok(this.element.textContent.trim().match(/USD/),
|
||||||
'using defined currency when given');
|
'using defined currency when given');
|
||||||
|
|
||||||
|
await render(hbs`{{fmt-fiat-currency 0.00123 'BTC'}}`);
|
||||||
|
assert.ok(this.element.textContent.trim().match(/0.00123/),
|
||||||
|
'allows more decimals');
|
||||||
|
assert.ok(this.element.textContent.trim().match(/BTC/),
|
||||||
|
'using defined currency when given');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,12 +39,4 @@ module('Unit | Component | add-reimbursement', function(hooks) {
|
|||||||
assert.equal(component.totalEUR, '71');
|
assert.equal(component.totalEUR, '71');
|
||||||
assert.equal(component.totalUSD, '59');
|
assert.equal(component.totalUSD, '59');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('#updateTotalAmountFromFiat', async function(assert) {
|
|
||||||
let component = createComponent('component:add-reimbursement');
|
|
||||||
component.expenses = expenses;
|
|
||||||
await component.exchangeRates.fetchRates();
|
|
||||||
component.updateTotalAmountFromFiat();
|
|
||||||
assert.equal(component.total, '0.01323322', 'converts EUR and USD totals to BTC and rounds to 8 decimals');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupTest } from 'ember-qunit';
|
||||||
|
import createComponent from 'kredits-web/tests/helpers/create-component';
|
||||||
|
// import moment from 'moment';
|
||||||
|
|
||||||
|
module('Unit | Component | confirmed-in', function(hooks) {
|
||||||
|
setupTest(hooks);
|
||||||
|
|
||||||
|
test('it exists', function(assert) {
|
||||||
|
let component = createComponent('component:confirmed-in');
|
||||||
|
assert.ok(component);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#confirmedInBlocks', function(assert) {
|
||||||
|
const kredits = this.owner.lookup('service:kredits');
|
||||||
|
kredits.set('currentBlock', 419550);
|
||||||
|
let component = createComponent('component:confirmed-in');
|
||||||
|
component.args.confirmedAtBlock = 420000;
|
||||||
|
|
||||||
|
assert.equal(component.confirmedInBlocks, 450);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#confirmedInSeconds', function(assert) {
|
||||||
|
const kredits = this.owner.lookup('service:kredits');
|
||||||
|
kredits.set('currentBlock', 419550);
|
||||||
|
let component = createComponent('component:confirmed-in');
|
||||||
|
component.args.confirmedAtBlock = 420000;
|
||||||
|
|
||||||
|
assert.equal(component.confirmedInSeconds, 13500);
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#confirmedInHumanTime', function(assert) {
|
||||||
|
const kredits = this.owner.lookup('service:kredits');
|
||||||
|
kredits.set('currentBlock', 419550);
|
||||||
|
let component = createComponent('component:confirmed-in');
|
||||||
|
component.args.confirmedAtBlock = 420000;
|
||||||
|
|
||||||
|
assert.equal(component.confirmedInHumanTime, '4 hours');
|
||||||
|
})
|
||||||
|
|
||||||
|
test('#isConfirmed', function(assert) {
|
||||||
|
const kredits = this.owner.lookup('service:kredits');
|
||||||
|
kredits.set('currentBlock', 430000);
|
||||||
|
let component = createComponent('component:confirmed-in');
|
||||||
|
component.args.confirmedAtBlock = 420000;
|
||||||
|
|
||||||
|
assert.ok(component.isConfirmed);
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -1,29 +1,31 @@
|
|||||||
// import { isEmpty, isPresent } from '@ember/utils';
|
import { isEmpty } from '@ember/utils';
|
||||||
// import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
// import { setupTest } from 'ember-qunit';
|
import { setupTest } from 'ember-qunit';
|
||||||
// import Contributor from 'kredits-web/models/contributor';
|
import Contributor from 'kredits-web/models/contributor';
|
||||||
//
|
|
||||||
// module('Unit | Controller | index', function(hooks) {
|
module('Unit | Controller | Dashboard', function(hooks) {
|
||||||
// setupTest(hooks);
|
setupTest(hooks);
|
||||||
//
|
|
||||||
// let addFixtures = function(controller) {
|
let addFixtures = function(controller) {
|
||||||
// [
|
[
|
||||||
// { github_username: "neo", github_uid: "318", totalKreditsEarned: 10000 },
|
{ github_username: "neo", github_uid: "318", totalKreditsEarned: 10000 },
|
||||||
// { github_username: "morpheus", github_uid: "843", totalKreditsEarned: 15000 },
|
{ github_username: "morpheus", github_uid: "843", totalKreditsEarned: 15000 },
|
||||||
// { github_username: "trinity", github_uid: "123", totalKreditsEarned: 5000 },
|
{ github_username: "trinity", github_uid: "123", totalKreditsEarned: 5000 },
|
||||||
// { github_username: "mouse", github_uid: "696", totalKreditsEarned: 0 }
|
{ github_username: "mouse", github_uid: "696", totalKreditsEarned: 0 }
|
||||||
// ].forEach(fixture => {
|
].forEach(fixture => {
|
||||||
// controller.get('kredits.contributors').push(Contributor.create(fixture));
|
controller.get('kredits.contributors').push(Contributor.create(fixture));
|
||||||
// });
|
});
|
||||||
// };
|
};
|
||||||
//
|
|
||||||
// test('doesn\'t contain people with 0 balance', function(assert) {
|
test('kreditsToplistSorted()', function(assert) {
|
||||||
// let controller = this.owner.lookup('controller:index');
|
const controller = this.owner.lookup('controller:dashboard');
|
||||||
// addFixtures(controller);
|
addFixtures(controller);
|
||||||
//
|
|
||||||
// let contributorsSorted = controller.get('contributorsSorted');
|
const kreditsToplistSorted = controller.get('kreditsToplistSorted');
|
||||||
//
|
|
||||||
// assert.ok(isPresent(contributorsSorted.findBy('github_username', 'neo')));
|
assert.equal(kreditsToplistSorted.length, 3,
|
||||||
// assert.ok(isEmpty(contributorsSorted.findBy('github_username', 'mouse')));
|
'contains all contributors with kredits');
|
||||||
// });
|
assert.ok(isEmpty(kreditsToplistSorted.findBy('github_username', 'mouse')),
|
||||||
// });
|
'does not contain contributors with 0 kredits');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupTest } from 'ember-qunit';
|
import { setupTest } from 'ember-qunit';
|
||||||
|
|
||||||
module('Unit | Route | signup/eth-account', function(hooks) {
|
module('Unit | Route | signup/account', function(hooks) {
|
||||||
setupTest(hooks);
|
setupTest(hooks);
|
||||||
|
|
||||||
test('it exists', function(assert) {
|
test('it exists', function(assert) {
|
||||||
let route = this.owner.lookup('route:signup/eth-account');
|
let route = this.owner.lookup('route:signup/account');
|
||||||
assert.ok(route);
|
assert.ok(route);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -7,7 +7,7 @@ module('Unit | Service | exchange-rates', function(hooks) {
|
|||||||
test('#fetchRates', async function(assert) {
|
test('#fetchRates', async function(assert) {
|
||||||
let service = this.owner.lookup('service:exchange-rates');
|
let service = this.owner.lookup('service:exchange-rates');
|
||||||
await service.fetchRates();
|
await service.fetchRates();
|
||||||
assert.equal(service.btceur, 9167.57, 'fetches BTCEUR from Bitstamp');
|
assert.equal(service.EUR, 9167.57, 'fetches BTCEUR from Bitstamp');
|
||||||
assert.equal(service.btcusd, 10749.70, 'fetches BTCUSD from Bitstamp');
|
assert.equal(service.USD, 10749.70, 'fetches BTCUSD from Bitstamp');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { floatToBtc, btcToSats, satsToBtc } from 'kredits-web/utils/btc-conversions';
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
|
||||||
|
module('Unit | Utility | btc-conversions', function() {
|
||||||
|
test('floatToBtc', function(assert) {
|
||||||
|
assert.equal(floatToBtc(0.001429007), 0.00142901);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('btcToSats', function(assert) {
|
||||||
|
assert.equal(btcToSats(0.001429007), 142901);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('satsToBtc', function(assert) {
|
||||||
|
assert.equal(satsToBtc(142901), 0.00142901);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user