Merge pull request #210 from 67P/feature/confirmation-eta
Add confirmed-in component, show ETA for proposed reimbursements
This commit was merged in pull request #210.
This commit is contained in:
@@ -44,26 +44,10 @@
|
|||||||
|
|
||||||
<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>
|
class="green small" type="button">+ Add another item</button>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Confirming in <strong>{{this.confirmedInBlocks}}</strong> blocks (~ {{this.confirmedInHumanTime}})
|
||||||
@@ -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">
|
||||||
|
{{fmt-fiat-currency expense.amount expense.currency}}
|
||||||
|
</div>
|
||||||
|
<p class="description">
|
||||||
|
{{expense.description}}
|
||||||
|
</p>
|
||||||
|
<p class="tags">
|
||||||
|
{{#each expense.tags as |tag|}}
|
||||||
|
<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>
|
||||||
@@ -3,44 +3,36 @@
|
|||||||
<li data-reimbursement-id={{reimbursement.id}}
|
<li data-reimbursement-id={{reimbursement.id}}
|
||||||
class="{{item-status reimbursement}}">
|
class="{{item-status reimbursement}}">
|
||||||
<p class="meta">
|
<p class="meta">
|
||||||
<span class="title">
|
|
||||||
Expenses covered by {{reimbursement.contributor.name}}
|
|
||||||
</span>
|
|
||||||
<span class="recipient">
|
<span class="recipient">
|
||||||
<UserAvatar @contributor={{reimbursement.contributor}} />
|
<UserAvatar @contributor={{reimbursement.contributor}} />
|
||||||
</span>
|
</span>
|
||||||
|
<span class="title">
|
||||||
|
Expenses covered by {{reimbursement.contributor.name}}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="token-amount">
|
<p class="token-amount">
|
||||||
<span class="amount">
|
<span class="amount">
|
||||||
{{sats-to-btc reimbursement.amount}}</span> <span class="symbol">BTC</span>
|
{{sats-to-btc reimbursement.amount}}</span> <span class="symbol">BTC</span>
|
||||||
</p>
|
</p>
|
||||||
<ul class="expense-list">
|
|
||||||
{{#each reimbursement.expenses as |expense|}}
|
<ExpenseList @expenses={{reimbursement.expenses}} />
|
||||||
<li>
|
|
||||||
<div class="description" rowspan="2">
|
<div class="meta">
|
||||||
<h4>
|
<p class="confirmation-eta">
|
||||||
<span class="date">{{expense.date}}:</span>
|
<ConfirmedIn @confirmedAtBlock={{reimbursement.confirmedAt}} />
|
||||||
<span class="title">{{expense.title}}</span>
|
</p>
|
||||||
</h4>
|
<p class="actions">
|
||||||
<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}}"
|
<a href="{{this.ipfsGatewayUrl}}/{{reimbursement.ipfsHash}}"
|
||||||
class="button small" target="_blank" rel="noopener noreferrer">
|
class="button small" target="_blank" rel="noopener noreferrer">
|
||||||
Inspect IPFS data
|
Inspect IPFS data
|
||||||
</a>
|
</a>
|
||||||
|
{{#if this.kredits.currentUserIsCore}}
|
||||||
<button {{on "click" (fn this.veto reimbursement.id)}}
|
<button {{on "click" (fn this.veto reimbursement.id)}}
|
||||||
disabled={{reimbursement.vetoed}}
|
disabled={{reimbursement.vetoed}}
|
||||||
class="button small danger" type="button">veto</button>
|
class="button small danger" type="button">veto</button>
|
||||||
</p>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
import getLocale from 'kredits-web/utils/get-locale';
|
||||||
|
|
||||||
|
export default helper(function(dateStr) {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const locale = getLocale();
|
||||||
|
return new Intl.DateTimeFormat(locale).format(date);
|
||||||
|
});
|
||||||
@@ -118,9 +118,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 => {
|
||||||
@@ -738,5 +741,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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,4 +31,24 @@ 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;
|
||||||
|
|
||||||
|
&.confirmation-eta {
|
||||||
|
}
|
||||||
|
|
||||||
|
&.actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
@@ -0,0 +1,5 @@
|
|||||||
|
export default function() {
|
||||||
|
return (navigator.languages && navigator.languages.length) ?
|
||||||
|
navigator.languages[0] :
|
||||||
|
navigator.language;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
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');
|
||||||
|
})
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user