Compare commits

..

30 Commits

Author SHA1 Message Date
Râu Cao 6a72a5b418 2.0.0-beta.3 2023-01-15 13:18:42 +08:00
Râu Cao d90ddd464a Hide confirmed-in content when confirmed 2023-01-15 13:16:38 +08:00
Râu Cao 2626543f3e Fix linter errors 2023-01-15 13:09:39 +08:00
Râu Cao 088db24534 Merge pull request #207 from 67P/feature/optional_full_sync
Make full data sync optional, start via button
2023-01-15 12:36:59 +08:00
Râu Cao f7b7daa024 Merge pull request #210 from 67P/feature/confirmation-eta
Add confirmed-in component, show ETA for proposed reimbursements
2023-01-15 12:36:43 +08:00
Râu Cao bb11e2267b Merge pull request #211 from 67P/feature/expense-list
Expense list UI improvements
2023-01-15 12:35:17 +08:00
Râu Cao 7cd023a21b Update app/components/add-reimbursement/template.hbs
Co-authored-by: Manuel Wiedenmann <manuel@funkensturm.de>
2023-01-15 12:34:40 +08:00
Râu Cao 1095bf0218 Add more button colors, icon style for small buttons
Plus tag icon for the tag buttons
2023-01-13 15:58:39 +08:00
Râu Cao 138cec0389 Localize date in expense list 2023-01-13 15:58:39 +08:00
Râu Cao a93be41e08 Add expense-list component, DRY up code, add tags 2023-01-13 15:58:39 +08:00
Râu Cao 89f6fa0b5c Use component argument in tests 2023-01-13 15:57:50 +08:00
Râu Cao e2a80eafd1 Remove dev logs 2023-01-13 14:14:24 +08:00
Râu Cao 662e76979b Show confirmation ETA for proposed reimbursements 2023-01-11 14:19:03 +08:00
Râu Cao 6c7de97e38 Get duration in human time 2023-01-11 14:10:35 +08:00
Râu Cao fe27b010da Add confirmed-in component 2023-01-11 13:59:41 +08:00
Râu Cao ff716c68ea Update current block when new one is mined 2023-01-11 13:26:58 +08:00
Râu Cao 84dded2f10 Do not automatically push new release versions 2023-01-11 12:44:56 +08:00
Râu Cao ee664c06c1 2.0.0-beta.2 2023-01-11 12:44:32 +08:00
Râu Cao 62b618b657 Add type to button element 2023-01-11 12:42:15 +08:00
Râu Cao 348dc03429 Merge pull request #209 from 67P/feature/veto_reimbursements
Finish Reimbursement UI (MVP)
2023-01-09 11:34:37 +08:00
Râu Cao d159fca816 Add todo note to async action 2023-01-09 11:31:31 +08:00
Râu Cao a582f41fde Cache reimbursement data on incoming events 2022-12-31 15:07:02 +07:00
Râu Cao 2082b51c5b Add expenses from file
Allow to upload a JSON file containing a list of expense items
2022-12-31 15:05:36 +07:00
Râu Cao e543708b42 Add IPFS inspect button to reimbursements 2022-12-31 12:20:59 +07:00
Râu Cao 0b0dea095f Reject empty tag strings 2022-12-31 12:20:27 +07:00
Râu Cao c7eb81450c Allow to veto reimbursements 2022-12-30 22:26:32 +07:00
Râu Cao ca1ccf6d93 Formatting 2022-12-30 22:25:50 +07:00
Râu Cao 0b79d48be9 Merge pull request #208 from 67P/bugfix/204-reimbursement_form
Fix timezone issue in expense item form
2022-12-30 22:24:43 +07:00
Râu Cao f8cc453d7e Fix timezone issue in expense item form
Dates being added with 00:00 time still carry the timezone set for the
OS/browser, and thus get changed to the previous day when in a positive
offset zone. This change removes the timezone offset entirely when
adding the line item, so that the chosen date is always assumed to be in
UTC time.

fixes #204
2022-12-30 22:20:16 +07:00
Râu Cao f86190030f Make full data sync optional, start via button 2022-11-14 22:58:15 +01:00
34 changed files with 547 additions and 201 deletions
+6
View File
@@ -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": [
+7 -2
View File
@@ -63,7 +63,10 @@ export default class AddExpenseItemComponent extends Component {
let dateInput = (this.date instanceof Array) ? let dateInput = (this.date instanceof Array) ?
this.date[0] : this.date; this.date[0] : this.date;
const [ date ] = dateInput.toISOString().split('T');
const [ date ] = moment(dateInput).utcOffset(0, true)
.toISOString()
.split('T');
const isValid = this.validateForm(); const isValid = this.validateForm();
if (!isValid) return false; if (!isValid) return false;
@@ -78,7 +81,9 @@ export default class AddExpenseItemComponent extends Component {
} }
if (isPresent(this.tags)) { if (isPresent(this.tags)) {
expense.tags = this.tags.split(',').map(t => t.trim()); expense.tags = this.tags.split(',')
.map(t => t.trim())
.filter(t => t.length > 0);
} }
this.args.addExpenseItem(expense); this.args.addExpenseItem(expense);
+19 -1
View File
@@ -6,6 +6,7 @@ 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 isValidAmount from 'kredits-web/utils/is-valid-amount'; import isValidAmount from 'kredits-web/utils/is-valid-amount';
import readFileContent from 'kredits-web/utils/read-file-content';
import config from 'kredits-web/config/environment'; import config from 'kredits-web/config/environment';
export default class AddReimbursementComponent extends Component { export default class AddReimbursementComponent extends Component {
@@ -84,8 +85,25 @@ export default class AddReimbursementComponent extends Component {
this.total = btcAmount.toFixed(8); this.total = btcAmount.toFixed(8);
} }
// TODO use ember-concurrency here
// https://github.com/67P/kredits-web/pull/209#discussion_r1064234421
@action @action
updateContributor(event) { async addExpensesFromFile (evt) {
const content = await readFileContent(evt.target.files[0]);
const expenses = JSON.parse(content);
if (expenses instanceof Array) {
for (const item of expenses) {
this.addExpenseItem(item);
}
} else {
console.warn("Expenses in file must be a list of items:");
console.debug(content);
}
}
@action
updateContributor (event) {
this.recipientId = event.target.value; this.recipientId = event.target.value;
} }
+10 -20
View File
@@ -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>
@@ -87,3 +71,9 @@
<AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} /> <AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} />
{{/if}} {{/if}}
</form> </form>
<form id="add-expenses-from-file">
<h3>Add expense items from file</h3>
<input type="file" multiple="false"
onchange={{fn this.addExpensesFromFile}}
accept="application/json" />
</form>
+1 -1
View File
@@ -3,7 +3,7 @@ import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed'; 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; @alias('communityFunds.balances') balances;
get loading () { get loading () {
+24
View File
@@ -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;
}
}
+3
View File
@@ -0,0 +1,3 @@
{{#unless this.isConfirmed}}
Confirming in <strong>{{this.confirmedInBlocks}}</strong> blocks (~ {{this.confirmedInHumanTime}})
{{/unless}}
+9
View File
@@ -0,0 +1,9 @@
import Component from '@glimmer/component';
export default class ExpenseListComponent extends Component {
get showDeleteButton () {
return !!this.args.deletable;
}
}
+29
View File
@@ -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 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>
@@ -1,7 +1,23 @@
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 config from 'kredits-web/config/environment';
export default class ReimbursementListComponent extends Component { export default class ReimbursementListComponent extends Component {
@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);
});
}
} }
+25 -18
View File
@@ -3,29 +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="recipient"><UserAvatar @contributor={{reimbursement.contributor}} /></span> <span class="recipient">
<span class="title">Expenses covered by {{reimbursement.contributor.name}}</span> <UserAvatar @contributor={{reimbursement.contributor}} />
</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>&#8239;<span class="symbol">BTC</span> {{sats-to-btc reimbursement.amount}}</span>&#8239;<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> <a href="{{this.ipfsGatewayUrl}}/{{reimbursement.ipfsHash}}"
</div> class="button small" target="_blank" rel="noopener noreferrer">
<div class="amount"> Inspect IPFS data
{{fmt-fiat-currency expense.amount expense.currency}} </a>
</div> {{#if this.kredits.currentUserIsCore}}
</li> <button {{on "click" (fn this.veto reimbursement.id)}}
{{/each}} disabled={{reimbursement.vetoed}}
</ul> class="button small danger" type="button">veto</button>
{{/if}}
</p>
</div>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
+3 -1
View File
@@ -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({
@@ -33,6 +33,8 @@ export default Controller.extend({
showQuickFilterUnconfirmed: false, showQuickFilterUnconfirmed: false,
showQuickFilterConfirmed: false, showQuickFilterConfirmed: false,
showFullContributionSync: gt('kredits.missingHistoricContributionsCount', 0),
showIntroText: computed('kredits.{hasAccounts,currentUser}', function(){ showIntroText: computed('kredits.{hasAccounts,currentUser}', function(){
return (!this.kredits.hasAccounts || !this.kredits.currentUser); return (!this.kredits.hasAccounts || !this.kredits.currentUser);
}), }),
+8
View File
@@ -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);
});
+5 -2
View File
@@ -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);
} }
} }
+43 -21
View File
@@ -48,6 +48,8 @@ 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', []);
@@ -118,9 +120,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 +232,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);
@@ -368,11 +381,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 +403,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'));
@@ -617,6 +631,18 @@ export default Service.extend({
yield this.fetchMissingObjects.perform('Reimbursement'); yield this.fetchMissingObjects.perform('Reimbursement');
}).group('syncTaskGroup'), }).group('syncTaskGroup'),
vetoReimbursement (id) {
console.debug('[kredits] veto against reimbursement', id);
const reimbursement = this.reimbursements.findBy('id', id);
return this.kredits.Reimbursement.functions.veto(id, { gasLimit: 300000 })
.then(data => {
console.debug('[kredits] veto response', data);
reimbursement.set('pendingTx', data);
return data;
});
},
// //
// Contract events // Contract events
// //
@@ -676,7 +702,7 @@ export default Service.extend({
}, },
async handleContributionVetoed (contributionId) { async handleContributionVetoed (contributionId) {
console.debug('[kredits] ContributionVetoed event received for ', contributionId); console.debug('[kredits] ContributionVetoed event received for #', contributionId);
const c = this.contributions.findBy('id', contributionId); const c = this.contributions.findBy('id', contributionId);
if (c) { if (c) {
@@ -687,9 +713,6 @@ export default Service.extend({
} }
}, },
//
// TODO test when reimbursement txs are successful
//
async handleReimbursementAdded (id, addedByAccount, amount) { async handleReimbursementAdded (id, addedByAccount, amount) {
console.debug('[kredits] ReimbursementAdded event received', { id, addedByAccount, amount }); console.debug('[kredits] ReimbursementAdded event received', { id, addedByAccount, amount });
@@ -703,20 +726,19 @@ export default Service.extend({
} }
const data = await this.kredits.Reimbursement.getById(id); const data = await this.kredits.Reimbursement.getById(id);
this.loadReimbursementFromData(data); const r = this.loadReimbursementFromData(data);
this.browserCache.reimbursements.setItem(r.id.toString(), r.serialize());
}, },
// async handleReimbursementVetoed (id) {
// TODO test when reimbursement txs are successful and veto is implemented console.debug(`[kredits] ReimbursementVetoed received for #${id}`);
// const r = this.reimbursements.findBy('id', id);
handleReimbursementVetoed (id) { console.debug('[kredits] reimbursement', r);
console.debug('[kredits] ReimbursementVetoed received for ', id);
const reimbursement = this.reimbursements.findBy('id', id);
console.debug('[kredits] reimbursement', this.reimbursement);
if (reimbursement) { if (r) {
reimbursement.set('vetoed', true); r.set('vetoed', true);
reimbursement.set('pendingTx', null); r.set('pendingTx', null);
this.browserCache.reimbursements.setItem(r.id.toString(), r.serialize());
} }
}, },
@@ -730,5 +752,5 @@ export default Service.extend({
this.contributors this.contributors
.findBy('address', to) .findBy('address', to)
.incrementProperty('balance', value); .incrementProperty('balance', value);
}, }
}); });
+34 -13
View File
@@ -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);
&:focus, &:active, &.active { border-color: rgba(40, 21, 40, 1);
border-color: $green; &:hover { background-color: rgba(40, 21, 40, 0.8); }
} &:focus, &:active, &.active { border-color: $pink; }
}
&.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 {
-4
View File
@@ -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;
+6 -4
View File
@@ -62,15 +62,12 @@ main {
} }
} }
// TODO Remove after switch to Tailwind CSS
&.text-center { &.text-center {
text-align: center; text-align: center;
} }
p { p {
&.mg-bottom-md {
margin-bottom: 2rem;
}
&.actions { &.actions {
text-align: center; text-align: center;
padding-top: 2rem; padding-top: 2rem;
@@ -177,4 +174,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; }
} }
+17 -19
View File
@@ -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;
}
} }
@@ -7,6 +7,11 @@ ul.reimbursement-list {
grid-row-gap: 0.5rem; grid-row-gap: 0.5rem;
padding-top: 1.6rem; padding-top: 1.6rem;
&.vetoed {
text-decoration: line-through;
opacity: 0.6;
}
.token-amount { .token-amount {
text-align: right; text-align: right;
@@ -26,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;
}
}
}
} }
+1
View File
@@ -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

+26
View File
@@ -105,6 +105,32 @@
@showQuickFilter={{this.showQuickFilterConfirmed}} /> @showQuickFilter={{this.showQuickFilterConfirmed}} />
</div> </div>
</section> </section>
{{#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">
+1 -1
View File
@@ -2,7 +2,7 @@
<h2>Complete your contributor profile</h2> <h2>Complete your contributor profile</h2>
</header> </header>
<div class="content text-lg"> <div class="content text-lg">
<p class="mg-bottom-md"> <p class="mb-8">
Kredits allow you to take part in project governance, and to earn rewards for 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. your contributions. For both, you will need an Ethereum wallet/account.
</p> </p>
+5
View File
@@ -0,0 +1,5 @@
export default function() {
return (navigator.languages && navigator.languages.length) ?
navigator.languages[0] :
navigator.language;
}
+8
View File
@@ -0,0 +1,8 @@
export default function (file) {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = event => resolve(event.target.result)
reader.onerror = error => reject(error)
reader.readAsText(file)
})
}
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "kredits-web", "name": "kredits-web",
"version": "2.0.0-beta.1", "version": "2.0.0-beta.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "kredits-web", "name": "kredits-web",
"version": "2.0.0-beta.1", "version": "2.0.0-beta.3",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.19.1", "@babel/eslint-parser": "^7.19.1",
+1 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "kredits-web", "name": "kredits-web",
"version": "2.0.0-beta.1", "version": "2.0.0-beta.3",
"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",
@@ -22,7 +22,6 @@
"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": {
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
+3 -3
View File
@@ -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-0%2B7c8ca45b%22%7D%2C%22web3ProviderUrl%22%3A%22https%3A%2F%2Frsk-testnet.kosmos.org%22%2C%22web3RequiredChainId%22%3A31%2C%22web3RequiredNetworkName%22%3A%22RSK%20Testnet%22%2C%22githubConnectUrl%22%3A%22https%3A%2F%2Fhal8000.chat.kosmos.org%2Fkredits%2Fsignup%2Fconnect%2Fgithub%22%2C%22githubSignupUrl%22%3A%22https%3A%2F%2Fhal8000.chat.kosmos.org%2Fkredits%2Fsignup%2Fgithub%22%2C%22ipfs%22%3A%7B%22host%22%3A%22ipfs.kosmos.org%22%2C%22port%22%3A%225444%22%2C%22protocol%22%3A%22https%22%2C%22gatewayUrl%22%3A%22https%3A%2F%2Fipfs.kosmos.org%2Fipfs%22%7D%2C%22tokens%22%3A%7B%22BTC%22%3A%220x2260fac5e5542a773aa44fbcfedf7c193bc2c599%22%7D%2C%22btcBalanceAPI%22%3A%22https%3A%2F%2Fapi.kosmos.org%2Fkredits%2Fonchain_btc_balance%22%2C%22corsProxy%22%3A%22https%3A%2F%2Fcors.5apps.com%2F%3Furi%3D%22%2C%22exportApplicationGlobal%22%3Afalse%7D" /> <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.3%2Bd90ddd46%22%7D%2C%22web3ProviderUrl%22%3A%22https%3A%2F%2Frsk-testnet.kosmos.org%22%2C%22web3RequiredChainId%22%3A31%2C%22web3RequiredNetworkName%22%3A%22RSK%20Testnet%22%2C%22githubConnectUrl%22%3A%22https%3A%2F%2Fhal8000.chat.kosmos.org%2Fkredits%2Fsignup%2Fconnect%2Fgithub%22%2C%22githubSignupUrl%22%3A%22https%3A%2F%2Fhal8000.chat.kosmos.org%2Fkredits%2Fsignup%2Fgithub%22%2C%22ipfs%22%3A%7B%22host%22%3A%22ipfs.kosmos.org%22%2C%22port%22%3A%225444%22%2C%22protocol%22%3A%22https%22%2C%22gatewayUrl%22%3A%22https%3A%2F%2Fipfs.kosmos.org%2Fipfs%22%7D%2C%22tokens%22%3A%7B%22BTC%22%3A%220x2260fac5e5542a773aa44fbcfedf7c193bc2c599%22%7D%2C%22btcBalanceAPI%22%3A%22https%3A%2F%2Fapi.kosmos.org%2Fkredits%2Fonchain_btc_balance%22%2C%22corsProxy%22%3A%22https%3A%2F%2Fcors.5apps.com%2F%3Furi%3D%22%2C%22exportApplicationGlobal%22%3Afalse%7D" />
<link integrity="" rel="stylesheet" href="/assets/vendor-ca96b2e19fc9f356e3dbad90c9f3f323.css"> <link integrity="" rel="stylesheet" href="/assets/vendor-ca96b2e19fc9f356e3dbad90c9f3f323.css">
<link integrity="" rel="stylesheet" href="/assets/kredits-web-bdbc10028be8d48749a246964068e561.css"> <link integrity="" rel="stylesheet" href="/assets/kredits-web-f21602587acec9a1744c244385a83592.css">
@@ -25,7 +25,7 @@
<script src="/assets/vendor-4ee536ded971436a83d35253df25f2cb.js" integrity="sha256-yb+pC9iuZ8WcQwk9VuDNhSlWguL7BEABsEOEhBpwUC0= sha512-50LApx5XqeKHu/FMaljzqhAMPgkqSiy2rDhUcViYBcXWBBYmilFirup/iWvLiNCdfr5ct9wk7+tU9iShdSgDww==" ></script> <script src="/assets/vendor-4ee536ded971436a83d35253df25f2cb.js" integrity="sha256-yb+pC9iuZ8WcQwk9VuDNhSlWguL7BEABsEOEhBpwUC0= sha512-50LApx5XqeKHu/FMaljzqhAMPgkqSiy2rDhUcViYBcXWBBYmilFirup/iWvLiNCdfr5ct9wk7+tU9iShdSgDww==" ></script>
<script src="/assets/kredits-web-597bd2c84f295cea39c51678944cb39f.js" integrity="sha256-Ctb96mli0/Rkuej7AJB6wLegTJElHqGYpBOiKkB3spw= sha512-pwHfdUGsokaPO3vAZV4qYG+ljL+5ceExlHARAoyTsd15+yCDCbKsmJzuwLXnagNn/O1IzkDlEN0wed5l3FbuhQ==" ></script> <script src="/assets/kredits-web-89113e4d5c371829e3d1b05e86cbab9b.js" integrity="sha256-aabl6JZt69T2BZIv4yXcRFfmhWCyEhVA2V4g1ZzkZ6s= sha512-aD0s+kWNsXdHJFxIkgncvwNZVhtARosjjKnbLE0w1ihSrvRoptWXV1SgySdZIHKxFgbNmJGA2oQOzhBKIoqvVg==" ></script>
</body> </body>
@@ -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);
})
});