Compare commits

...

270 Commits

Author SHA1 Message Date
Râu Cao 08bedda288 2.0.0-beta.1 2022-11-14 15:56:57 +01:00
Râu Cao 7c8ca45b27 Merge pull request #205 from 67P/feature/improve_caching
Improve caching, fix Firefox private mode usage
2022-11-14 15:48:33 +01:00
Râu Cao ff3fe49dd0 Wording 2022-11-11 21:11:17 +01:00
Râu Cao 92056517a6 Cache contribution vetos
Fixes potentially not marking contributions as vetoed if there's no
incoming event after loading the app. Also improves performance either
way.
2022-11-11 21:11:17 +01:00
Râu Cao 11f19c8344 Refactor adding new contributions from events
Fixes contributions not being added when not already in memory (from
creation). Simplifies the entire function.
2022-11-11 21:11:13 +01:00
Râu Cao 0f8d7d84ca Don't cache contribution details in browser storage 2022-11-11 17:02:45 +01:00
Râu Cao e1780109aa Merge pull request #203 from 67P/feature/rsk-testnet
Migrate/switch to RSK testnet
2022-11-11 17:01:36 +01:00
Râu Cao 8c2f6aa38b Update Travis CI config 2022-11-11 16:57:51 +01:00
Râu Cao afbce03a4a Adapt test for rsk storage namespace 2022-11-11 16:38:39 +01:00
Râu Cao 1f2dd9d198 Use new @kredits/contracts 2022-11-03 16:57:55 +01:00
Râu Cao e4814923f9 Update contracts module branch 2022-10-31 12:48:49 +01:00
Râu Cao ad015f1cee Use required chain ID and network name
Use the chain ID when loading the web3 wallet/provider, and use the
network name for the localstorage namespace.
2022-10-24 16:45:02 +02:00
Râu Cao 0c5aebf7b4 WIP Migrate to RSK testnet 2022-10-24 16:30:38 +02:00
Râu Cao 52f7e86fc9 Merge pull request #202 from 67P/chore/fix_js_linting
Fix ESLint setup
2022-10-06 16:29:52 +02:00
Râu Cao fb66bd4f0d Fix some linter errors and warnings 2022-10-06 12:29:57 +02:00
Râu Cao 19553b9777 Fix eslint setup
babel-eslint isn't working with node 16 anymore

In addition to the new packages, also turn the remaining Ember errors
into warnings, so we can fix them without `npm test` failing in the
meantime.
2022-10-06 12:29:03 +02:00
Râu Cao 441dc21894 Merge pull request #201 from 67P/update-for-new-amounts
Fix for new Contribution#totalKreditsEarned type
2022-10-06 12:26:09 +02:00
bumi 0732911e13 Cleanup 2022-10-06 11:03:50 +02:00
bumi 57934ae7a3 Fix for new Contribution#totalKreditsEarned type
totalKreditsEarned is no longer a Big number but now already a number.
I think this is due to the change of the denomination.
2022-10-06 10:52:22 +02:00
basti 1e240b6539 Merge pull request #199 from 67P/chore/rename_recipient_for_reimbursements
Rename contributorId to recipientId
2022-06-12 07:17:47 +02:00
basti 2c773c147a Merge pull request #200 from 67P/feature/wbtc_to_btc
Switch from ETH/WBTC to BTC
2022-06-12 07:17:17 +02:00
basti e05449a7ff Show wallet balance for BTC instead of Gnosis Safe 2022-05-24 16:19:26 +02:00
basti 44b631c688 Switch from ETH/WBTC to RBTC/BTC 2022-05-22 17:04:28 +02:00
basti 6cd62d3764 Fix looking up contributor by ID string 2022-05-22 17:03:31 +02:00
basti 4c749bca35 Use integer for contributor ID everywhere
closes #145
2022-05-21 18:16:26 +02:00
basti 466354cff7 Update/improve README
Especially the local-contracts part.
2022-05-01 14:19:45 +02:00
basti 721c330f3c WIP Rename contributorId to recipientId 2022-04-27 18:17:33 +02:00
basti d07963ad6b Merge pull request #198 from 67P/chore/dependencies
Upgrade dependencies, fix contracts linking
2022-04-27 18:17:20 +02:00
basti eef9fa1aba Refactor kredits setup code a bit 2022-04-27 18:15:12 +02:00
basti 77dbde8bb4 Update dependencies, build configs 2022-04-27 18:14:56 +02:00
basti 64bd2477c7 Merge pull request #195 from 67P/feature/expenses
Introduce budget, reimbursements for expenses
2021-06-03 16:23:45 +02:00
basti aed29ff982 Remove obsolete property from class and template
Co-authored-by: Garret Alfert <alfert@wevelop.de>
2020-11-17 13:40:44 +01:00
basti f3f158ff26 Update app/services/kredits.js
Co-authored-by: Garret Alfert <alfert@wevelop.de>
2020-11-17 13:38:47 +01:00
basti 2a8fd3b001 Update app/components/topbar-account-panel/component.js
Co-authored-by: Garret Alfert <alfert@wevelop.de>
2020-11-17 12:47:43 +01:00
basti 087cb706d2 Fix JS linting errors 2020-10-31 13:30:44 +01:00
basti b58870a03c Octanify add-reimbursement syntax
Fixes linter errors
2020-10-31 13:05:40 +01:00
basti ccda6ec354 Reset BTC amount to 0 when last expense item removed
Fix a logical bug that prevented setting the amount to 0 upon removing
the last item from the list/form.
2020-10-31 12:58:03 +01:00
basti 2045b94951 Allow HTML comments, remove obsolete comment 2020-10-31 12:49:29 +01:00
basti be16a57043 Octanify add-expense-item syntax
Fixes linter errors
2020-10-31 12:49:03 +01:00
basti ae06385039 Improve mobile layout of budget view a bit 2020-10-31 12:21:31 +01:00
basti 7b1c3c813c Finish reimbursement loading and sync 2020-10-30 15:26:32 +01:00
basti ec50e7deef Fix small layout issue with token amounts
Wasn't enough space for longer amounts
2020-10-30 15:03:54 +01:00
basti c3b1bd2f4c Make font sizes more consistent 2020-10-30 15:03:45 +01:00
basti 93e2a23ac7 WIP reimbursement sync 2020-10-09 15:40:46 +02:00
basti a41d1b1dbd Handle incoming and vetoed reimbursements 2020-10-09 15:09:35 +02:00
basti c0ccf83061 Don't allow form submission unless connected and core 2020-10-09 14:41:10 +02:00
basti 4ab5c3c372 Listen to tx status of submitted reimbursements 2020-10-09 14:29:53 +02:00
basti 5d4fb1c423 Fix reimbursement handling after creation 2020-10-01 17:23:22 +02:00
basti 361ccb103d Remove commented code 2020-10-01 12:58:07 +02:00
basti 2bf590886c Fix newly created contributions not showing
It's trying to use a non-existing block number to calculate
confirmedAtBlock, which then fails the filter to be shown.
2020-10-01 12:50:18 +02:00
basti c7b6f9e3e7 Submit reimbursements 2020-10-01 12:48:01 +02:00
basti c1eb93c596 Mark input fields as valid when re-submitting form 2020-09-30 16:57:10 +02:00
basti d85235ff88 Remove obsolete template code 2020-09-30 16:51:22 +02:00
basti 63675a1e13 Convert fiat totals to BTC and add to total amount 2020-09-30 16:46:35 +02:00
basti 87bc8f5cb5 Visualize disabled input fields by greying out text 2020-09-30 16:06:57 +02:00
basti 4abc1593cb Add total sums of EUR/USD expenses 2020-09-30 16:04:53 +02:00
basti 25b2a8b0e5 Create exchange rate service , use CORS proxy for requests 2020-09-30 15:03:55 +02:00
basti b5700092a8 Add util for fetching bitcoin exchange rate 2020-09-30 12:10:21 +02:00
basti 7288d75237 Don't load more reimbursements when already loaded 2020-09-30 10:32:36 +02:00
basti 444ec62a66 Validate expense/reimbursement forms
Adds some general helpers and styles for minimalistic form validation.
2020-09-29 19:31:58 +02:00
basti 326d46b1a1 Validate total amount 2020-09-29 18:09:48 +02:00
basti 083c873631 Scroll to current item; remove some obsolete code 2020-09-29 17:59:00 +02:00
basti 37dc91da57 Clearly highlight focused buttons 2020-09-29 17:59:00 +02:00
basti 7c86edcaaa Remove expense item from list 2020-09-29 17:59:00 +02:00
basti e4d2fdfce4 WIP Reimbursements/expenses 2020-09-29 17:58:38 +02:00
basti 4722064337 WIP expense items 2020-09-28 11:46:55 +02:00
basti 9f35add07f WIP Reimbursement UI 2020-09-10 16:28:01 +02:00
basti e86541574d Format and round the USD value of balances 2020-08-13 16:30:39 +02:00
basti 5f90bc23bc Dynamically load all balances from Safe 2020-08-13 16:13:50 +02:00
basti 1dd63b2c7a Basic styles for budget balances 2020-08-13 15:43:51 +02:00
basti f1bd20a6f4 Add helper for formatting cryptocurrencies 2020-08-13 15:19:17 +02:00
basti ec6b72008d Add whitespace between reimbursement items in list
They're a bit difficult to scan otherwise.
2020-08-13 14:58:49 +02:00
basti dec71c6f4f Load community funds balances from mainnet Gnosis Safe 2020-08-13 00:27:40 +02:00
basti 52172ea065 add-reimbursement component 2020-08-12 22:53:08 +02:00
basti f63d14d7b4 Move sorted contributors to kredits service 2020-08-12 22:51:52 +02:00
basti ef0fc11edf Add reimbursement form route and component 2020-07-25 18:24:16 +02:00
basti 083334ed18 Don't redirect to dashboard after connecting web3 account 2020-07-25 18:23:55 +02:00
basti 69a6c850bf Convert route to class 2020-07-25 18:22:02 +02:00
basti 8074d2d0e3 Hide empty sections in budget 2020-07-10 13:01:06 +02:00
basti 11c3078b55 Style reimbursement list items 2020-07-09 23:53:06 +02:00
basti fb267813fb Introduce item-list CSS, move item colors to variables 2020-07-09 22:41:11 +02:00
basti a96cca879d Use browser's locale for currency format 2020-07-09 22:15:25 +02:00
basti 4849a755dc Render currencies etc. 2020-07-09 17:07:05 +02:00
basti eaac245f4e WIP Basic reimbursement lists 2020-07-09 15:46:20 +02:00
basti 95743330e4 WIP Generic data handling and caching
Add basic support for reimbursements via new, generic data handling
functions.
2020-06-27 16:35:45 +02:00
basti 4a9dd5ff09 Re-organize functions of kredits service 2020-06-27 14:01:05 +02:00
basti 3b127b0e81 WIP Budget route and basic layout 2020-06-27 13:36:28 +02:00
basti 769317777c Refactor routes
* Add specific dashboard route
* Port application route to Octane
* Fetch/sync contributors in application route, but only sync
  contributions in dashboard route for now
2020-06-27 13:36:02 +02:00
basti 174073fc42 1.17.0 2020-06-02 11:55:29 +02:00
basti 733e5e3683 Merge pull request #191 from 67P/chore/update_dependencies
Update dependencies
2020-06-02 11:52:10 +02:00
basti 090d1d0856 Update dependencies
Fixes a bunch of security audit warnings in the process.
2020-06-02 11:25:35 +02:00
basti e047cc45ba Merge pull request #190 from 67P/feature/improve_contribution_sync
Improve data sync
2020-06-02 10:55:36 +02:00
basti c64a503546 Remove unused async from function
Throws a linter warning, now that it's not an RSVP promise anymore.
2020-05-31 12:46:51 +02:00
basti 13021d665d Merge pull request #188 from 67P/feature/schema_updates
Add new contribution kinds
2020-05-31 10:45:16 +02:00
basti b5dbb2449d Merge pull request #189 from 67P/feature/data_loading
Cache data in browser storage, and load from local cache when present
2020-05-31 10:44:36 +02:00
basti 302c3315a3 Update debug logs while fetching past contributions 2020-05-29 22:28:42 +02:00
basti bac4c1c425 Load all past contributions once everything else is loaded 2020-05-29 22:04:03 +02:00
basti c54d754c89 Only sync contributions if loaded from cache 2020-05-29 20:55:19 +02:00
basti 63dd5430bc WIP Sync contributions 2020-05-29 18:56:05 +02:00
basti eb2c613ac9 Extract data loading from fetch methods 2020-05-29 13:12:39 +02:00
basti bf6d9dbc31 Remove RSVP from kredits service
Just native Promise will do.
2020-05-29 13:11:36 +02:00
basti c491f10ec3 Remove obsolete utils 2020-05-28 14:18:30 +02:00
basti d4b546bfcd Load contributions from cache if present
And then fetch more from network. Also, only load 30 from network before
first render (with nothing cached), then load more after the page has
rendered.
2020-05-28 13:59:10 +02:00
basti 69141a31c4 Process contribution data before loading models 2020-05-28 13:44:32 +02:00
basti e7c9620bbe Add network namespace for browser cache
We want to be able to cache data from multiple networks side-by-side,
without them interfering with each other.
2020-05-28 11:53:19 +02:00
basti 832de3c06f Load contributors from cache if present
And then update from network later
2020-05-28 11:33:47 +02:00
basti 50e1fc728c Cache contributors in IndexedDB 2020-05-27 17:33:36 +02:00
basti 871731110b Process contributor data
Add util function for processing contributor data, and remove bignums,
etc.
2020-05-27 16:27:08 +02:00
basti f996c89dfe Add localforage, basic cache service 2020-05-27 12:59:37 +02:00
basti 1d8e9ddf5d Remove obsolete npm cache Travis config
It's cached by default now
2020-05-27 11:19:23 +02:00
basti 352c9fe3ab Add new contribution types 2020-05-27 11:15:42 +02:00
basti e2a944ed5b Update schemas package
Use the new one from the org namespace.
2020-05-16 13:21:02 +02:00
basti c57d4162d7 1.16.0 2020-05-16 11:28:23 +02:00
basti 41cc37d3eb Merge pull request #186 from 67P/chore/upgrade_ember_and_deps
Upgrade Ember and deps to 3.18/Octane
2020-05-11 13:18:58 +02:00
basti 081584a508 Remove obsolete file 2020-05-11 13:15:47 +02:00
basti 3cc01cce4e Fix some minor template issues 2020-05-11 13:15:17 +02:00
basti cedbe3cb3d Formatting 2020-05-11 13:03:46 +02:00
basti b8da04c145 Re-add contribution status class 2020-05-11 13:03:46 +02:00
galfert 009caa53e5 Fix typo in contribution-status helper
The wrong property name caused an exception when trying to remove the observer.
2020-05-11 12:18:07 +02:00
basti 9a1f543921 Mark remaining template-lint issues as pending
These need more updates outside of the templates, like e.g. Glimmer
components and such.
2020-05-11 10:24:38 +02:00
basti 22300c2980 Fix actions, etc. 2020-05-11 10:21:23 +02:00
basti 018132034d Fix github icon filename 2020-05-11 10:21:23 +02:00
basti f5d7916556 Turn on warnings for observer usage 2020-05-11 10:21:23 +02:00
basti 12df28f7fd Fix ESLint errors 2020-05-11 10:21:22 +02:00
basti 2f4f350a93 WIP Octanify the templates 2020-05-11 10:21:18 +02:00
basti 4946b3311d Remove all proposal-related code
We're not using proposals at the moment, so it's just in the way.
2020-05-10 10:25:06 +02:00
basti cc2f91afce Do not lint icon templates 2020-05-09 17:39:54 +02:00
basti aa351cbe17 Move icon templates out of pod directory
Glimmer components don't have extra markup around them, so we don't need
the component JS files anymore. Also, we can easily ignore the icon
templates when linting now.
2020-05-09 17:37:58 +02:00
basti 065c635580 Upgrade to latest Ember, etc. 2020-05-09 17:07:35 +02:00
basti 67e3c9a7c3 Merge pull request #185 from 67P/dependabot/npm_and_yarn/jquery-3.5.0
Bump jquery from 3.4.1 to 3.5.0
2020-04-30 19:54:31 +02:00
dependabot[bot] 4f4b3c5c17 Bump jquery from 3.4.1 to 3.5.0
Bumps [jquery](https://github.com/jquery/jquery) from 3.4.1 to 3.5.0.
- [Release notes](https://github.com/jquery/jquery/releases)
- [Commits](https://github.com/jquery/jquery/compare/3.4.1...3.5.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-04-30 16:18:08 +00:00
basti 4f6bafa31e Merge pull request #184 from 67P/dependabot/npm_and_yarn/acorn-5.7.4
Bump acorn from 5.7.3 to 5.7.4
2020-03-16 10:50:11 -05:00
dependabot[bot] 94846c6e00 Bump acorn from 5.7.3 to 5.7.4
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-16 12:37:30 +00:00
basti 45d553866e Remove obsolete file 2019-12-14 16:35:13 +01:00
basti 8ac6e3ad3c 1.15.0 2019-12-14 12:06:28 +01:00
basti 215ad1d640 Merge pull request #180 from 67P/feature/add_contribution_with_params
Add "give kredits" button to contributor profiles
2019-12-13 17:33:18 +01:00
basti 3f4cc39067 Refactor assignment
Co-authored-by: Garret Alfert <alfert@wevelop.de>
2019-12-13 17:04:52 +01:00
basti 0a6b6180b9 Add "give kredits" button to contributor profiles
closes #172
2019-12-12 17:33:08 +01:00
basti 536868002f Support query params for adding contribution
Just contributor ID, kind, and amount for now.
2019-12-10 18:16:54 +01:00
basti 04a7f9139d Allow partial override of contribution attributes 2019-12-10 13:40:08 +03:00
basti 863b542e3e Add failing test for partial attribute override
Currently the default attributes are only set correctly when either none
or all attributes are handed to the component
2019-12-10 13:18:21 +03:00
basti 23d86f1502 Add unit test for add-contribution component 2019-12-10 13:09:30 +03:00
basti 314cf58c29 1.14.0 2019-12-02 15:59:30 +01:00
basti 9c063b82f1 Merge pull request #169 from 67P/feature/163-small_screen_details_new
Show contributor/contribution details on mobile
2019-12-02 15:57:22 +01:00
basti b397e4faf6 Merge pull request #168 from 67P/bugfix/ios_button_styling
Reset iOS button styling
2019-11-29 21:07:35 +01:00
galfert 256197094e Don't show intro text on details page 2019-11-29 18:06:32 +01:00
galfert 9898d4bb1d Show contributor/contribution details on mobile
Refs #163

On small screens, when selecting a contribution or contributor, the
lists are hidden and only the details are shown.
2019-11-29 18:05:52 +01:00
galfert c321dc51b0 Reset iOS button styling
Removes the pill shaped, shaded button styling on iOS.
2019-11-29 17:44:22 +01:00
basti 8c759601ca 1.13.0 2019-11-27 15:22:02 +01:00
galfert 15e5d3c2fb Merge pull request #166 from 67P/feature/welcome_text
Add intro, fix topbar on mobile, fix filter button
2019-11-27 13:50:31 +01:00
basti 47b7a6501b Add intro text for new users
closes #164
2019-11-27 12:39:49 +01:00
basti 42304a8e27 Fix account panel breaking out of topbar on mobile
refs #163
2019-11-27 11:55:08 +01:00
basti 9a4db6daf6 Do not require account for filtering contributions 2019-11-27 11:00:10 +01:00
basti 26dc78f553 1.12.0 2019-11-22 12:30:20 +01:00
galfert 5a618f7ef9 Merge pull request #165 from 67P/feature/161-copy_contribution
Add copy/re-submit button to all contribution details
2019-11-22 11:55:03 +01:00
basti 5d29bd9604 Add copy/re-submit button to all contribution details
We can in fact just use the exact same re-submission as for vetoed
contributions. So all that is needed is a new button in the UI.

closes #161
2019-11-21 21:13:42 +01:00
basti 97cc71024f Merge pull request #160 from 67P/bugfix/remove_redundant_pending_status
Remove redundant pending status from contribution-status helper
2019-11-15 16:59:50 +01:00
galfert 58f1382df1 Fix helper test filename
Files that don't end with '-test', will not be run.
2019-11-15 16:23:28 +01:00
galfert 6c5406c0b9 Remove redundant pending status from contribution-status helper 2019-11-15 15:28:46 +01:00
basti b2457421be 1.11.0 2019-10-18 17:38:50 +02:00
basti 134eb7d355 Update deps
Fixes security vulns
2019-10-18 17:36:55 +02:00
basti 1dc1a9767e Merge pull request #159 from 67P/feature/pending_changes
Gracefully handle pending changes for contributions
2019-10-18 17:31:41 +02:00
galfert 243a41db88 Remove volatile() property modifier
The modifier got deprecated and wasn't actually needed in any of the used cases.
2019-10-18 16:39:10 +02:00
galfert 523d4524b5 Make contribution-status helper detect pending changes 2019-10-18 16:37:44 +02:00
galfert c21613f20a Merge branch 'master' into feature/pending_changes 2019-10-18 15:17:41 +02:00
basti 12b127c75d Merge pull request #158 from 67P/feature/recompute_helper
Make contribution-status helper recompute
2019-10-18 09:56:50 +02:00
basti a7dd058d21 Remove obsolete parens 2019-10-18 09:52:38 +02:00
basti de6606b29d Merge pull request #156 from 67P/feature/profile_zoom
Add support for Zoom display name in profiles
2019-09-20 23:51:35 +02:00
basti ec701b3130 Update lockfile 2019-09-20 23:45:33 +02:00
galfert 8e7430ebb9 Make contribution-status helper recompute
When any of the depending properties of the contribution changes, the helper recomputes its output.
2019-09-20 18:27:40 +02:00
basti 1deecafee7 Handle pending changes for contributions
This adds the pending tx data to pending contributions (after adding or
vetoing, until the tx is mined). It also disables the veto button while
pending.
2019-09-20 15:43:34 +02:00
basti 51d8b6c8c1 Respect disabled button color for all buttons 2019-09-20 15:40:22 +02:00
bumi b28f6e6bd7 Use official kredits-contracts npm release 2019-09-20 11:17:38 +02:00
basti 4ab28016a3 Depend on contracts master
PR has been merged
2019-09-18 09:38:10 +02:00
basti 6995b67a22 Add support for Zoom display name in profiles 2019-09-18 09:35:24 +02:00
basti 0076d59a42 Remove obsolete ID from icon markup 2019-09-18 09:35:06 +02:00
basti 36b92d97ca Use configured IPFS gateway URL on contributor profile 2019-09-18 09:34:11 +02:00
basti f272bcfcbc Prevent transition abortion on contributor save 2019-09-18 09:32:57 +02:00
basti 985e1673ac Add Zoom icon 2019-09-18 09:31:58 +02:00
basti fe4af46852 1.10.0 2019-09-10 17:01:23 +02:00
basti 7b9659e22f Update deps
Fixes security vulns
2019-09-10 16:59:20 +02:00
basti 8be18fc27c Merge pull request #155 from 67P/feature/resubmit_vetoed_contribution
Re-submit vetoed contributions
2019-09-10 16:53:36 +02:00
basti 7e518780c5 Merge pull request #154 from 67P/feature/sign_in
Improve account connect/loading, check network
2019-09-10 16:48:55 +02:00
basti 89391c1543 Update details of pending contribution when mined
Fetches complete details of new contributions and replaces any potential
pending, incomplete objects in the collection.
2019-09-10 15:21:50 +02:00
basti 05e3118a0c Re-submit vetoed contributions 2019-09-10 11:47:07 +02:00
basti 494b5d9bdd Use configured IPFS gateway URL for button 2019-09-05 11:53:20 +02:00
basti f8f1ad644c WIP: vetoed contribution details
Adds a blurb and resubmit button to the detail view for vetoed
contributions.
2019-09-05 11:36:50 +02:00
basti 5a5051bac6 Check required network before using user provider
closes #82
2019-09-04 18:51:49 +02:00
basti 304c0ac8d0 Only use user wallet if it's already open/connected 2019-09-04 18:29:25 +02:00
basti cae13ed662 Sign in button to connect account 2019-09-04 18:22:48 +02:00
basti 729110f8d1 Remove obsolete APM domains 2019-09-04 17:04:18 +02:00
basti 11f66de6e6 1.9.0 2019-09-03 13:43:17 +02:00
basti 9bf2ae2d7f Merge pull request #151 from 67P/feature/layout_sizing
Improve sizing of entire layout
2019-09-03 13:40:38 +02:00
basti 2a384f3eed Improve sizing of entire layout 2019-09-01 18:06:56 +02:00
basti 6b3666def9 1.8.0 2019-09-01 16:04:21 +02:00
basti 74c8e32cc6 Merge pull request #142 from 67P/feature/signup
Sign up via GitHub
2019-08-29 12:52:04 +02:00
basti 5dde978499 Merge branch 'master' into feature/signup 2019-08-29 12:46:00 +02:00
galfert 0625ee9d79 Handle undefined Github access token 2019-08-28 12:53:21 +02:00
basti dc5dfe19f2 Add success page to GitHub signup 2019-08-27 18:19:46 +02:00
galfert c518b7f565 Merge pull request #149 from 67P/chore/dao_address
Rename kernel address to DAO address
2019-08-27 15:12:45 +02:00
basti d13242b210 Rename kernel address to DAO address
We're making the naming consistent everwhere.
2019-08-27 15:00:18 +02:00
basti ada029586a Merge pull request #148 from 67P/doc/dev_network
Documentation for IPFS and Metamask
2019-08-26 18:53:24 +02:00
galfert 7bf5fa444a Add notice about Metamask when using local devchain 2019-08-26 18:09:38 +02:00
galfert d08d5f9e8d Add IPFS Gateway config to readme 2019-08-26 18:00:17 +02:00
basti a2bd3f76b8 1.7.0 2019-08-12 14:55:54 +02:00
basti a4ca37d882 Merge pull request #146 from 67P/feauture/141-time_picker
Add time input to contribution form
2019-08-12 14:52:07 +02:00
basti 143317d909 Merge pull request #147 from 67P/docs/readme
Fix npm run start:local instructions
2019-08-04 19:43:06 +02:00
fsmanuel d8bcc8ff67 Fix npm run start:local instructions 2019-08-04 19:36:09 +02:00
basti e4a39ec67c Fix bug with default date input
When the input isn't touched by the user, the date isn't stored as an
array on the property.
2019-08-02 16:58:12 +02:00
basti 5b47ba9e11 Import missing module 2019-08-02 16:58:01 +02:00
basti d8f565cd9e Use start of current hour as default contribution time
Co-Authored-By: Garret Alfert <alfert@wevelop.de>
2019-08-02 16:08:13 +02:00
basti a9480a09f1 Add time input to conribution form
After adding it as a separate field, I figured out that we should use
the same picker, because otherwise we'd have to keep the date in sync
depending on the time zone. Zones are too messy in that regard, and the
date is already normalized to UTC before creating the contribution
record.

closes #141
2019-07-31 13:20:39 +02:00
galfert 60ba019139 Fix HAL8000 URL
Wrong port number.
2019-07-26 19:02:41 +02:00
galfert 413bcddb1e Wire up signup via Github 2019-07-26 03:38:18 +02:00
basti c2273d1778 Merge pull request #144 from 67P/feature/validate_eth_addresses
Validate Ethereum addresses in input fields
2019-07-24 17:30:51 +02:00
basti 780afc9bc6 Fix wrong property name 2019-07-24 11:56:51 +02:00
basti a8ae2b0156 Validate Ethereum addresses in input fields 2019-07-22 17:29:31 +02:00
basti d6d2b1a61c Add signup completion page 2019-07-21 17:03:50 +02:00
basti 4c70c0d233 Basic signup view 2019-07-21 16:04:00 +02:00
basti 725d36bed6 Add signup button for unknown users 2019-07-18 19:43:39 +02:00
basti 0a71d7ad91 Add signup route 2019-07-18 19:31:03 +02:00
basti 6a176b5904 Fix empty name in account info
When the user brings Ethereum accounts, but none of them is known as
contributor, instead of "Anonymous" it just shows nothing. This fixes
the if condition in the template to eliminate the faulty case.
2019-07-18 19:27:53 +02:00
basti c7d046aa46 Move topbar account info to component, add tests
Adds a failing test for users with wallet, but no known contributor
account, where it currently shows no name at all.
2019-07-18 19:26:16 +02:00
basti aa9c3648c0 1.6.0 2019-07-17 17:41:49 +02:00
galfert 9c18593529 Merge pull request #140 from 67P/feature/contribution_details
Contribution details
2019-07-17 17:23:14 +02:00
basti 52fe7ff026 Remove commented CSS 2019-07-17 16:55:04 +02:00
basti b02967c5ef Basic contribution details
Adds a details pane for contribution records, similar to contributor
profiles.

Just the basic details for now. Can be extended with all kinds of
data, links, icons, etc. from here on.
2019-07-17 12:58:16 +02:00
basti 7236b2d2b3 Add basics for contribution details
Route, controller, links, selected property/styles, etc.
2019-07-16 15:35:35 +02:00
basti fd1ae0a612 Merge pull request #139 from 67P/feature/update_ember
Update dependencies
2019-07-15 20:07:02 +02:00
basti 525a39479b 1.5.1 2019-07-15 19:19:29 +02:00
galfert 612b76300c Remove ember-cli-update dependency
All but one of the ~80 vulnerabilities reported by `npm audit` were from dependencies of `ember-cli-update`.
2019-07-15 19:17:56 +02:00
galfert 42fe01b349 Run codemods 2019-07-15 19:17:56 +02:00
galfert 80af572742 Run ember-cli-update --to 3.10.1 2019-07-15 19:17:56 +02:00
galfert d7a0817ac6 Update ember-cli-update 2019-07-15 19:17:56 +02:00
basti c65880972a Merge pull request #138 from 67P/feature/use_ember_auto_import
Use ember-auto-import instead of ember-browserify
2019-07-15 18:54:55 +02:00
galfert c94319e6a8 Use same fork of ndjson as ipfs-http-client does 2019-07-15 18:07:58 +02:00
galfert f9dbd47922 Add ndjson dependency
Required for ipfs-http-client dependency
2019-07-15 17:50:30 +02:00
galfert 636ac30622 Use ember-auto-import instead of ember-browserify 2019-07-15 17:11:41 +02:00
galfert 791943bb95 Merge pull request #137 from 67P/feature/mark_selected_profile
Mark contributor in list as selected when opening details
2019-07-12 19:02:00 +02:00
basti d9ae654f42 Mark contributor in list as selected when opening details 2019-07-12 16:02:32 +02:00
basti 8a156f964b Remove obsolete CSS 2019-07-12 16:02:21 +02:00
basti 743a4cc3ac Add release drafter config
Configures the Release Drafter app, which automatically adds draft
release notes with the titles of merged PRs.

https://github.com/organizations/67P/settings/installations/1265652
2019-07-12 12:04:29 +02:00
basti 5f1ec4887e 1.5.0 2019-07-12 11:50:55 +02:00
bumi fe16a605c1 Merge pull request #136 from 67P/dev/local_ipfs_gateway
Use local default IPFS gateway
2019-07-12 09:43:36 +00:00
basti 9cf743b02a Merge pull request #135 from 67P/feature/contributor_profiles
Contributor profiles
2019-07-12 11:42:26 +02:00
basti d7294d9d7f Remove debug log 2019-07-12 11:39:18 +02:00
basti d8a9886442 Use local default IPFS gateway
Same as for ipfs.kosmos.org, this turns document fetches into GET
requests, which will be cached.
2019-07-11 22:24:51 +02:00
basti d83d671580 Add account links to contributor profiles 2019-07-11 15:33:17 +02:00
basti 0bbca079c5 Link site name to dashboard
Enables closing of details pane
2019-07-11 10:02:50 +02:00
basti 8bd7a77d44 Only use 3-pane layout when profile selected
Later it can be used for contribution details and other things, too.
2019-07-11 09:59:03 +02:00
basti a73a4da575 Redirect index to dashboard 2019-07-11 09:43:24 +02:00
basti 8cc1b02d19 WIP Details pane + contributor profiles
Adds a 3-pane layout option for showing details, as well as a dashboard
sub-route for showing contributor details in the new details pane.
2019-07-11 09:35:01 +02:00
basti 6b49ca26c0 Add link title for contributions
Enables a user to read the whole title in case it's cut off
2019-07-10 15:00:44 +02:00
basti ffe555e2ee Cut off long contribution titles with an ellipsis
Prevents line breaks of titles
2019-07-10 14:48:18 +02:00
basti aa98b35796 1.4.0 2019-07-10 12:31:45 +02:00
galfert b2bbbfd010 Merge pull request #134 from 67P/feature/quick_filter
Add quick filters
2019-07-08 15:52:06 +02:00
basti edd7ffd3c5 Add quick filter for contribution kind 2019-07-06 16:16:28 +02:00
basti abb7d95804 Add quick filters 2019-07-06 15:49:29 +02:00
basti 0e0b1afe3a WIP: add quick filter buttons 2019-07-05 22:01:51 +02:00
basti 29a6d79d38 Merge pull request #132 from 67P/feature/ipfs_gateway
Use new IPFS gateway for document fetches
2019-07-02 18:46:10 +02:00
basti 07c579850e 1.3.0 2019-07-02 17:32:25 +02:00
basti 091ebf4b6a Use new IPFS gateway for GET fetches
When adding the gateway URL, the wrapper will do a normal fetch to
`https://ipfs.kosmos.org/ipfs/$hash` instead of using the generic POST
requests to the IPFS API endpoint. This enables normal browser caching
for all IPFS documents in this app.
2019-07-02 17:20:19 +02:00
basti b4e89492b4 1.2.0 2019-06-27 19:51:21 +02:00
galfert f5c89fd567 Merge pull request #131 from 67P/dev/version_script
Ensure share repo is updated when versioning
2019-06-27 17:51:11 +02:00
galfert 1fbf8f0769 Merge pull request #130 from 67P/feature/sort_contributions
Sort contributions by date instead of ID
2019-06-27 17:50:21 +02:00
basti cc8952f844 Ensure share repo is updated when versioning
Currently, it's easy to forget manually pushing to master after
versioning/building. In which case things can be merged remotely, and
when pulling with rebase the version commit ends up being changed and
behind what was actually tagged.

This adds a postversion script which simply pushes to the remote
automatically after the version commit and tag have been added by npm.
2019-06-27 16:40:42 +02:00
basti 25ea20e63b Sort contributions by date instead of ID
This changes the sorting to sort first by date and time, and only then
by ID.

refs #121
2019-06-27 16:38:56 +02:00
basti e60651858d 1.1.1 2019-06-27 16:37:37 +02:00
basti 24390c6503 Merge pull request #129 from 67P/feature/disable_travis_success_notifcations
Don't send emails for successful Travis builds
2019-06-27 16:18:56 +02:00
galfert c1d5c90745 Don't send emails for successful Travis builds 2019-06-27 16:09:24 +02:00
198 changed files with 60169 additions and 30804 deletions
-1
View File
@@ -4,7 +4,6 @@
root = true
[*]
end_of_line = lf
charset = utf-8
+36 -6
View File
@@ -1,8 +1,22 @@
'use strict';
module.exports = {
root: true,
parser: '@babel/eslint-parser',
parserOptions: {
ecmaVersion: 2017,
sourceType: 'module'
requireConfigFile: false,
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
legacyDecorators: true
},
babelOptions: {
babelrc: false,
configFile: false,
plugins: [
['@babel/plugin-proposal-decorators', { legacy: true }],
],
},
},
globals: {
console: true
@@ -19,7 +33,16 @@ module.exports = {
},
rules: {
'ember/avoid-leaking-state-in-ember-objects': 'warn',
'no-console': 'off'
'no-console': 'off',
'ember/no-jquery': 'error',
'ember/require-computed-property-dependencies': 'warn',
'ember/no-computed-properties-in-native-classes': 'warn',
'ember/no-observers': 'warn',
'ember/no-classic-classes': 'warn',
'ember/no-classic-components': 'warn',
'ember/no-controller-access-in-routes': 'warn',
'ember/no-actions-hash': 'warn',
'ember/require-tagless-components': 'warn'
},
overrides: [
// node files
@@ -35,13 +58,20 @@ module.exports = {
'server/**/*.js'
],
parserOptions: {
sourceType: 'script',
ecmaVersion: 2015
sourceType: 'script'
},
env: {
browser: false,
node: true
}
},
plugins: ['node'],
rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, {
// add your custom rules and overrides for node files here
// this can be removed once the following is fixed
// https://github.com/mysticatea/eslint-plugin-node/issues/77
'node/no-unpublished-require': 'off'
})
}
]
};
+4
View File
@@ -0,0 +1,4 @@
template: |
## Changes
$CHANGES
+88 -3
View File
@@ -1,9 +1,94 @@
'use strict';
module.exports = {
extends: 'recommended',
extends: 'octane',
rules: {
'simple-unless': false
}
'simple-unless': false,
'no-nested-interactive': false,
'no-html-comments': false
},
ignore: [
'kredits-web/templates/components/**',
'app/templates/components/**'
],
pending: [
{
"moduleId": "app/templates/dashboard",
"only": [
"no-action"
]
},
{
"moduleId": "app/components/add-contribution/template",
"only": [
"no-action",
"no-curly-component-invocation"
]
},
{
"moduleId": "app/components/add-contributor/template",
"only": [
"no-action",
"no-curly-component-invocation"
]
},
{
"moduleId": "app/components/contribution-list/template",
"only": [
"no-action",
"no-curly-component-invocation"
]
},
{
"moduleId": "app/components/contributor-list/template",
"only": [
"no-action"
]
},
{
"moduleId": "app/components/topbar-account-panel/template",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributions/new",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributions/resubmit",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributors/edit",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/contributors/new",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/signup/eth-account",
"only": [
"no-action"
]
},
{
"moduleId": "app/templates/signup/index",
"only": [
"no-action"
]
}
]
};
+10 -16
View File
@@ -1,27 +1,21 @@
---
language: node_js
node_js:
- "10"
- "14"
- "16"
sudo: false
dist: xenial
dist: precise
notifications:
email: false
addons:
chrome: stable
cache:
directories:
- $HOME/.npm
npm: false
env:
global:
# See https://git.io/vdao3 for details.
- JOBS=1
before_install:
- npm config set spin false
script:
- npm run lint:hbs
- npm run lint:js
- npm test
branches:
only:
- master
+29 -28
View File
@@ -5,23 +5,24 @@
This README outlines the details of collaborating on this Ember application.
A short introduction of this app could easily go here.
## Prerequisites
## Development
### Prerequisites
You will need the following things properly installed on your computer.
* [Git](https://git-scm.com/)
* [Node.js](https://nodejs.org/) (with npm)
* [Ember CLI](https://ember-cli.com/)
* [Google Chrome](https://google.com/chrome/)
* [Google Chrome](https://google.com/chrome/) (only for running tests)
## Installation
### Installation
* `git clone git@github.com:67P/kredits-web.git` this repository
* `cd kredits-web`
* `npm install`
## Running / Development
### Building/running for development
* `ember serve` - by default kredits-web connects to the Ethreum Kovan network
* Visit your app at [http://localhost:4200](http://localhost:4200).
@@ -29,11 +30,11 @@ You will need the following things properly installed on your computer.
See [working with locally deployed contracts](https://github.com/67P/kredits-web#working-with-locally-deployed-contracts) for details on how to develop with locally deployed contracts.
### Code Generators
### Code generators
Make use of the many generators for code, try `ember help generate` for more details
### Running Tests
### Running tests
* `ember test`
* `ember test --server`
@@ -55,58 +56,58 @@ _(You need collaborator permissions on the 5apps Deploy project.)_
`npm run deploy`
## Working with locally deployed contracts
### Working with locally deployed contracts
The smart contracts and their JavaScript wrapper library are developed in the
[kredits-contracts](https://github.com/67P/kredits-contracts) repo/package.
You can run `kredits-web` on your machine, against a local, simulated Ethereum
network, provided e.g. by [ganache](http://truffleframework.com/ganache/).
[kredits-contracts](https://github.com/67P/kredits-contracts) holds all the tools
to start and set up such a simulated network, as well as to deploy smart
contracts to it.
You can run `kredits-web` on your machine, against a local, simulated
blockchain. [kredits-contracts](https://github.com/67P/kredits-contracts)
contains all the tools to start and set up such a simulated network, as well as
to deploy the Kredits smart contracts to it.
These are the basic steps to get up and running:
#### 1. IPFS
Run a local IPFS deamon in offline mode.
Run a local IPFS deamon.
* Make sure CORS headers are configured. See [IPFS](#ipfs) for more info.
* `ipfs daemon --offline`
* `ipfs daemon`
#### 2. kredits-contracts
Get your local Ethereum development node running. (See [kredits-contracts README](https://github.com/67P/kredits-contracts)
for details.
Run a local devchain with test data. (See [kredits-contracts
README](https://github.com/67P/kredits-contracts) for details.
* Clone [kredits-contracts](https://github.com/67P/kredits-contracts)
* `npm install`
* `npm run devchain` - runs a local development chain
* `npm run bootstrap` - bootstrap runs deploys all the contracts
* `npm link` - make the `kredits-contracts` module linkable as `kredits-contracts` on your machine (currently broken :( )
* `npm run bootstrap` - deploys all contracts and seeds test data
* `npm link` - makes the `kredits-contracts` module linkable as `kredits-contracts` on your machine
#### 3. kredits-web
With IPFS and Ethereum/ganache running, you can now start this Ember app.
With IPFS and the local devchain running, you can now link the contracts and
start the Ember app:
* `npm link kredits-contracts` - link the local `kredits-contracts` package
* `npm run start:local` - WEB3_PROVIDER_URL=http://localhost:7545 must be set for local settings
* `npm link kredits-contracts` - links the local `kredits-contracts` package (has to be done again after every `npm install`)
* `npm run start:local` - runs the Ember app with WEB3_PROVIDER_URL=http://localhost:8545 set
If you restart the devchain your network ID changes. So kredits-contracts does not find the correct DAO Kernel address.
If that is the case the DAO address must be provided as `KREDITS_KERNEL_ADDRESS` environment variable.
#### 4. Metamask network
* `KREDITS_KERNEL_ADDRESS=<0xYOURADDRESS> npm run start:local` - find your address with the `npm run dao:address` command in the kredits-contracts repo.
If you want to interact with the local contracts via a Web3 wallet, switch the
network to a "Custom RPC" one, with the RPC URL `http://localhost:8545`.
#### IPFS
Install IPFS with your favorite package manager and run
If you haven't configured your IPFS node for CORS yet, you can do so by running
the following commands:
ipfs init (on initial installation)
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["localhost:4200"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "GET", "POST"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
ipfs config Addresses.Gateway /ip4/127.0.0.1/tcp/8080
## Further Reading / Useful Links
+6 -8
View File
@@ -1,14 +1,12 @@
import Application from '@ember/application';
import Resolver from './resolver';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from './config/environment';
const App = Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver
});
export default class App extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
}
loadInitializers(App, config.modulePrefix);
export default App;
+30 -12
View File
@@ -1,11 +1,23 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed';
import { assign } from '@ember/polyfills';
import moment from 'moment';
import { inject as service } from '@ember/service';
export default Component.extend({
kredits: service(),
attributes: null,
contributors: Object.freeze([]),
contributors: computed('kredits.contributorsSorted.[]', function() {
return this.kredits.contributorsSorted.map(c => {
return {
id: c.id.toString(),
name: c.name
}
})
}),
isValidContributor: notEmpty('contributorId'),
isValidKind: notEmpty('kind'),
@@ -21,18 +33,19 @@ export default Component.extend({
init () {
this._super(...arguments);
this.set('defaultDate', new Date());
// Default attributes used by reset
this.set('attributes', {
this.set('defaultDate', moment().startOf('hour').toDate());
this.set('defaultAttr', {
contributorId: null,
kind: null,
date: [new Date()],
date: this.defaultDate,
amount: null,
description: null,
url: null,
details: null
});
this.set('attributes', assign({}, this.defaultAttr, this.attributes));
this.reset();
},
@@ -42,15 +55,23 @@ export default Component.extend({
actions: {
submit () {
submit (evt) {
evt.preventDefault();
if (!this.isValid) {
alert('Invalid data. Please review and try again.');
return;
}
const attributes = this.getProperties(Object.keys(this.attributes));
const [ date/* , time */ ] = attributes.date[0].toISOString().split('T');
attributes.date = date;
attributes.contributorId = parseInt(this.contributorId);
let dateInput = (attributes.date instanceof Array) ?
attributes.date[0] : attributes.date;
const [ date, time ] = dateInput.toISOString().split('T');
[ attributes.date, attributes.time ] = [ date, time ];
this.set('inProgress', true);
@@ -62,9 +83,6 @@ export default Component.extend({
window.alert('Something went wrong. Check the browser console for details.');
})
.finally(() => this.set('inProgress', false));
}
}
});
+81 -53
View File
@@ -1,58 +1,86 @@
<form {{action "submit" on="submit"}}>
<p>
<select required onchange={{action (mut contributorId) value="target.value"}}>
<option value="" selected disabled hidden>Contributor</option>
{{#each contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}}
</select>
</p>
<p>
<select required onchange={{action (mut kind) value="target.value"}}>
<option value="" selected disabled hidden>Kind</option>
<option value="community" selected={{eq kind "community"}}>Community</option>
<option value="design" selected={{eq kind "design"}}>Design</option>
<option value="dev" selected={{eq kind "dev"}}>Development</option>
<option value="docs" selected={{eq kind "docs"}}>Documentation</option>
<option value="ops" selected={{eq kind "ops"}}>IT Operations</option>
<option value="special" selected={{eq kind "special"}}>Special</option>
</select>
</p>
<p>
{{ember-flatpickr
allowInput=false
altFormat="F j, Y"
altInput=true
altInputClass="date-alt"
date=date
dateFormat="Y-m-d"
defaultDate=defaultDate
maxDate=defaultDate
onChange=(action (mut date))
}}
</p>
<p>
{{input type="text"
placeholder="500"
value=amount
class=(if isValidAmount "valid" "")}}
</p>
<p>
{{input type="text"
placeholder="Description"
value=description
class=(if isValidDescription "valid" "")}}
</p>
<p>
{{input type="text"
placeholder="URL (optional)"
value=url
class=(if isValidUrl "valid" "")}}
</p>
<form onsubmit={{action "submit"}}>
<label>
<p class="label">Contributor:</p>
<p>
<select required onchange={{action (mut this.contributorId) value="target.value"}}>
<option value="" selected disabled hidden></option>
{{#each this.contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}}
</select>
</p>
</label>
<label>
<p class="label">Kind:</p>
<p>
<select required onchange={{action (mut this.kind) value="target.value"}}>
<option value="" selected disabled hidden></option>
<option value="bureaucracy" selected={{eq this.kind "bureaucracy"}}>Bureaucracy</option>
<option value="community" selected={{eq this.kind "community"}}>Community</option>
<option value="design" selected={{eq this.kind "design"}}>Design</option>
<option value="dev" selected={{eq this.kind "dev"}}>Development</option>
<option value="docs" selected={{eq this.kind "docs"}}>Documentation</option>
<option value="ops" selected={{eq this.kind "ops"}}>IT Operations</option>
<option value="outreach" selected={{eq this.kind "outreach"}}>Outreach</option>
<option value="qa" selected={{eq this.kind "qa"}}>Quality Assurance</option>
<option value="special" selected={{eq this.kind "special"}}>Special</option>
</select>
</p>
</label>
<label>
<p class="label">Date:</p>
<p>
{{ember-flatpickr
date=this.date
defaultDate=this.defaultDate
maxDate=this.defaultDate
enableTime=true
time_24hr=true
onChange=(action (mut this.date))
}}
</p>
</label>
<label>
<p class="label">Amount:</p>
<p>
{{input type="text"
placeholder="500"
value=this.amount
class=(if this.isValidAmount "valid" "")}}
</p>
</label>
<label>
<p class="label">Description:</p>
<p>
{{input type="text"
value=this.description
class=(if this.isValidDescription "valid" "")}}
</p>
</label>
<label>
<p class="label">URL (optional):</p>
<p>
{{input type="text"
value=this.url
class=(if this.isValidUrl "valid" "")}}
</p>
</label>
{{#if this.details}}
<label>
<p class="label">Details:</p>
<p>
<pre>
{{this.details}}
</pre>
</p>
</label>
{{/if}}
<p class="actions">
{{input type="submit"
disabled=inProgress
value=(if inProgress "Processing" "Save")}}
disabled=this.inProgress
value=(if this.inProgress "Processing" "Save")}}
</p>
</form>
+20 -5
View File
@@ -1,22 +1,29 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { isPresent } from '@ember/utils';
import { isAddress } from 'web3-utils';
export default Component.extend({
router: service(),
kredits: service(),
attributes: null,
// TODO: add proper address validation
isValidAccount: notEmpty('account'),
isValidAccount: computed('account', function() {
return isAddress(this.account);
}),
isValidName: notEmpty('name'),
isValidURL: notEmpty('url'),
isValidGithubUID: notEmpty('github_uid'),
isValidGithubUsername: notEmpty('github_username'),
isValidGiteaUsername: notEmpty('gitea_username'),
isValidWikiUsername: notEmpty('wiki_username'),
isValidZoomDisplayName: notEmpty('zoom_display_name'),
isValid: and(
'isValidAccount',
'isValidName',
@@ -42,7 +49,8 @@ export default Component.extend({
github_username: null,
github_uid: null,
gitea_username: null,
wiki_username: null
wiki_username: null,
zoom_display_name: null
});
},
@@ -52,9 +60,15 @@ export default Component.extend({
actions: {
submit () {
submit (evt) {
evt.preventDefault();
if (!this.kredits.currentUserIsCore) {
window.alert('Only core team members can edit profiles. Please ask someone to set you up.');
return;
}
if (!this.isValid) {
alert('Invalid data. Please review and try again.');
window.alert('Invalid data. Please review and try again.');
return;
}
@@ -70,6 +84,7 @@ export default Component.extend({
window.alert('Something went wrong. Please check the browser console.');
}).finally(() => {
this.set('inProgress', false);
this.router.transitionTo('dashboard');
});
}
+28 -22
View File
@@ -1,50 +1,56 @@
<form {{action "submit" on="submit"}}>
<form onsubmit={{action "submit"}}>
<p>
<label for="c-account">Ethereum account</label>
{{input name="account" id="c-account"
type="text" value=account
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
class=(if isValidAccount "valid" "")}}
<Input @type="text"
@value={{this.account}}
name="account" id="c-account"
placeholder="0xF18E631Ea191aE4ebE70046Fcb01a436554421BA4"
class={{if this.isValidAccount "valid" ""}} />
</p>
<p>
<label for="c-kind">Kind</label>
<select required onchange={{action (mut kind) value="target.value"}} id="c-kind">
<option value="person" selected={{eq kind "person"}}>Person</option>
<option value="organization" selected={{eq kind "organization"}}>Organization</option>
<select required onchange={{action (mut this.kind) value="target.value"}} id="c-kind">
<option value="person" selected={{eq this.kind "person"}}>Person</option>
<option value="organization" selected={{eq this.kind "organization"}}>Organization</option>
</select>
</p>
<p>
<label for="c-name">Name</label>
{{input name="name" type="text" value=name placeholder="Zero Cool"
class=(if isValidName "valid" "") id="c-name"}}
{{input name="name" type="text" value=this.name placeholder="Zero Cool"
class=(if this.isValidName "valid" "") id="c-name"}}
</p>
<p>
<label for="c-url">URL</label>
{{input name="url" type="text" value=url placeholder="http://zerocool.bit"
class=(if isValidURL "valid" "") id="c-url"}}
{{input name="url" type="text" value=this.url placeholder="http://zerocool.bit"
class=(if this.isValidURL "valid" "") id="c-url"}}
</p>
<p>
<label for="c-github-uid">GitHub UID</label>
{{input name="github_uid" type="text" value=github_uid placeholder="2342"
class=(if isValidGithubUID "valid" "") id="c-github-uid"}}
{{input name="github_uid" type="text" value=this.github_uid placeholder="2342"
class=(if this.isValidGithubUID "valid" "") id="c-github-uid"}}
</p>
<p>
<label for="c-github-username">GitHub username</label>
{{input name="github_username" type="text" value=github_username placeholder="zerocool"
class=(if isValidGithubUsername "valid" "") id="c-github-username"}}
{{input name="github_username" type="text" value=this.github_username placeholder="zerocool"
class=(if this.isValidGithubUsername "valid" "") id="c-github-username"}}
</p>
<p>
<label for="c-gitea-username">Gitea username</label>
{{input name="gitea_username" type="text" value=gitea_username placeholder="zerocool"
class=(if isValidGiteaUsername "valid" "") id="c-gitea-username"}}
{{input name="gitea_username" type="text" value=this.gitea_username placeholder="zerocool"
class=(if this.isValidGiteaUsername "valid" "") id="c-gitea-username"}}
</p>
<p>
<label for="c-wiki-username">Wiki username</label>
{{input name="wiki_username" type="text" value=wiki_username placeholder="ZeroCool"
class=(if isValidWikiUsername "valid" "") id="c-wiki-username"}}
{{input name="wiki_username" type="text" value=this.wiki_username placeholder="ZeroCool"
class=(if this.isValidWikiUsername "valid" "") id="c-wiki-username"}}
</p>
<p>
<label for="c-zoom-display-name">Zoom display name</label>
{{input name="zoom_display_name" type="text" value=this.zoom_display_name placeholder="Zero Cool"
class=(if this.isValidZoomDisplayName "valid" "") id="c-zoom-display-name"}}
</p>
<p class="actions">
{{input type="submit" disabled=inProgress
value=(if inProgress "Processing" "Save")}}
{{input type="submit" disabled=this.inProgress
value=(if this.inProgress "Processing" "Save")}}
</p>
</form>
@@ -0,0 +1,86 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import moment from 'moment';
import isValidAmount from 'kredits-web/utils/is-valid-amount';
import { isPresent } from '@ember/utils';
export default class AddExpenseItemComponent extends Component {
@tracked amount = '0';
@tracked currency = 'EUR';
@tracked date = moment().startOf('hour').toDate();
@tracked title = '';
@tracked description = '';
@tracked url = '';
@tracked tags = '';
defaultDate = moment().startOf('hour').toDate();
currencies = [
{ code: 'EUR' },
{ code: 'USD' }
];
get isValidAmount () {
return isValidAmount(this.amount);
}
get amountInputClass () {
return this.isValidTotal ? 'valid' : '';
}
validateForm () {
const formEl = document.querySelector('form#add-expense-item');
const inputFields = formEl.querySelectorAll('input');
inputFields.forEach(i => i.classList.remove('invalid'));
let validity = true;
if (!this.isValidAmount) {
document.querySelector('input[name=expense-amount]').classList.add('invalid');
validity = false;
}
if (!formEl.checkValidity()) {
inputFields.forEach(i => {
if (!i.validity.valid) {
i.classList.add('invalid');
validity = false;
}
})
}
return validity;
}
@action
updateCurrency(event) {
this.currency = event.target.value;
}
@action
submit (e) {
e.preventDefault();
let dateInput = (this.date instanceof Array) ?
this.date[0] : this.date;
const [ date ] = dateInput.toISOString().split('T');
const isValid = this.validateForm();
if (!isValid) return false;
const expense = {
amount: parseFloat(this.amount),
currency: this.currency,
date: date,
title: this.title,
description: isPresent(this.description) ? this.description : undefined,
url: isPresent(this.url) ? this.url : undefined,
}
if (isPresent(this.tags)) {
expense.tags = this.tags.split(',').map(t => t.trim());
}
this.args.addExpenseItem(expense);
}
}
@@ -0,0 +1,69 @@
<form id="add-expense-item" {{on "submit" this.submit}} novalidate>
<fieldset class="horizontal">
<label>
<p class="label">Amount:</p>
<p>
<Input @name="expense-amount"
@type="text"
@placeholder="10"
@value={{this.amount}}
@required={{true}}
@pattern="([0-9]*[.])?[0-9]+"
@class={{this.amountInputClass}} />
</p>
</label>
<label>
<p class="label">Currency:</p>
<p>
<select required name="expense-currency" {{on "change" this.updateCurrency}}>
<option value="" selected disabled hidden></option>
{{#each this.currencies as |currency|}}
<option value={{currency.code}} selected={{eq this.currency currency.code}}>{{currency.code}}</option>
{{/each}}
</select>
</p>
</label>
</fieldset>
<label>
<p class="label">Date:</p>
<p>
<EmberFlatpickr @date={{this.date}}
@defaultDate={{this.defaultDate}}
@maxDate={{this.defaultDate}}
@enableTime={{false}}
@onChange={{fn (mut this.date)}} />
</p>
</label>
<label>
<p class="label">Title:</p>
<p>
<Input @name="expense-title"
@type="text"
@value={{this.title}}
@required={{true}} />
</p>
</label>
<label>
<p class="label">Description (optional):</p>
<p>
<Input @name="expense-description" @type="text" @value={{this.description}} />
</p>
</label>
<label>
<p class="label">URL (optional):</p>
<p>
<Input @name="expense-url" @type="url" @value={{this.url}} />
</p>
</label>
<label>
<p class="label">Tags (comma-separated, optional):</p>
<p>
<Input @name="expense-tags" @type="text" @value={{this.tags}} />
</p>
</label>
<p class="actions">
<Input @type="submit" @value="Add" @class="green"
@title="Add item to reimbursement" />
</p>
</form>
-64
View File
@@ -1,64 +0,0 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { and, notEmpty } from '@ember/object/computed';
export default Component.extend({
attributes: null,
contributors: Object.freeze([]),
isValidContributor: notEmpty('contributorId'),
isValidAmount: computed('amount', function() {
return parseInt(this.amount, 10) > 0;
}),
isValidDescription: notEmpty('description'),
isValidUrl: notEmpty('url'),
isValid: and('isValidContributor',
'isValidAmount',
'isValidDescription'),
init () {
this._super(...arguments);
// Default attributes used by reset
this.set('attributes', {
contributorId: null,
kind: 'community',
amount: null,
description: null,
url: null,
});
},
didInsertElement() {
this._super(...arguments);
this.reset();
},
reset: function() {
this.setProperties(this.attributes);
},
actions: {
submit() {
if (!this.isValid) {
alert('Invalid data. Please review and try again.');
return;
}
let attributes = Object.keys(this.attributes);
let proposal = this.getProperties(attributes);
let saved = this.save(proposal);
// The promise handles inProgress
this.set('inProgress', saved);
saved.then(() => {
this.reset();
window.scroll(0,0);
window.alert('Proposal added.');
});
}
}
});
-44
View File
@@ -1,44 +0,0 @@
<form {{action "submit" on="submit"}}>
<p>
<select required onchange={{action (mut contributorId) value="target.value"}}>
<option value="" selected disabled hidden>Contributor</option>
{{#each contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq contributorId contributor.id}}>{{contributor.github_username}}</option>
{{/each}}
</select>
</p>
<p>
<select required onchange={{action (mut kind) value="target.value"}}>
<option value="community" selected={{eq kind "community"}}>Community</option>
<option value="design" selected={{eq kind "design"}}>Design</option>
<option value="dev" selected={{eq kind "dev"}}>Development</option>
<option value="docs" selected={{eq kind "docs"}}>Documentation</option>
<option value="ops" selected={{eq kind "ops"}}>IT Operations</option>
</select>
</p>
<p>
{{input type="text"
placeholder="100"
value=amount
class=(if isValidAmount "valid" "")}}
</p>
<p>
{{input type="text"
placeholder="Description"
value=description
class=(if isValidDescription "valid" "")}}
</p>
<p>
{{input type="text"
placeholder="URL (optional)"
value=url
class=(if isValidUrl "valid" "")}}
</p>
<p class="actions">
{{input type="submit"
disabled=(is-pending inProgress)
value=(if (is-pending inProgress) "Processing" "Save")}}
{{#link-to "index"}}Back{{/link-to}}
</p>
</form>
@@ -0,0 +1,147 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { A } from '@ember/array';
import { scheduleOnce } from '@ember/runloop';
import isValidAmount from 'kredits-web/utils/is-valid-amount';
import config from 'kredits-web/config/environment';
export default class AddReimbursementComponent extends Component {
@service router;
@service kredits;
@service exchangeRates;
@alias('kredits.contributorsSorted') contributors;
@tracked recipientId = null;
@tracked title = '';
@tracked total = '0';
@tracked expenses = A([]);
@tracked expenseFormVisible = true;
constructor() {
super(...arguments);
this.exchangeRates.fetchRates();
}
get isValidTotal () {
return isValidAmount(this.total);
}
get totalInputClass () {
return this.isValidTotal ? 'valid' : '';
}
get totalEUR () {
const expenses = this.expenses.filterBy('currency', 'EUR');
if (expenses.length > 0) {
return expenses.mapBy('amount')
.reduce((summation, current) => summation + current);
} else {
return 0;
}
}
get totalUSD () {
const expenses = this.expenses.filterBy('currency', 'USD');
if (expenses.length > 0) {
return expenses.mapBy('amount')
.reduce((summation, current) => summation + current);
} else {
return 0;
}
}
get submitButtonEnabled () {
return this.isValidTotal &&
(this.expenses.length > 0);
}
get submitButtonDisabled () {
return !this.submitButtonEnabled;
}
scrollToExpenseItemForm () {
const anchor = document.getElementById('new-expense-item');
anchor.scrollIntoView();
}
updateTotalAmountFromFiat() {
let btcAmount = parseFloat(this.total);
if (this.exchangeRates.btceur > 0 && this.totalEUR > 0) {
btcAmount += (this.totalEUR / this.exchangeRates.btceur);
}
if (this.exchangeRates.btcusd > 0 && this.totalUSD > 0) {
btcAmount += (this.totalUSD / this.exchangeRates.btcusd);
}
if (this.totalUSD === 0 && this.totalEUR === 0) {
btcAmount = 0;
}
this.total = btcAmount.toFixed(8);
}
@action
updateContributor(event) {
this.recipientId = event.target.value;
}
@action
showExpenseForm () {
this.expenseFormVisible = true;
scheduleOnce('afterRender', this, this.scrollToExpenseItemForm);
}
@action
addExpenseItem (expenseItem) {
this.expenses.pushObject(expenseItem);
this.updateTotalAmountFromFiat();
this.expenseFormVisible = false;
}
@action
removeExpenseItem (expenseItem) {
this.expenses.removeObject(expenseItem);
this.updateTotalAmountFromFiat();
if (this.expenses.length === 0) {
this.expenseFormVisible = true;
}
}
@action
submit (e) {
e.preventDefault();
if (!this.kredits.currentUser) { window.alert('You need to connect your RSK account first.'); return false }
if (!this.kredits.currentUserIsCore) { window.alert('Only core contributors can submit reimbursements.'); return false }
const contributor = this.contributors.findBy('id', parseInt(this.recipientId));
const attributes = {
amount: parseInt(parseFloat(this.total) * 100000000), // convert to sats
token: config.tokens['BTC'],
recipientId: parseInt(this.recipientId),
title: `Expenses covered by ${contributor.name}`,
description: this.description,
url: this.url,
expenses: JSON.parse(JSON.stringify((this.expenses)))
}
this.inProgress = true;
this.kredits.addReimbursement(attributes)
.then((/* reimbursement */) => {
this.router.transitionTo('budget');
})
.catch(e => {
console.error('Could not add reimbursement:', e);
window.alert('Something went wrong. Please check the browser console.')
})
.finally(() => {
this.inProgress = false;
});
}
}
@@ -0,0 +1,89 @@
<form {{on "submit" this.submit}} novalidate>
<label>
<p class="label">Contributor:</p>
<p>
<select required {{on "change" this.updateContributor}}>
<option value="" selected disabled hidden></option>
{{#each this.contributors as |contributor|}}
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}}
</select>
</p>
</label>
<fieldset class="horizontal thirds total-amounts">
<label>
<p class="label">Total amount (BTC):</p>
<p>
<Input @type="text"
@placeholder="0.0015"
@value={{this.total}}
@required={{true}}
@pattern="([0-9]*[.])?[0-9]+"
@class={{this.totalInputClass}} />
</p>
</label>
<label>
<p class="label">EUR total</p>
<p>
<Input @type="text"
@name="total-eur"
@value={{this.totalEUR}}
@disabled={{true}} />
</p>
</label>
<label>
<p class="label">USD total</p>
<p>
<Input @type="text"
@name="total-usd"
@value={{this.totalUSD}}
@disabled={{true}} />
</p>
</label>
</fieldset>
<h3>Expense items</h3>
{{#if this.expenses}}
<ul class="expense-list">
{{#each this.expenses as |expense|}}
<li>
<div class="description" rowspan="2">
<h4>
<span class="date">{{expense.date}}:</span>
<span class="title">{{expense.title}}</span>
</h4>
<p class="description">{{expense.description}}</p>
</div>
<div class="amount">
{{fmt-fiat-currency expense.amount expense.currency}}
</div>
<div class="actions">
<button {{on "click" (fn this.removeExpenseItem expense)}}
class="danger small" type="button">delete</button>
</div>
</li>
{{/each}}
</ul>
<p class="actions">
<button {{on "click" this.showExpenseForm}}
class="green small" type="button">+ Add another item</button>
</p>
{{else}}
<p>No line items yet.</p>
{{/if}}
<p class="actions">
{{#if this.inProgress}}
<Input @type="submit" @value="Submitting..." @disabled={{true}}
@title="Submit/propose this reimbursement" />
{{else}}
<Input @type="submit" @value="Submit" @disabled={{this.submitButtonDisabled}}
@title="Submit/propose this reimbursement" />
{{/if}}
</p>
{{#if this.expenseFormVisible}}
<h3 id="new-expense-item">New expense item</h3>
<AddExpenseItem @addExpenseItem={{fn this.addExpenseItem}} />
{{/if}}
</form>
@@ -0,0 +1,12 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
export default class BudgetBalancesComponent extends Component {
@service communityFunds
@alias('communityFunds.balances') balances;
get loading () {
return !this.communityFunds.balancesLoaded;
}
}
@@ -0,0 +1,18 @@
<table class="token-balances {{if this.loading 'loading'}}">
<thead>
<tr>
<th>Token</th>
<th>Amount</th>
<th>Fiat value</th>
</tr>
</thead>
<tbody>
{{#each this.balances as |balance|}}
<tr>
<th>{{balance.token.symbol}}</th>
<td class="amount">{{balance.confirmed_balance}}</td>
<td class="fiat-amount">~{{balance.balanceUSD}} USD</td>
</tr>
{{/each}}
</tbody>
</table>
@@ -1,7 +1,6 @@
<div class="chart">
{{ember-chart type="doughnut"
data=chartData
options=chartOptions
width=200
height=200}}
<EmberChart @type="doughnut"
@data={{this.chartData}}
@options={{this.chartOptions}}
@width="200" @height="200" />
</div>
+51 -2
View File
@@ -1,9 +1,54 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { sort } from '@ember/object/computed';
import { isPresent } from '@ember/utils';
import { inject as service } from '@ember/service';
export default Component.extend({
tagName: 'ul',
classNames: ['contribution-list'],
router: service(),
tagName: 'div',
classNames: ['contributions'],
selectedContribution: null,
showQuickFilter: false,
hideSmallContributions: false,
contributorId: null,
contributionKind: null,
kredits: service(),
contributorsSorting: Object.freeze(['name:asc']),
contributors: sort('kredits.contributors', 'contributorsSorting'),
contributorsActive: computed('contributors.[]', 'contributions', function() {
const activeIds = new Set(this.contributions.mapBy('contributorId'));
return this.contributors.filter(c => activeIds.has(c.id));
}),
contributionKinds: computed('contributions.[]', function() {
return this.contributions.mapBy('kind').uniq();
}),
contributionsFiltered: computed('contributions.[]', 'hideSmallContributions', 'contributorId', 'contributionKind', function() {
return this.contributions.filter(c => {
let included = true;
if (this.hideSmallContributions &&
c.amount <= 500) { included = false; }
if (isPresent(this.contributorId) &&
c.contributorId !== parseInt(this.contributorId)) { included = false; }
if (isPresent(this.contributionKind) &&
c.kind !== this.contributionKind) { included = false; }
return included;
});
}),
actions: {
@@ -13,6 +58,10 @@ export default Component.extend({
} else {
window.alert('Only members can veto contributions. Please ask someone to set you up.');
}
},
openContributionDetails(contribution) {
this.router.transitionTo('dashboard.contributions.show', contribution);
}
}
+54 -23
View File
@@ -1,25 +1,56 @@
{{#each contributions as |contribution|}}
<li data-contribution-id={{contribution.id}} class="{{contribution-status contribution}} {{if contribution.vetoed "vetoed"}}">
<p class="meta">
<span class="recipient">{{user-avatar contributor=contribution.contributor}}</span>
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
<span class="title">
{{#if contribution.url}}
<a href={{contribution.url}} target="_blank" rel="noopener">{{contribution.description}}</a>
{{else}}
{{contribution.description}}
{{/if}}
</span>
{{#if @showQuickFilter}}
<div class="quick-filter">
<p>
<label class="filter-contributor">
Contributor:
<select onchange={{action (mut this.contributorId) value="target.value"}}>
<option value="" selected>all</option>
{{#each this.contributorsActive as |contributor|}}
<option value={{contributor.id}} selected={{eq this.contributorId contributor.id}}>{{contributor.name}}</option>
{{/each}}
</select>
</label>
<label class="filter-contribution-kind">
Kind:
<select onchange={{action (mut this.contributionKind) value="target.value"}}>
<option value="" selected>all</option>
{{#each this.contributionKinds as |kind|}}
<option value={{kind}} selected={{eq this.contributionKind kind}}>{{capitalize-string kind}}</option>
{{/each}}
</select>
</label>
<label class="filter-contribution-size">
<Input @type="checkbox" @checked={{this.hideSmallContributions}} />
Hide small contributions
</label>
</p>
<p class="kredits-amount">
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
</p>
{{#unless contribution.vetoed}}
{{#unless (is-confirmed-contribution contribution)}}
<p class="voting">
<button {{action "veto" contribution.id}} class="small danger">veto</button>
</p>
</div>
{{/if}}
<ul class="item-list contribution-list {{if @loading 'loading'}}">
{{#each this.contributionsFiltered as |contribution|}}
<li role="button" data-contribution-id={{contribution.id}}
{{action "openContributionDetails" contribution}}
class="{{item-status contribution}}{{if (eq contribution.id @selectedContributionId) " selected"}}">
<p class="meta">
<span class="recipient"><UserAvatar @contributor={{contribution.contributor}} /></span>
<span class="category {{contribution.kind}}">({{contribution.kind}})</span>
<span class="title">{{contribution.description}}</span>
</p>
<p class="kredits-amount">
<span class="amount">{{contribution.amount}}</span><span class="symbol">₭S</span>
</p>
{{#unless contribution.vetoed}}
{{#unless (is-confirmed-contribution contribution)}}
<p class="voting">
{{input type="button" class="button small danger" value="veto"
click=(action "veto" contribution.id)
disabled=contribution.hasPendingChanges}}
</p>
{{/unless}}
{{/unless}}
{{/unless}}
</li>
{{/each}}
</li>
{{/each}}
</ul>
+7 -11
View File
@@ -1,22 +1,18 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
export default Component.extend({
tagName: '',
tagName: 'table',
classNames: 'contributor-list',
selectedContributor: null,
router: service(),
selectedContributorId: null,
actions: {
toggleContributorInfo(contributor) {
if (contributor.showMetadata) {
contributor.set('showMetadata', false);
} else {
this.contributorList.setEach('showMetadata', false);
contributor.set('showMetadata', true);
}
openContributorDetails(contributor) {
this.router.transitionTo('dashboard.contributors.show', contributor);
}
}
});
+25 -39
View File
@@ -1,39 +1,25 @@
<tbody>
{{#each contributorList as |c|}}
<tr role="button" class={{if (is-current-user c.contributor) "current-user"}} {{action "toggleContributorInfo" c}}>
<td class="person">
{{user-avatar contributor=c.contributor}} {{c.contributor.name}}
</td>
<td class="kredits">
<span class="amount">
{{#if showUnconfirmedKredits}}
{{c.amountTotal}}
{{else}}
{{c.amountConfirmed}}
{{/if}}
</span>
<span class="symbol">₭S</span>
</td>
</tr>
<tr class="metadata {{if (is-current-user c.contributor) "current-user"}} {{if c.showMetadata "visible"}}">
<td colspan="2">
<ul>
<li>
<a href="https://rinkeby.etherscan.io/address/{{c.contributor.account}}" target="_blank" rel="noopener">Inspect Ethereum transactions</a>
</li>
{{#if c.contributor.ipfsHash}}
<li>
<a href="https://ipfs.io/ipfs/{{c.contributor.ipfsHash}}" target="_blank" rel="noopener">Inspect IPFS profile</a>
</li>
{{/if}}
<li>
{{link-to "Edit profile" "contributors.edit" c.contributor.id}}
</li>
</ul>
{{#if c.showMetadata}}
<pre>{{c.contributor.ipfsData}}</pre>
{{/if}}
</td>
</tr>
{{/each}}
</tbody>
<table class="contributor-list {{if @loading 'loading'}}">
<thead>
</thead>
<tbody>
{{#each @contributorList as |c|}}
<tr role="button"
onclick={{action "openContributorDetails" c.contributor}}
class="{{if (is-current-user c.contributor) "current-user"}} {{if (eq c.contributor.id @selectedContributorId) "selected"}}">
<td class="person">
<UserAvatar @contributor={{c.contributor}} /> {{c.contributor.name}}
</td>
<td class="kredits">
<span class="amount">
{{#if @showUnconfirmedKredits}}
{{c.amountTotal}}
{{else}}
{{c.amountConfirmed}}
{{/if}}
</span>
<span class="symbol">₭S</span>
</td>
</tr>
{{/each}}
</tbody>
</table>
@@ -0,0 +1,15 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
export default Component.extend({
tagName: "",
account: null,
iconComponentName: computed('account.site', function() {
if (this.account.site.match(/github|gitea|wiki|zoom/)) {
return 'icon-account-' + this.account.site.replace(/\./g, '-');
} else {
return 'icon-web-globe';
}
})
});
@@ -0,0 +1,5 @@
<a href={{this.account.url}} target="_blank" rel="noopener noreferrer"
title="{{this.account.username}} on {{this.account.site}}">
<span class="site">{{this.account.site}}</span>
{{component this.iconComponentName}}
</a>
-20
View File
@@ -1,20 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'ul',
classNames: ['proposal-list'],
actions: {
confirm(proposalId) {
if (this.contractInteractionEnabled) {
this.confirmProposal(proposalId);
} else {
window.alert('Only members can vote on proposals. Please ask someone to set you up.');
}
}
}
});
-20
View File
@@ -1,20 +0,0 @@
{{#each proposals as |proposal|}}
<li data-proposal-id={{proposal.id}} class={{if proposal.isExecuted "confirmed" "unconfirmed"}}>
<p class="meta">
<span class="category {{proposal.kind}}">♥ ({{proposal.kind}})</span>
<span class="recipient">{{proposal.contributor.name}}:</span>
</p>
<p class="kredits-amount">
<span class="amount">{{proposal.amount}}</span><span class="symbol">₭S</span>
</p>
<p class="description">
<span class="description">{{proposal.description}}</span>
</p>
<p class="voting">
{{#unless proposal.isExecuted}}
<span class="votes">({{proposal.votesCount}}/{{proposal.votesNeeded}} votes)</span>
<button {{action "confirm" proposal.id}}>+1</button>
{{/unless}}
</p>
</li>
{{/each}}
@@ -0,0 +1,7 @@
import Component from '@glimmer/component';
import { sort } from '@ember/object/computed';
export default class ReimbursementListComponent extends Component {
itemSorting = Object.freeze(['pendingStatus:asc', 'id:desc']);
@sort('args.items', 'itemSorting') itemsSorted;
}
@@ -0,0 +1,31 @@
<ul class="item-list spaced reimbursement-list {{if @loading 'loading'}}">
{{#each this.itemsSorted as |reimbursement|}}
<li data-reimbursement-id={{reimbursement.id}}
class="{{item-status reimbursement}}">
<p class="meta">
<span class="recipient"><UserAvatar @contributor={{reimbursement.contributor}} /></span>
<span class="title">Expenses covered by {{reimbursement.contributor.name}}</span>
</p>
<p class="token-amount">
<span class="amount">
{{sats-to-btc reimbursement.amount}}</span>&#8239;<span class="symbol">BTC</span>
</p>
<ul class="expense-list">
{{#each reimbursement.expenses as |expense|}}
<li>
<div class="description" rowspan="2">
<h4>
<span class="date">{{expense.date}}:</span>
<span class="title">{{expense.title}}</span>
</h4>
<p class="description">{{expense.description}}</p>
</div>
<div class="amount">
{{fmt-fiat-currency expense.amount expense.currency}}
</div>
</li>
{{/each}}
</ul>
</li>
{{/each}}
</ul>
@@ -0,0 +1,45 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { isPresent } from '@ember/utils';
export default Component.extend({
tagName: '',
kredits: service(),
router: service(),
setupInProgress: false,
userHasEthereumWallet: computed(function() {
return isPresent(window.ethereum);
}),
showConnectButton: computed('userHasEthereumWallet',
'kredits.hasAccounts', function() {
return this.userHasEthereumWallet &&
!this.kredits.hasAccounts;
}),
actions: {
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);
}
}
}
});
@@ -0,0 +1,18 @@
<section id="user-account">
{{#if this.setupInProgress}}
Connecting account...
{{else}}
{{#if (and this.kredits.hasAccounts this.kredits.currentUser)}}
{{this.kredits.currentUser.name}}
{{#if this.kredits.currentUserIsCore}}
<span class="core-flag">(core)</span>
{{/if}}
{{else}}
<span class="anonymous">Anonymous</span>
<button onclick={{action "signup"}} class="small" type="button">Sign up</button>
{{#if this.showConnectButton}}
<button onclick={{action "connectAccount"}} class="small green" type="button">Connect account</button>
{{/if}}
{{/if}}
{{/if}}
</section>
+24 -2
View File
@@ -1,11 +1,33 @@
import Component from '@ember/component';
import { alias } from '@ember/object/computed';
import { computed } from '@ember/object';
const SIZES = {
'small': '128', // pixels
'medium': '256',
'large': '512'
}
export default Component.extend({
contributor: null,
tagName: 'img',
classNames: ['avatar'],
classNameBindings: ['size'],
attributeBindings: ['src', 'title'],
src: alias('contributor.avatarURL'),
title: alias('contributor.name')
size: 'small',
src: alias('avatarURL'),
title: alias('contributor.name'),
avatarURL: computed('contributor.github_uid', 'size', function() {
const github_uid = this.contributor.github_uid;
if (github_uid) {
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=${SIZES[this.size]}`;
} else {
// TODO use custom avatar
return '';
}
})
});
+10
View File
@@ -0,0 +1,10 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
export default class BudgetController extends Controller {
@service kredits;
@alias('kredits.reimbursementsUnconfirmed') reimbursementsUnconfirmed;
@alias('kredits.reimbursementsConfirmed') reimbursementsConfirmed;
}
+2
View File
@@ -6,6 +6,8 @@ export default Controller.extend({
kredits: service(),
queryParams: ['contributorId', 'kind', 'amount'],
contributors: alias('kredits.contributors'),
minedContributors: filterBy('contributors', 'id'),
@@ -0,0 +1,7 @@
import ContributionsNewController from 'kredits-web/controllers/contributions/new';
export default ContributionsNewController.extend({
attributes: null,
});
+1 -3
View File
@@ -8,9 +8,7 @@ export default Controller.extend({
actions: {
save (attributes) {
return this.kredits
.updateContributor(this.model.id, attributes)
.then(() => this.transitionToRoute('index'))
return this.kredits.updateContributor(this.model.id, attributes);
}
}
+1 -3
View File
@@ -8,9 +8,7 @@ export default Controller.extend({
actions: {
save (contributor) {
return this.kredits
.addContributor(contributor)
.then(() => this.transitionToRoute('index'))
return this.kredits.addContributor(contributor);
}
}
@@ -5,13 +5,18 @@ import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
showDetailsPane: false,
selectedContributorId: null,
selectedContributionId: null,
currentBlock: alias('kredits.currentBlock'),
contributions: alias('kredits.contributions'),
contributionsConfirmed: alias('kredits.contributionsConfirmed'),
contributionsUnconfirmed: alias('kredits.contributionsUnconfirmed'),
contributionsSorting: Object.freeze(['id:desc']),
contributionsSorting: Object.freeze(['date:desc', 'time:desc', 'id:desc']),
contributionsUnconfirmedSorted: sort('contributionsUnconfirmed', 'contributionsSorting'),
contributionsConfirmedSorted: sort('contributionsConfirmed', 'contributionsSorting'),
@@ -25,18 +30,27 @@ export default Controller.extend({
showUnconfirmedKredits: true,
hideUnconfirmedKredits: not('showUnconfirmedKredits'),
showQuickFilterUnconfirmed: false,
showQuickFilterConfirmed: false,
showIntroText: computed('kredits.{hasAccounts,currentUser}', function(){
return (!this.kredits.hasAccounts || !this.kredits.currentUser);
}),
actions: {
vetoContribution (contributionId) {
this.kredits.veto(contributionId).then(transaction => {
console.debug('[controllers:index] Veto submitted to Ethereum blockhain: '+transaction.hash);
console.debug('[controllers:index] Veto submitted to chain: '+transaction.hash);
});
},
confirmProposal (proposalId) {
this.kredits.vote(proposalId).then(transaction => {
console.debug('[controllers:index] Vote submitted to Ethereum blockhain: '+transaction.hash);
});
toggleQuickFilterUnconfirmed () {
this.toggleProperty('showQuickFilterUnconfirmed');
},
toggleQuickFilterConfirmed () {
this.toggleProperty('showQuickFilterConfirmed');
}
}
@@ -0,0 +1,12 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import config from 'kredits-web/config/environment';
export default Controller.extend({
ipfsGatewayUrl: computed(function() {
return config.ipfs.gatewayUrl;
})
});
@@ -0,0 +1,17 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import config from 'kredits-web/config/environment';
export default Controller.extend({
roleName: computed('model.isCore', 'totalKreditsEarned', function() {
if (this.model.isCore) return 'Core Contributor';
if (this.model.totalKreditsEarned <= 5000) return 'Newcomer';
return 'Contributor';
}),
ipfsGatewayUrl: computed(function() {
return config.ipfs.gatewayUrl;
})
});
-24
View File
@@ -1,24 +0,0 @@
import Controller from '@ember/controller';
import { alias, filterBy } from '@ember/object/computed';
import { inject as service } from '@ember/service';
export default Controller.extend({
kredits: service(),
contributors: alias('kredits.contributors'),
minedContributors: filterBy('contributors', 'id'),
actions: {
save(proposal) {
// contributorIpfsHash is needed for the proposal ipfs data. I'm not happy to do this here but I think to load all the contributors in addProposal again is a bit too much. I hope we can refactor it later.
let contributor = this.contributors.findBy('id', proposal.contributorId);
proposal.contributorIpfsHash = contributor.get('ipfsHash');
return this.kredits.addProposal(proposal)
.then((proposal) => {
this.transitionToRoute('index');
return proposal;
});
}
}
});
+49
View File
@@ -0,0 +1,49 @@
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');
});
}
}
});
+14
View File
@@ -0,0 +1,14 @@
import Controller from '@ember/controller';
import config from 'kredits-web/config/environment';
export default Controller.extend({
actions: {
connectGithub () {
window.location = config.githubConnectUrl;
}
}
});
+7
View File
@@ -0,0 +1,7 @@
import Helper from '@ember/component/helper';
export default Helper.extend({
compute([string]) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
});
-20
View File
@@ -1,20 +0,0 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
export default Helper.extend({
kredits: service(),
currentBlock: alias('kredits.currentBlock'),
compute([contribution]) {
if (contribution.vetoed) {
return 'vetoed';
} else if (contribution.confirmedAt > this.currentBlock) {
return 'unconfirmed';
} else {
return 'confirmed'
}
}
});
+18
View File
@@ -0,0 +1,18 @@
import { helper } from '@ember/component/helper';
export default helper(function fmtCryptoCurrency(params/*, hash*/) {
let fmtAmount;
const amount = params[0];
const code = params[1];
switch(code) {
case 'RBTC':
fmtAmount = amount / 1000000000000000000;
break;
case 'BTC':
fmtAmount = amount / 100000000;
break;
}
return fmtAmount;
});
+11
View File
@@ -0,0 +1,11 @@
import { helper } from '@ember/component/helper';
export default helper(function fmtFiatCurrency(params) {
const lang = navigator.language || navigator.userLanguage;
const formatter = new Intl.NumberFormat(lang, {
style: 'currency',
currency: params[1] || 'EUR',
currencyDisplay: 'code'
})
return formatter.format(params[0]);
});
+56
View File
@@ -0,0 +1,56 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import { once } from '@ember/runloop';
export default Helper.extend({
kredits: service(),
currentBlock: alias('kredits.currentBlock'),
compute([item]) {
this.setupRecompute(item);
let status = [];
if (item.vetoed) {
status.push('vetoed');
} else if (item.confirmedAt > this.currentBlock) {
status.push('unconfirmed');
} else {
status.push('confirmed');
}
if (item.hasPendingChanges) {
status.push('pending');
}
return status.join(' ');
},
destroy () {
if (this.teardown) this.teardown();
this._super(...arguments);
},
setupRecompute (item) {
if (this.teardown) this.teardown();
item.addObserver('vetoed' , this, this.triggerRecompute);
item.addObserver('confirmedAt' , this, this.triggerRecompute);
item.addObserver('currentBlock' , this, this.triggerRecompute);
item.addObserver('hasPendingChanges' , this, this.triggerRecompute);
this.teardown = () => {
item.removeObserver('vetoed', this, this.triggerRecompute);
item.removeObserver('confirmedAt', this, this.triggerRecompute);
item.removeObserver('currentBlock', this, this.triggerRecompute);
item.removeObserver('hasPendingChanges', this, this.triggerRecompute);
};
},
triggerRecompute () {
once(this, this.recompute);
}
});
+5
View File
@@ -0,0 +1,5 @@
import { helper } from '@ember/component/helper';
export default helper(function satsToBtc(amount/*, hash*/) {
return amount / 100000000;
});
+30 -8
View File
@@ -1,28 +1,50 @@
import EmberObject from '@ember/object';
import bignumber from 'kredits-web/utils/cps/bignumber';
import EmberObject, { computed } from '@ember/object';
import { isEmpty, isPresent } from '@ember/utils';
import moment from 'moment';
export default EmberObject.extend({
// Contract
id: null,
contributorId: null,
amount: null,
confirmedAt: bignumber('confirmedAtBlock', 'toNumber'),
confirmedAt: null,
vetoed: null,
ipfsHash: null,
creatorAccount: null,
// contributor model instance
contributor: null,
// TODO contributor who submitted the contribution
// submittedBy: null,
// IPFS
kind: null,
description: null,
details: null,
url: null,
ipfsData: '',
date: null,
time: null,
pendingTx: null,
init () {
this._super(...arguments);
this.set('details', {});
}
if (isEmpty(this.details)) this.set('details', {});
},
iso8601Date: computed('date', 'time', function() {
return this.time ? `${this.date}T${this.time}` : this.date;
}),
jsDate: computed('iso8601Date', function() {
return moment(this.iso8601Date).toDate();
}),
hasPendingChanges: computed('pendingTx', function() {
return isPresent(this.pendingTx);
}),
serialize () {
return JSON.stringify(this);
}
});
+8 -14
View File
@@ -1,15 +1,12 @@
import { computed } from '@ember/object';
import EmberObject from '@ember/object';
import bignumber from 'kredits-web/utils/cps/bignumber';
import kreditsValue from 'kredits-web/utils/cps/kredits';
export default EmberObject.extend({
// Contract
id: bignumber('idRaw', 'toString'),
id: null,
account: null,
balance: kreditsValue('balanceRaw'),
totalKreditsEarned: bignumber('totalKreditsEarnedRaw', 'toNumber'),
contributionsCount: bignumber('contributionsCountRaw', 'toNumber'),
balance: 0,
totalKreditsEarned: 0,
contributionsCount: 0,
isCore: false,
ipfsHash: null,
@@ -20,12 +17,9 @@ export default EmberObject.extend({
github_username: null,
github_uid: null,
wiki_username: null,
ipfsData: '',
zoom_display_name: null,
avatarURL: computed('github_uid', function() {
let github_uid = this.github_uid;
if (github_uid) {
return `https://avatars2.githubusercontent.com/u/${github_uid}?v=3&s=128`;
}
}),
serialize () {
return JSON.stringify(this);
}
});
-32
View File
@@ -1,32 +0,0 @@
import EmberObject from '@ember/object';
import { alias } from '@ember/object/computed';
import bignumber from 'kredits-web/utils/cps/bignumber';
export default EmberObject.extend({
// Contract
id: bignumber('idRaw', 'toString'),
creatorAccount: null,
contributorId: bignumber('contributorIdRaw', 'toString'),
amount: bignumber('amountRaw', 'toNumber'),
votesCount: bignumber('votesCountRaw', 'toNumber'),
votesNeeded: bignumber('votesNeededRaw', 'toNumber'),
executed: null,
ipfsHash: null,
// Shortcuts
isExecuted: alias('executed'),
// IPFS
kind: null,
description: null,
details: null,
url: null,
ipfsData: '',
init () {
this._super(...arguments);
this.set('details', {});
}
});
+47
View File
@@ -0,0 +1,47 @@
import EmberObject, { computed } from '@ember/object';
import moment from 'moment';
import { isPresent } from '@ember/utils';
export default EmberObject.extend({
// Contract
id: null,
recipientId: null,
token: null,
amount: null,
confirmedAt: null,
vetoed: null,
ipfsHash: null,
// contributor model instance
recipient: null,
// TODO contributor who submitted the reimbursement
// recordedBy: null,
// IPFS
expenses: null, // Array of expense objects
pendingTx: null,
iso8601Date: computed('date', 'time', function() {
return this.time ? `${this.date}T${this.time}` : this.date;
}),
jsDate: computed('iso8601Date', function() {
return moment(this.iso8601Date).toDate();
}),
hasPendingChanges: computed('pendingTx', function() {
return isPresent(this.pendingTx);
}),
pendingStatus: computed('pendingTx', function() {
return isPresent(this.pendingTx) ? 'isPending' : 'notPending';
}),
serialize () {
return JSON.stringify(this);
}
});
-3
View File
@@ -1,3 +0,0 @@
import Resolver from 'ember-resolver';
export default Resolver;
+28 -9
View File
@@ -1,22 +1,41 @@
import EmberRouter from '@ember/routing/router';
import config from './config/environment';
const Router = EmberRouter.extend({
location: config.locationType,
rootURL: config.rootURL
});
export default class Router extends EmberRouter {
location = config.locationType;
rootURL = config.rootURL;
}
Router.map(function() {
this.route('proposals', function() {
this.route('new');
this.route('dashboard', function() {
this.route('contributors', function() {
this.route('show', { path: ':id' });
});
this.route('contributions', function() {
this.route('show', { path: ':id' });
});
});
this.route('contributions', function() {
this.route('new');
this.route('new', { queryParams: ['contributorId', 'kind', 'amount'] });
this.route('resubmit', { path: ':id/resubmit' });
});
this.route('contributors', function() {
this.route('new');
this.route('edit', { path: ':id/edit' });
});
});
this.route('signup', function() {
this.route('github');
this.route('eth-account');
this.route('complete');
});
this.route('budget', function() {
this.route('expenses');
export default Router;
this.route('reimbursements', function() {});
});
this.route('reimbursements', function() {
this.route('new');
});
});
+24 -20
View File
@@ -1,31 +1,35 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { schedule } from '@ember/runloop';
export default Route.extend({
kredits: service(),
export default class ApplicationRoute extends Route {
@service kredits;
@service communityFunds;
beforeModel(transition) {
const kredits = this.kredits;
return kredits.setup().then(() => {
kredits.get('kredits').preflightChecks().catch((error) => {
beforeModel(/* transition */) {
return this.kredits.setup().then(() => {
this.kredits.kredits.preflightChecks().catch(error => {
console.error('Kredits preflight check failed!');
console.error(error);
});
if (kredits.get('accountNeedsUnlock')) {
if (confirm('It looks like you have an Ethereum wallet available. Please unlock your account.')) {
transition.retry();
}
}
}).catch((error) => {
}).catch(error => {
console.log('Error initializing Kredits', error);
});
},
}
model() {
return this.kredits.loadInitialData().then(() => {
this.kredits.addContractEventHandlers()
});
}
afterModel() {
return this.kredits.loadInitialData()
.then(() => {
this.kredits.addContractEventHandlers();
});
if (this.kredits.contributorsNeedSync) {
schedule('afterRender', this.kredits.syncContributors,
this.kredits.syncContributors.perform);
}
schedule('afterRender', this.communityFunds.fetchBalances,
this.communityFunds.fetchBalances.perform);
}
});
}
+34
View File
@@ -0,0 +1,34 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { isEmpty, isPresent } from '@ember/utils';
import { schedule } from '@ember/runloop';
export default class BudgetRoute extends Route {
@service browserCache;
@service kredits;
async model () {
if (isPresent(this.kredits.reimbursements) &&
isEmpty(this.kredits.reimbursementsPending)) {
console.debug('[route:budget] Reimbursements loaded before, no need to sync or load');
return;
} else {
const numCachedReimbursements = await this.browserCache.reimbursements.length();
if (numCachedReimbursements > 0) {
await this.kredits.loadObjectsFromCache('Reimbursement');
this.kredits.set('reimbursementsNeedSync', true);
} else {
await this.kredits.fetchObjects('Reimbursement', { page: { size: 10 } });
}
}
}
afterModel() {
if (this.kredits.reimbursementsNeedSync) {
schedule('afterRender', this.kredits.syncReimbursements,
this.kredits.syncReimbursements.perform);
}
schedule('afterRender', this.kredits.fetchMissingReimbursements,
this.kredits.fetchMissingReimbursements.perform);
}
}
+4
View File
@@ -0,0 +1,4 @@
import Route from '@ember/routing/route';
export default class BudgetExpensesRoute extends Route {
}
+9
View File
@@ -0,0 +1,9 @@
import Route from '@ember/routing/route';
export default class ContributionsNewRoute extends Route {
model (params) {
return { params };
}
}
+21
View File
@@ -0,0 +1,21 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default Route.extend({
kredits: service(),
model(params) {
return this.kredits.contributions.findBy('id', parseInt(params.id));
},
setupController (controller, model) {
this._super(controller, model);
controller.set('attributes', model.getProperties([
'contributorId', 'kind', 'amount', 'description', 'url', 'details'
]));
controller.set('attributes.date', model.jsDate);
}
});
+2 -1
View File
@@ -22,7 +22,8 @@ export default Route.extend({
github_username: model.github_username,
github_uid: model.github_uid,
gitea_username: model.gitea_username,
wiki_username: model.wiki_username
wiki_username: model.wiki_username,
zoom_display_name: model.zoom_display_name
});
}
+18
View File
@@ -0,0 +1,18 @@
import Route from '@ember/routing/route';
import { schedule } from '@ember/runloop';
import { inject as service } from '@ember/service';
export default class DashboardRoute extends Route {
@service kredits;
afterModel() {
if (this.kredits.contributionsNeedSync) {
schedule('afterRender', this.kredits.syncContributions,
this.kredits.syncContributions.perform);
}
schedule('afterRender', this.kredits.fetchMissingContributions,
this.kredits.fetchMissingContributions.perform);
}
}
@@ -0,0 +1,32 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import { alias } from '@ember/object/computed';
export default Route.extend({
kredits: service(),
contributions: alias('kredits.contributions'),
model (params) {
return this.contributions.findBy('id', parseInt(params.id));
},
setupController (controller, model) {
this._super(controller, model);
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: true,
selectedContributionId: model.id
});
},
deactivate () {
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: false,
selectedContributionId: null
});
}
});
+32
View File
@@ -0,0 +1,32 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import { alias } from '@ember/object/computed';
export default Route.extend({
kredits: service(),
contributors: alias('kredits.contributors'),
model (params) {
return this.contributors.findBy('id', params.id);
},
setupController (controller, model) {
this._super(controller, model);
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: true,
selectedContributorId: model.id
});
},
deactivate () {
this.controllerFor('dashboard')
.setProperties({
showDetailsPane: false,
selectedContributorId: null
});
}
});
+9
View File
@@ -0,0 +1,9 @@
import Route from '@ember/routing/route';
export default Route.extend({
redirect () {
this.transitionTo('dashboard');
}
});
+9
View File
@@ -0,0 +1,9 @@
import Route from '@ember/routing/route';
export default class ReimbursementsNewRoute extends Route {
model (params) {
return { params };
}
}
+4
View File
@@ -0,0 +1,4 @@
import Route from '@ember/routing/route';
export default Route.extend({
});
+4
View File
@@ -0,0 +1,4 @@
import Route from '@ember/routing/route';
export default Route.extend({
});
+17
View File
@@ -0,0 +1,17 @@
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');
}
}
});
+29
View File
@@ -0,0 +1,29 @@
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);
let accessToken;
try {
accessToken = window.location.hash.match(/access_token=(.+)/)[1];
} catch (error) { /* ignore */ }
if (isEmpty(accessToken) || accessToken === 'undefined') {
console.error('No GitHub access token found.');
this.transitionTo('signup');
return;
}
this.kredits.set('githubAccessToken', accessToken);
this.transitionTo('signup.eth-account');
}
});
+37
View File
@@ -0,0 +1,37 @@
import Service from '@ember/service';
import * as localforage from 'localforage';
import config from 'kredits-web/config/environment';
function createStore(name) {
let networkName;
if (config.web3RequiredNetworkName) {
networkName = config.web3RequiredNetworkName.toLocaleLowerCase().replace(' ', '-');
} else {
networkName = 'custom';
}
return localforage.createInstance({ name: `kredits:${networkName}:${name}` });
}
export default class BrowserCacheService extends Service {
constructor() {
super(...arguments);
this.stores = {
contributors: createStore('contributors'),
contributions: createStore('contributions'),
reimbursements: createStore('reimbursements')
}
}
get contributors() {
return this.stores.contributors;
}
get contributions() {
return this.stores.contributions;
}
get reimbursements() {
return this.stores.reimbursements;
}
}
+39
View File
@@ -0,0 +1,39 @@
import Service, { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { A } from '@ember/array';
import { task } from 'ember-concurrency-decorators';
import config from 'kredits-web/config/environment';
export default class CommunityFundsService extends Service {
@service exchangeRates;
@tracked balancesLoaded = false;
@tracked balances = A([]);
@task
*fetchBalances () {
yield fetch(config.btcBalanceAPI).then(res => res.json())
.then(res => {
return this.processBalances(res);
})
.catch(err => {
console.log(`[community-funds] Fetching balances failed:`);
console.error(err);
});
}
async processBalances (res) {
await this.exchangeRates.fetchRates();
// Format and round the approximate USD value
const lang = navigator.language || navigator.userLanguage;
const balanceUSD = res.confirmed_balance * this.exchangeRates.btcusd;
res.balanceUSD = Math.round(balanceUSD).toLocaleString(lang);
this.balances.pushObject({
...res,
...{ token: { name: 'BTC', symbol: 'BTC'} }
});
this.balancesLoaded = true;
}
}
+29
View File
@@ -0,0 +1,29 @@
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import config from 'kredits-web/config/environment';
// Need to go through proxy for CORS headers
const bitstampBaseUrl = `${config.corsProxy}https://www.bitstamp.net/api/v2`;
async function fetchFromBitstamp(currencyPair) {
try {
const res = await fetch(`${bitstampBaseUrl}/ticker/${currencyPair}/`).then(r => r.json());
return parseFloat(res.vwap); // Last 24 hours volume weighted average price
} catch(e) {
console.error('Could not fetch exchange rate from Bitstamp:', e);
return 0;
}
}
export default class ExchangeRatesService extends Service {
@tracked btceur = 0;
@tracked btcusd = 0;
async fetchRates (source='bitstamp') {
switch(source) {
case 'bitstamp':
this.btceur = await fetchFromBitstamp('btceur');
this.btcusd = await fetchFromBitstamp('btcusd');
}
}
}
+555 -221
View File
@@ -1,61 +1,171 @@
import ethers from 'npm:ethers';
import Kredits from 'npm:kredits-contracts';
import RSVP from 'rsvp';
import ethers from 'ethers';
import Kredits from '@kredits/contracts';
import Service from '@ember/service';
import EmberObject from '@ember/object';
import { computed } from '@ember/object';
import { alias, notEmpty } from '@ember/object/computed';
import { isEmpty } from '@ember/utils';
import { alias, filterBy, notEmpty, sort } from '@ember/object/computed';
import { isEmpty, isPresent } from '@ember/utils';
import { inject as service } from '@ember/service';
import { task, taskGroup } from 'ember-concurrency';
import groupBy from 'kredits-web/utils/group-by';
import processContributorData from 'kredits-web/utils/process-contributor-data';
import processContributionData from 'kredits-web/utils/process-contribution-data';
import processReimbursementData from 'kredits-web/utils/process-reimbursement-data';
import formatKredits from 'kredits-web/utils/format-kredits';
import config from 'kredits-web/config/environment';
import Contributor from 'kredits-web/models/contributor'
import Proposal from 'kredits-web/models/proposal'
import Contribution from 'kredits-web/models/contribution'
import Contributor from 'kredits-web/models/contributor';
import Contribution from 'kredits-web/models/contribution';
import Reimbursement from 'kredits-web/models/reimbursement';
// Lets us access the model classes dynamically
const models = { Contributor, Contribution, Reimbursement };
export default Service.extend({
browserCache: service(),
currentBlock: null,
currentUserAccounts: null, // default to not having an account. this is the wen web3 is loaded.
currentUser: null,
contributors: null,
contributions: null,
proposals: null,
reimbursements: null,
githubAccessToken: null,
currentUserIsContributor: notEmpty('currentUser'),
currentUserIsCore: alias('currentUser.isCore'),
hasAccounts: notEmpty('currentUserAccounts'),
accountNeedsUnlock: computed('currentUserAccounts', function() {
return this.currentUserAccounts && isEmpty(this.currentUserAccounts);
}),
contributorsMined: filterBy('contributors', 'id'),
contributorsSorting: Object.freeze(['name:asc']),
contributorsSorted: sort('contributorsMined', 'contributorsSorting'),
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
return this.contributions.filter(contribution => {
return contribution.confirmedAt > this.currentBlock;
// When data was loaded from cache, we need to fetch updates from the network
contributorsNeedSync: false,
contributionsNeedSync: false,
reimbursementsNeedSync: false,
init () {
this._super(...arguments);
this.set('contributors', []);
this.set('contributions', []);
this.set('reimbursements', []);
},
// This is called in the application route's beforeModel(). So it is
// initialized before everything else, and we can rely on the ethProvider and
// the potential currentUserAccounts to be available
getEthProvider () {
let ethProvider;
return new Promise(resolve => {
function instantiateWithoutAccount () {
console.debug('[kredits] Creating new instance from npm module class');
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
resolve({
ethProvider: ethProvider,
ethSigner: null
});
}
async function instantiateWithAccount (web3Provider, context) {
console.debug('[kredits] Using user-provided Web3 instance, e.g. from Metamask');
ethProvider = new ethers.providers.Web3Provider(web3Provider);
const network = await ethProvider.getNetwork();
if (isPresent(config.web3RequiredChainId) &&
network.chainId !== config.web3RequiredChainId) {
return instantiateWithoutAccount();
}
ethProvider.listAccounts().then(accounts => {
context.set('currentUserAccounts', accounts);
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
resolve({
ethProvider,
ethSigner
});
});
}
if (window.ethereum) {
if (window.ethereum.isConnected()) {
instantiateWithAccount(window.ethereum, this);
} else {
instantiateWithoutAccount();
}
}
// Legacy dapp browsers...
else if (window.web3) {
instantiateWithAccount(window.web3.currentProvider, this);
}
// Non-dapp browsers...
else {
instantiateWithoutAccount();
}
});
},
async setup () {
const kredits = await this.getEthProvider().then(providerAndSigner => {
return new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
ipfsConfig: config.ipfs
});
});
await kredits.init();
this.set('kredits', kredits);
this.set('currentBlock', await kredits.provider.getBlockNumber());
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
this.getCurrentUser.then(contributorData => {
this.set('currentUser', contributorData);
});
}
return kredits;
},
getCurrentUser: computed('kredits.provider', 'currentUserAccounts.[]', function() {
if (isEmpty(this.currentUserAccounts)) {
return Promise.resolve();
}
return this.kredits.Contributor
.functions.getContributorIdByAddress(this.currentUserAccounts.firstObject)
.then((id) => {
// check if the user is a contributor or not
if (id === 0) {
return Promise.resolve();
} else {
return this.kredits.Contributor.getById(id);
}
});
}),
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() {
return this.contributions
.filterBy('vetoed', false)
.filter(contribution => {
return contribution.confirmedAt <= this.currentBlock;
});
totalSupply: computed(function() {
return this.kredits.Token.functions.totalSupply().then(total => {
return formatKredits(total);
})
}),
totalKreditsEarned: computed(function() {
return this.kredits.Contribution.functions.totalKreditsEarned(true);
}),
kreditsByContributor: computed('contributionsUnconfirmed.@each.vetoed', 'contributors.[]', function() {
const contributionsUnconfirmed = this.contributionsUnconfirmed.filterBy('vetoed', false);
const contributionsGrouped = groupBy(contributionsUnconfirmed, 'contributorId');
const contributorsWithUnconfirmed = contributionsGrouped.map(c => c.value.toString());
const contributorsWithUnconfirmed = contributionsGrouped.map(c => c.value);
const contributorsWithOnlyConfirmed = this.contributors.reject(c => contributorsWithUnconfirmed.includes(c.id))
const kreditsByContributor = contributionsGrouped.map(c => {
const amountUnconfirmed = c.items.mapBy('amount').reduce((a, b) => a + b);
const contributor = this.contributors.findBy('id', c.value.toString());
const contributor = this.contributors.findBy('id', c.value);
return EmberObject.create({
contributor: contributor,
@@ -77,106 +187,50 @@ export default Service.extend({
return kreditsByContributor;
}),
init () {
this._super(...arguments);
this.set('contributors', []);
this.set('proposals', []);
this.set('contributions', []);
},
// This is called in the application route's beforeModel(). So it is
// initialized before everything else, and we can rely on the ethProvider and
// the potential currentUserAccounts to be available
getEthProvider () {
let ethProvider;
return new RSVP.Promise(async (resolve) => {
function instantiateWithoutAccount () {
console.debug('[kredits] Creating new instance from npm module class');
console.debug(`[kredits] providerURL: ${config.web3ProviderUrl}`);
ethProvider = new ethers.providers.JsonRpcProvider(config.web3ProviderUrl);
resolve({
ethProvider: ethProvider,
ethSigner: null
});
}
async function instantiateWithAccount (web3Provider, context) {
console.debug('[kredits] Using user-provided instance, e.g. from Mist browser or Metamask');
ethProvider = new ethers.providers.Web3Provider(web3Provider);
// const network = await ethProvider.getNetwork();
ethProvider.listAccounts().then(accounts => {
context.set('currentUserAccounts', accounts);
const ethSigner = accounts.length === 0 ? null : ethProvider.getSigner();
resolve({
ethProvider,
ethSigner
});
});
}
if (window.ethereum) {
try {
// Request account access if needed
await window.ethereum.enable();
// Acccounts now exposed
instantiateWithAccount(window.ethereum, this);
} catch (error) {
instantiateWithoutAccount();
}
}
// Legacy dapp browsers...
else if (window.web3) {
instantiateWithAccount(window.web3.currentProvider, this);
}
// Non-dapp browsers...
else {
instantiateWithoutAccount();
}
});
},
setup () {
return this.getEthProvider().then((providerAndSigner) => {
let kredits = new Kredits(providerAndSigner.ethProvider, providerAndSigner.ethSigner, {
addresses: { Kernel: config.kreditsKernelAddress },
apm: config.kreditsApmDomain,
ipfsConfig: config.ipfs
});
return kredits
.init()
.then(async (kredits) => {
this.set('kredits', kredits);
this.set('currentBlock', await kredits.provider.getBlockNumber());
if (this.currentUserAccounts && this.currentUserAccounts.length > 0) {
this.getCurrentUser.then((contributorData) => {
this.set('currentUser', contributorData);
});
}
return kredits;
});
});
},
totalSupply: computed(function() {
return this.kredits.Token.functions.totalSupply().then(total => {
return formatKredits(total);
})
contributionsUnconfirmed: computed('contributions.[]', 'currentBlock', function() {
return this.contributions
.filter(c => c.confirmedAt > this.currentBlock);
}),
totalKreditsEarned: computed(function() {
return this.kredits.Contribution.functions.totalKreditsEarned(true)
.then(total => total.toNumber());
contributionsConfirmed: computed('contributions.[]', 'currentBlock', function() {
return this.contributions
.filterBy('vetoed', false)
.filter(c => c.confirmedAt <= this.currentBlock);
}),
reimbursementsUnconfirmed: computed('reimbursements.[]', 'currentBlock', function() {
return this.reimbursements
.filter(r => r.confirmedAt > this.currentBlock);
}),
loadInitialData () {
return this.getContributors()
.then(contributors => this.contributors.pushObjects(contributors))
.then(() => this.getContributions())
.then(contributions => this.contributions.pushObjects(contributions))
reimbursementsConfirmed: computed('reimbursements.[]', 'currentBlock', function() {
return this.reimbursements
.filterBy('vetoed', false)
.filter(r => r.confirmedAt <= this.currentBlock);
}),
reimbursementsPending: computed('reimbursements.[]', 'pendingTx', function() {
return this.reimbursements.filter(r => !r.id);
}),
async loadInitialData () {
const numCachedContributors = await this.browserCache.contributors.length();
if (numCachedContributors > 0) {
await this.loadObjectsFromCache('Contributor');
this.set('contributorsNeedSync', true);
} else {
await this.fetchContributors();
}
const numCachedContributions = await this.browserCache.contributions.length();
if (numCachedContributions > 0) {
await this.loadObjectsFromCache('Contribution');
this.set('contributionsNeedSync', true);
} else {
await this.fetchContributions({ page: { size: 30 } });
}
return Promise.resolve();
},
addContributor (attributes) {
@@ -207,102 +261,366 @@ export default Service.extend({
});
},
getContributors () {
fetchContributors () {
console.debug(`[kredits] Fetching all contributors from the network`);
return this.kredits.Contributor.all()
.then((contributors) => {
return contributors.map((contributor) => {
return Contributor.create(contributor);
.then(contributors => {
return contributors.forEach(data => {
this.loadContributorFromData(data);
return;
});
})
.then(() => {
return this.cacheLoadedContributors();
});
},
addContribution (attributes) {
console.debug('[kredits] add contribution', attributes);
loadContributorFromData (data) {
const contributor = Contributor.create(processContributorData(data));
const loadedContributor = this.contributors.findBy('id', contributor.id);
if (loadedContributor) { this.contributors.removeObject(loadedContributor); }
this.contributors.pushObject(contributor);
return contributor;
},
return this.kredits.Contribution.addContribution(attributes, { gasLimit: 300000 })
async cacheLoadedContributors () {
for (const c of this.contributors) {
await this.browserCache.contributors.setItem(c.id.toString(), c.serialize());
}
console.debug(`[kredits] Cached ${this.contributors.length} contributors in browser storage`);
return Promise.resolve();
},
async loadContributorsFromCache () {
return this.browserCache.contributors.iterate((value/*, key , iterationNumber */) => {
this.contributors.pushObject(Contributor.create(JSON.parse(value)));
}).then((/* result */) => {
console.debug(`[kredits] Loaded ${this.contributors.length} contributors from cache`);
});
},
syncContributors: task(function * () {
yield this.fetchContributors();
this.set('contributorsNeedSync', false);
}),
addContribution (attributes) {
console.debug('[kredits] Adding contribution', attributes);
return this.kredits.Contribution.add(attributes, { gasLimit: 300000 })
.then(data => {
console.debug('[kredits] add contribution response', data);
console.debug('[kredits] Contribution.add response', data);
attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
const contribution = Contribution.create(attributes);
// TODO receive from wrapper
contribution.set('confirmedAtBlock', data.blockNumber + 40320);
contribution.set('pendingTx', data);
contribution.set('confirmedAtBlock', this.currentBlock + 40320);
this.contributions.pushObject(contribution);
return contribution;
});
},
addProposal (attributes) {
console.debug('[kredits] add proposal', attributes);
return this.kredits.Proposal.addProposal(attributes)
.then((data) => {
console.debug('[kredits] add proposal response', data);
attributes.contributor = this.contributors.findBy('id', attributes.contributorId);
return Proposal.create(attributes);
});
},
getProposals () {
return this.kredits.Proposal.all()
.then((proposals) => {
return proposals.map((proposal) => {
proposal.contributor = this.contributors.findBy('id', proposal.contributorId.toString());
return Proposal.create(proposal);
});
});
},
getContributions () {
return this.kredits.Contribution.all({page: {size: 200}})
fetchContributions (options = { page: { size: 200 } }) {
console.debug(`[kredits] Fetching contributions from the network`);
return this.kredits.Contribution.all(options)
.then(contributions => {
return contributions.map(contribution => {
contribution.contributor = this.contributors.findBy('id', contribution.contributorId.toString());
return Contribution.create(contribution);
return contributions.map(data => {
const contribution = this.loadContributionFromData(data);
return contribution;
});
})
.then(contributions => {
const cacheWrites = contributions.map(c => {
return this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
});
return Promise.all(cacheWrites).then(() => {
console.debug(`[kredits] Cached ${contributions.length} contributions in browser storage`);
});
});
},
vote (proposalId) {
console.debug('[kredits] vote for', proposalId);
return this.kredits.Proposal.functions.vote(proposalId)
.then(data => {
console.debug('[kredits] vote response', data);
return data;
});
loadContributionFromData(data) {
const contribution = Contribution.create(processContributionData(data));
contribution.set('contributor', this.contributors.findBy('id', data.contributorId));
const loadedContribution = this.contributions.findBy('id', contribution.id);
if (loadedContribution) { this.contributions.removeObject(loadedContribution); }
this.contributions.pushObject(contribution);
return contribution;
},
async cacheLoadedContributions () {
for (const c of this.contributions) {
await this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
}
console.debug(`[kredits] Cached ${this.contributions.length} contributions in browser storage`);
return Promise.resolve();
},
async loadContributionsFromCache () {
return this.browserCache.contributions.iterate((value/*, key , iterationNumber */) => {
this.contributions.pushObject(Contribution.create(JSON.parse(value)));
}).then((/* result */) => {
console.debug(`[kredits] Loaded ${this.contributions.length} contributions from cache`);
});
},
contributionTasks: taskGroup().enqueue(),
syncContributions: task(function * () {
yield this.fetchNewContributions.perform();
yield this.syncUnconfirmedContributions.perform();
this.set('contributionsNeedSync', false);
}).group('contributionTasks'),
fetchNewContributions: task(function * () {
const count = yield this.kredits.Contribution.functions.contributionsCount();
const lastKnownContributionId = Math.max.apply(null, this.contributions.mapBy('id'));
const toFetch = count - lastKnownContributionId;
if (toFetch > 0) {
console.debug(`[kredits] Fetching ${toFetch} new contributions`);
for (let id = lastKnownContributionId; id <= count; id++) {
const data = yield this.kredits.Contribution.getById(id);
const c = this.loadContributionFromData(data);
yield this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
}
} else {
console.debug(`[kredits] No new contributions to fetch`);
}
}),
fetchMissingContributions: task(function * () {
const count = yield this.kredits.Contribution.functions.contributionsCount();
const allIds = [...Array(count+1).keys()];
allIds.shift(); // remove first item, which is 0
const loadedContributions = new Set(this.contributions.mapBy('id'));
const toFetch = allIds.filter(id => !loadedContributions.has(id));
if (toFetch.length === 0) {
console.debug(`[kredits] No contributions left to fetch`);
return;
}
console.debug(`[kredits] Fetching ${toFetch.length} past contributions`);
let countFetched = 0;
for (let id = count; id > 0; id--) {
if (loadedContributions.has(id)) {
continue;
} else {
const data = yield this.kredits.Contribution.getById(id);
const c = this.loadContributionFromData(data);
yield this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
countFetched++;
if (countFetched % 20 === 0) {
console.debug(`[kredits] Fetched ${countFetched} more contributions`);
}
}
}
console.debug(`[kredits] Cached ${countFetched} past contributions`);
}).group('contributionTasks'),
syncUnconfirmedContributions: task(function * () {
if (this.contributionsUnconfirmed.length > 0) {
console.debug(`[kredits] Syncing unconfirmed contributions`);
for (const c of this.contributionsUnconfirmed) {
const data = yield this.kredits.Contribution.getById(c.id);
const contribution = this.loadContributionFromData(data);
yield this.browserCache.contributions.setItem(c.id.toString(), contribution.serialize());
}
} else {
console.debug(`[kredits] No unconfirmed contributions to sync`);
}
}),
veto (contributionId) {
console.debug('[kredits] veto against', contributionId);
const contribution = this.contributions.findBy('id', contributionId);
return this.kredits.Contribution.functions.veto(contributionId, { gasLimit: 300000 })
.then(data => {
console.debug('[kredits] veto response', data);
contribution.set('pendingTx', data);
return data;
});
},
getCurrentUser: computed('kredits.provider', function() {
if (isEmpty(this.currentUserAccounts)) {
return RSVP.resolve();
}
return this.kredits.Contributor
.functions.getContributorIdByAddress(this.get('currentUserAccounts.firstObject'))
.then((id) => {
// check if the user is a contributor or not
if (id === 0) {
return RSVP.resolve();
} else {
return this.kredits.Contributor.getById(id);
}
});
}),
//
// Generic data handling (for objects that can be vetoed)
//
findProposalById(proposalId) {
return this.proposals.findBy('id', proposalId.toString());
fetchObjects(objectClass, options = { page: { size: 200 } }) {
const objectClassLowerCase = objectClass.toLowerCase();
console.debug(`[kredits] Fetching ${objectClassLowerCase}s from the network`);
return this.kredits[objectClass].all(options)
.then(objects => {
return objects.map(data => {
const classInstance = this[`load${objectClass}FromData`](data);
return classInstance;
});
})
.then(objects => {
const cacheWrites = objects.map(o => {
return this.browserCache[objectClassLowerCase+'s']
.setItem(o.id.toString(), o.serialize());
});
return Promise.all(cacheWrites).then(() => {
console.debug(`[kredits] Cached ${objects.length} ${objectClassLowerCase+'s'} in browser storage`);
});
});
},
removeObjectFromCollectionIfLoaded (collection, objectId) {
const loadedObj = this[collection].findBy('id', objectId);
if (loadedObj) { this[collection].removeObject(loadedObj); }
},
async cacheLoadedObjects (collection) {
for (const o of this[collection]) {
await this.browserCache[collection].setItem(o.id, o.serialize());
}
console.debug(`[kredits] Cached ${this[collection].length} ${collection} in browser storage`);
},
async loadObjectsFromCache (objectClass) {
const collection = objectClass.toLowerCase()+'s';
return this.browserCache[collection].iterate((value/*, key , iterationNumber */) => {
const obj = models[objectClass].create(JSON.parse(value));
this.removeObjectFromCollectionIfLoaded(collection, obj.id)
this[collection].pushObject(obj);
}).then((/* result */) => {
console.debug(`[kredits] Loaded ${this[collection].length} ${collection} from cache`);
});
},
syncTaskGroup: taskGroup().enqueue(),
fetchNewObjects: task(function * (objectClass) {
const collection = objectClass.toLowerCase()+'s';
const count = yield this.kredits[objectClass].functions[`${collection}Count`]();
const lastKnownObjectId = Math.max.apply(null, this[collection].mapBy('id'));
const toFetch = count - lastKnownObjectId;
if (toFetch > 0) {
console.debug(`[kredits] Fetching ${toFetch} new ${collection}`);
for (let id = lastKnownObjectId; id <= count; id++) {
const data = yield this.kredits[objectClass].getById(id);
const o = this[`load${objectClass}FromData`](data);
yield this.browserCache[collection].setItem(o.id.toString(), o.serialize());
}
} else {
console.debug(`[kredits] No new ${collection} to fetch`);
}
}),
fetchMissingObjects: task(function * (objectClass) {
const collection = objectClass.toLowerCase()+'s';
const count = yield this.kredits[objectClass].functions[`${collection}Count`]();
const allIds = [...Array(count+1).keys()];
allIds.shift(); // remove first item, which is 0
const loadedObjects = new Set(this[collection].mapBy('id'));
const toFetch = allIds.filter(id => !loadedObjects.has(id));
if (toFetch.length === 0) {
console.debug(`[kredits] No ${collection} left to fetch`);
return;
}
console.debug(`[kredits] Fetching ${toFetch.length} past ${collection}`);
let countFetched = 0;
for (let id = count; id > 0; id--) {
if (loadedObjects.has(id)) {
continue;
} else {
const data = yield this.kredits[objectClass].getById(id);
const o = this[`load${objectClass}FromData`](data);
yield this.browserCache[collection].setItem(o.id.toString(), o.serialize());
countFetched++;
if (countFetched % 20 === 0) {
console.debug(`[kredits] Fetched ${countFetched} more ${collection}`);
}
}
}
console.debug(`[kredits] Cached ${countFetched} past ${collection}`);
}),
syncUnconfirmedObjects: task(function * (objectClass) {
const collection = objectClass.toLowerCase()+'s';
if (this.get(`${collection}Unconfirmed`).length > 0) {
console.debug(`[kredits] Syncing unconfirmed ${collection}`);
for (const o of this[`${collection}Unconfirmed`]) {
if (isEmpty(o.id)) return;
const data = yield this.kredits[objectClass].getById(o.id);
const object = this[`load${objectClass}FromData`](data);
yield this.browserCache[collection]
.setItem(o.id.toString(), object.serialize());
}
} else {
console.debug(`[kredits] No unconfirmed ${collection} to sync`);
}
}),
vetoAgainstObject (objectClass, objectId) {
console.debug(`[kredits] veto against ${objectClass.toLowerCase()}`, objectId);
const collection = objectClass.toLowerCase()+'s';
const object = this[collection].findBy('id', objectId);
return this.kredits[objectClass].functions.veto(objectId, { gasLimit: 300000 })
.then(data => {
console.debug('[kredits] veto response', data);
object.set('pendingTx', data);
return data;
});
},
//
// Reimbursements
//
loadReimbursementFromData(data) {
const obj = Reimbursement.create(processReimbursementData(data));
obj.set('contributor', this.contributors.findBy('id', data.recipientId));
this.removeObjectFromCollectionIfLoaded('reimbursements', obj.id);
this.reimbursements.pushObject(obj);
return obj;
},
addReimbursement (attributes) {
console.debug('[kredits] add reimbursement', attributes);
return this.kredits.Reimbursement.add(attributes, { gasLimit: 300000 })
.then(data => {
console.debug('[kredits] add reimbursement response', data);
const reimbursement = Reimbursement.create(attributes);
reimbursement.setProperties({
contributor: this.contributors.findBy('id', attributes.recipientId),
pendingTx: data,
confirmedAt: this.currentBlock + 40320
});
this.reimbursements.pushObject(reimbursement);
// Listen to tx mining/execution status
data.wait()
.then(d => console.debug('[kredits] tx successful', d))
.catch(e => {
window.alert('The transaction failed to execute. Please check the browser console.');
console.log('[kredits] tx error', e);
});
return reimbursement;
});
},
syncReimbursements: task(function * () {
yield this.fetchNewObjects.perform('Reimbursement');
yield this.syncUnconfirmedObjects.perform('Reimbursement');
this.set('reimbursementsNeedSync', false);
}).group('syncTaskGroup'),
fetchMissingReimbursements: task(function * () {
yield this.fetchMissingObjects.perform('Reimbursement');
}).group('syncTaskGroup'),
//
// Contract events
//
addContractEventHandlers () {
this.kredits.Contributor
.on('ContributorProfileUpdated', this.handleContributorChange.bind(this))
@@ -310,12 +628,12 @@ export default Service.extend({
.on('ContributorAdded', this.handleContributorChange.bind(this))
this.kredits.Contribution
.on('ContributionAdded', this.handleContributionAdded.bind(this))
.on('ContributionVetoed', this.handleContributionVetoed.bind(this))
this.kredits.Proposal
.on('ProposalCreated', this.handleProposalCreated.bind(this))
.on('ProposalVoted', this.handleProposalVoted.bind(this))
.on('ProposalExecuted', this.handleProposalExecuted.bind(this));
this.kredits.Reimbursement
.on('ReimbursementAdded', this.handleReimbursementAdded.bind(this))
.on('ReimbursementVetoed', this.handleReimbursementVetoed.bind(this))
this.kredits.Token
.on('Transfer', this.handleTransfer.bind(this));
@@ -327,65 +645,81 @@ export default Service.extend({
const contributorData = await this.kredits.Contributor.getById(contributorId);
const newContributor = Contributor.create(contributorData);
const oldContributor = this.contributors.findBy('id', contributorId.toString());
// TODO check for actual differences in the contributor data first
const oldContributor = this.contributors.findBy('id', contributorId);
if (oldContributor) {
console.debug('[kredits] old contributor', oldContributor);
// console.debug('[kredits] cached contributor', oldContributor);
this.contributors.removeObject(oldContributor);
}
console.debug('[kredits] new contributor', newContributor);
// console.debug('[kredits] incoming contributor data', newContributor);
this.contributors.pushObject(newContributor);
},
handleContributionVetoed (contributionId) {
async handleContributionAdded (id, contributorId, amount) {
console.debug('[kredits] ContributionAdded event received', { id, contributorId, amount });
const pendingContribution = this.contributions.find(c => {
return (c.id === null) &&
(c.contributorId === contributorId) &&
(c.amount.toString() === amount.toString());
});
if (pendingContribution) {
this.contributions.removeObject(pendingContribution);
}
const data = await this.kredits.Contribution.getById(id);
const c = this.loadContributionFromData(data);
await this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
},
async handleContributionVetoed (contributionId) {
console.debug('[kredits] ContributionVetoed event received for ', contributionId);
const contribution = this.contributions.findBy('id', contributionId);
console.debug('[kredits] contribution', contribution);
const c = this.contributions.findBy('id', contributionId);
if (contribution) {
contribution.set('vetoed', true);
if (c) {
console.debug('[kredits] Updating contribution', c);
c.set('vetoed', true);
c.set('pendingTx', null);
await this.browserCache.contributions.setItem(c.id.toString(), c.serialize());
}
},
handleProposalCreated (proposalId) {
let proposal = this.findProposalById(proposalId);
//
// TODO test when reimbursement txs are successful
//
async handleReimbursementAdded (id, addedByAccount, amount) {
console.debug('[kredits] ReimbursementAdded event received', { id, addedByAccount, amount });
if (proposal) {
console.debug('[events] proposal exists, not adding from event');
return;
const pendingReimbursement = this.reimbursementsPending.find(r => {
return r.amount.toString() === amount.toString();
});
if (pendingReimbursement) {
console.debug('[kredits] Found a pending reimbursement matching the event. Replacing it with the final record...');
this.reimbursements.removeObject(pendingReimbursement);
}
this.kredits.Proposal.getById(proposalId)
.then((proposal) => {
proposal.contributor = this.contributors.findBy('id', proposal.contributorId.toString());
this.proposals.pushObject(Proposal.create(proposal));
});
const data = await this.kredits.Reimbursement.getById(id);
this.loadReimbursementFromData(data);
},
// TODO: We may want to reload that proposal to show the voter as voted
handleProposalVoted (proposalId, voterId, totalVotes) {
let proposal = this.findProposalById(proposalId);
//
// TODO test when reimbursement txs are successful and veto is implemented
//
handleReimbursementVetoed (id) {
console.debug('[kredits] ReimbursementVetoed received for ', id);
const reimbursement = this.reimbursements.findBy('id', id);
console.debug('[kredits] reimbursement', this.reimbursement);
if (proposal) {
proposal.set('votesCount', totalVotes);
if (reimbursement) {
reimbursement.set('vetoed', true);
reimbursement.set('pendingTx', null);
}
},
handleProposalExecuted (proposalId, contributorId, amount) {
let proposal = this.findProposalById(proposalId);
if (proposal.get('isExecuted')) {
console.debug('[events] proposal already executed, not adding from event');
return;
}
proposal.set('executed', true);
this.contributors
.findBy('id', contributorId.toString())
.incrementProperty('balance', amount);
},
handleTransfer (from, to, value) {
value = value.toNumber();
+35 -3
View File
@@ -10,17 +10,30 @@ button, input[type=submit], .button {
text-transform: uppercase;
cursor: pointer;
letter-spacing: 0.1em;
-webkit-appearance: none;
&+button, &+input[type=submit], &+.button {
margin-left: 0.5rem;
}
&:hover {
background-color: rgba(22, 21, 40, 0.8);
}
&:focus, &:active, &.active {
border-color: $primary-color;
}
&[disabled] {
color: rgba(255,255,255,0.5);
}
&.small {
font-size: 0.8rem;
font-size: 0.86rem;
padding: 0.2rem 0.8rem;
}
&.danger {
&.danger:not(:disabled) {
color: $red;
background-color: rgba(40, 21, 21, 0.6);
border-color: rgba(40, 21, 21, 1);
@@ -28,9 +41,12 @@ button, input[type=submit], .button {
&:hover {
background-color: rgba(40, 21, 21, 0.8);
}
&:focus, &:active, &.active {
border-color: $red;
}
}
&.green {
&.green:not(:disabled) {
color: $green;
background-color: rgba(21, 40, 21, 0.6);
border-color: rgba(21, 40, 21, 1);
@@ -38,5 +54,21 @@ button, input[type=submit], .button {
&:hover {
background-color: rgba(21, 40, 21, 0.8);
}
&:focus, &:active, &.active {
border-color: $green;
}
}
&.icon {
svg {
width: 2rem;
height: 2rem;
vertical-align: middle;
margin-right: 1rem;
.fg {
fill: $primary-color;
}
}
}
}
+3 -4
View File
@@ -9,7 +9,6 @@ $primary-color: $blue;
$body-text-color: #fff;
#topbar {
background-color: rgba(255, 0, 255, .2);
}
$item-background-color: rgba(255,255,255,0.1);
$item-highlighted-background-color: rgba(255,255,255,0.2);
$item-border-color: rgba(255,255,255,0.2);
+55 -7
View File
@@ -1,13 +1,31 @@
section#add-contributor,
section#add-contribution,
section#add-proposal {
section#add-contribution,
section#add-item, // TODO use for all forms for adding data
section#signup {
form {
h3 {
font-size: 1.5rem;
font-weight: normal;
margin-top: 2em;
margin-bottom: 1em;
}
p {
margin-bottom: 1.5rem;
font-size: 1.2rem;
&.mg-bottom-md {
margin-bottom: 2rem;
}
&.label {
margin-bottom: 1.5rem;
font-size: 1rem;
margin-bottom: .5rem;
}
&.actions {
margin-bottom: 1.5rem;
padding-top: 1.5rem;
text-align: center;
a {
@@ -21,9 +39,34 @@ section#add-proposal {
display: block;
margin-bottom: 0.5rem;
opacity: 0.7;
> p {
margin-bottom: 1.5rem;
}
}
input[type=text], select {
fieldset {
border: none;
&.horizontal {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 2rem;
&.thirds {
grid-template-columns: 1fr 1fr 1fr;
}
&.total-amounts {
grid-template-columns: 2fr 1fr 1fr;
}
label {
}
}
}
input[type=text], input[type=url], select {
width: 100%;
padding: 1rem;
border: none;
@@ -31,17 +74,24 @@ section#add-proposal {
background-color: rgba(22, 21, 40, 0.3);
color: #fff;
font-size: 1.2rem;
font-weight: normal;
transition: border-color 0.1s linear;
&:focus, &.valid {
background-color: rgba(22, 21, 40, 0.6);
}
&:focus {
&:focus :not(:invalid) {
border-color: $blue;
}
&::placeholder {
color: rgba(238, 238, 238, 0.5);
}
&.invalid {
border-color: $red;
}
&:disabled {
color: rgba(255, 255, 255, 0.6);
}
}
select {
@@ -101,7 +151,5 @@ section#add-proposal {
color: #fff;
}
}
}
}
+35
View File
@@ -0,0 +1,35 @@
ul.item-list {
list-style: none;
> li {
padding: 0.8rem 1.2rem;
font-size: 1.2rem;
background-color: $item-background-color;
border-bottom: 1px solid $item-border-color;
&:first-of-type {
border-top: 1px solid $item-border-color;
}
&.selected {
background-color: $item-highlighted-background-color;
}
}
&.loading {
@include loading-border-top;
> li {
&:first-of-type {
border-top: none;
}
}
}
&.spaced {
> li {
border-top: 1px solid $item-border-color;
margin-bottom: 2rem;
}
}
}
+100 -2
View File
@@ -5,7 +5,7 @@ main {
padding: 1rem;
}
&#index {
&#dashboard {
width: 100%;
display: grid;
grid-row-gap: 2rem;
@@ -14,6 +14,15 @@ main {
"contributions";
}
&#budget {
width: 100%;
display: grid;
grid-row-gap: 2rem;
grid-template-areas:
"aside"
"content";
}
&.center-column {
display: flex;
flex-direction: column;
@@ -28,16 +37,105 @@ main {
}
}
}
div#content {
section {
&:first-of-type {
@include media-max(small) {
margin-top: 0;
}
}
}
}
section {
.content {
a {
font-size: inherit;
}
&.text-lg {
p {
font-size: 1.2rem;
margin-bottom: 1em;
line-height: 1.5em;
}
}
&.text-center {
text-align: center;
}
p {
&.mg-bottom-md {
margin-bottom: 2rem;
}
&.actions {
text-align: center;
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;
}
h2 {
font-size: inherit;
margin-bottom: 1em;
}
p {
margin-bottom: 1em;
line-height: 1.5em;
font-size: 1.2rem;
&:last-child {
margin-bottom: 0;
}
}
a {
text-decoration: none;
&:hover, &:active {
text-decoration: underline;
}
}
}
@media (min-width: 550px) {
main {
&#index {
&#dashboard {
grid-column-gap: 4rem;
grid-row-gap: 2rem;
grid-template-columns: 2fr 4fr;
grid-template-areas:
"stats contributions";
&.with-details {
grid-column-gap: 3rem;
grid-template-columns: 2fr 4fr 2fr;
grid-template-areas:
"stats contributions details";
}
}
&#budget {
grid-column-gap: 3rem;
grid-template-columns: 2fr 4fr 2fr;
grid-template-areas:
"aside content empty";
}
}
}
+17
View File
@@ -0,0 +1,17 @@
@mixin loading-border-top {
&::before {
display: block;
width: 100%;
height: 1px;
content: '';
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.2) 40%, #68d7fb 60%, rgba(255, 255, 255, 0.2));
background-size: 200% 200%;
animation: kitt 2.5s linear infinite;
}
}
@keyframes kitt {
0%{ background-position: 0% 0%}
50%{ background-position: 100% 0%}
100%{ background-position: 0% 0%}
}
+24 -24
View File
@@ -2,7 +2,6 @@
margin: 0;
padding: 0;
box-sizing: border-box;
font-size: 14px;
}
*:hover, *:active, *:focus {
@@ -17,13 +16,17 @@
$font-family-sans: 'Open Sans', sans-serif;
body {
html, body {
font-family: $font-family-sans;
font-weight: 300;
font-size: 12px;
color: $body-text-color;
}
body {
background-image: linear-gradient(to bottom, rgba(22, 21, 40, .4), rgba(0, 0, 0, .75)), url('/img/bg.jpg');
background-repeat: none;
background-attachment: fixed;
color: $body-text-color;
}
@media screen and (min-width: 900px) {
@@ -38,9 +41,13 @@ h1, h2, h3, h4, h5, input, button {
font-weight: 300;
}
a {
color: $primary-color;
}
section {
h2 {
font-size: 1.4rem;
font-size: 1.5rem;
color: $primary-color;
}
@@ -69,22 +76,6 @@ section {
}
}
}
&#proposals-open, &#proposals-closed {
.actions {
padding-top: 3rem;
font-size: 1rem;
color: $primary-color;
text-align: center;
@include media-max(small) {
padding-top: 2rem;
}
a {
color: $primary-color;
}
}
}
}
@media (min-width: 550px) {
@@ -97,9 +88,18 @@ section {
@import "buttons";
@import "forms";
@import "components/topbar";
@import "components/loading-spinner";
@import "components/contributor-list";
@import "components/proposal-list";
@import "sugar";
@import "item-list";
@import "components/budget-balances";
@import "components/contribution-details";
@import "components/contribution-list";
@import "components/contributor-list";
@import "components/contributor-profile";
@import "components/expense-list";
@import "components/external-account-link";
@import "components/loading-spinner";
@import "components/reimbursement-list";
@import "components/topbar";
@import "components/topbar-account-panel";
@import "components/user-avatar";
@@ -0,0 +1,42 @@
section#funds {
@include media-max(small) {
margin-bottom: 2rem;
}
table.token-balances {
opacity: 1;
transition: opacity 0.3s linear;
&.loading {
opacity: 0;
}
thead {
display: none;
}
th, td {
font-size: 1.2rem;
vertical-align: text-bottom;
}
th {
font-size: 1.5rem;
text-align: left;
padding-right: 1rem;
}
td {
text-align: right;
&.amount {
font-size: 2rem;
padding-right: 1.2rem;
}
&.fiat-amount {
color: rgba(255,255,255,0.8);
}
}
}
}
@@ -0,0 +1,100 @@
section#contribution-details {
header {
nav {
.amount {
font-size: 1.5rem;
}
.symbol {
padding-left: 0.2rem;
}
}
}
.content {
width: 100%;
margin: 0 0 1.5rem;
padding: 2rem 2rem;
border-top: 1px solid $item-border-color;
border-bottom: 1px solid $item-border-color;
background-color: $item-background-color;
h3 {
font-size: 1.5rem;
margin-bottom: 2rem;
}
p {
margin-bottom: 2rem;
span {
}
&.who-what-when {
font-size: 1.2rem;
margin-bottom: 1.2rem;
}
&:last-of-type {
margin-bottom: 0;
}
}
a:not(.button) {
color: $primary-color;
text-decoration: none;
&:not(.button):hover {
text-decoration: underline;
}
}
}
.actions {
text-align: center;
}
&.vetoed {
.content {
h3 {
text-decoration: line-through;
}
}
}
.hint.vetoed {
overflow: auto;
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid $item-border-color;
font-size: 1.2rem;
.icon {
display: inline-block;
width: 40px;
height: 40px;
float: left;
margin-right: 1rem;
margin-bottom: 1rem;
svg {
width: 100%;
height: auto;
}
}
}
}
// On small screens, hide intro text, contributor and contributions list when
// showing details
@include media-max(small) {
#dashboard.with-details {
#contributions, #stats {
display: none;
}
}
#intro.with-details {
display: none;
}
}
+19 -50
View File
@@ -6,23 +6,30 @@ main section {
}
}
.quick-filter {
font-size: 1.2rem;
margin-bottom: 2rem;
p {
padding: 0.2rem 0 0;
}
label {
&+ label {
margin-left: 3.6rem;
}
}
}
ul.contribution-list {
clear: both;
width: 100%;
list-style: none;
li {
display: grid;
grid-template-columns: auto 5rem 5rem;
grid-row-gap: 0.5rem;
padding: 0.8rem 1.2rem;
background-color: rgba(255,255,255,0.1);
font-size: 1.2rem;
border-bottom: 1px solid rgba(255,255,255,0.2);
&:first-of-type {
border-top: 1px solid rgba(255,255,255,0.2);
}
cursor: pointer;
&.confirmed {
grid-template-columns: auto 5rem;
@@ -37,8 +44,10 @@ ul.contribution-list {
p {
align-self: center;
margin: 0;
font-size: inherit;
line-height: 2rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.kredits-amount, &.voting {
text-align: right;
@@ -46,11 +55,8 @@ ul.contribution-list {
}
span {
font-size: inherit;
&.title {
a {
font-size: inherit;
text-decoration: none;
color: $body-text-color;
}
@@ -94,41 +100,4 @@ ul.contribution-list {
margin-right: 0.5rem;
}
}
}
@media (min-width: 550px) {
ul.proposal-list {
li {
grid-template-columns: auto 10rem;
grid-row-gap: 0.5rem;
p {
&.kredits-amount, &.voting {
text-align: right;
}
}
&.unconfirmed {
p {
&.kredits-amount, &.voting {
text-align: right;
}
&.description {
grid-column-start: span 1;
}
&.voting {
grid-column-start: span 1;
}
}
}
.description {
font-size: inherit;
}
}
}
}
+19 -50
View File
@@ -1,59 +1,24 @@
table.contributor-list {
position: relative;
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5rem;
tr {
border-bottom: 1px solid rgba(255,255,255,0.2);
background-color: $item-background-color;
border-bottom: 1px solid $item-border-color;
cursor: pointer;
&:first-of-type {
border-top: 1px solid rgba(255,255,255,0.2);
border-top: 1px solid $item-border-color;
}
&.selected {
background-color: $item-highlighted-background-color;
}
background-color: rgba(255,255,255,0.1);
&.current-user {
background-color: rgba(255,255,255,0.2);
}
&.metadata {
height: 0;
visibility: hidden;
&:not(.visible) {
border-bottom: none;
}
td {
padding: 0 1.2rem;
}
a {
color: $primary-color;
&:hover, &:active {
color: #fff;
}
}
ul {
list-style: none;
display: block;
overflow: hidden;
height: 0;
li {
display: inline;
&+li {
margin-left: 1rem;
}
}
}
&.visible {
height: auto;
visibility: visible;
ul {
height: auto;
}
}
font-weight: 500;
}
td {
@@ -71,20 +36,24 @@ table.contributor-list {
&.kredits {
text-align: right;
.amount {
font-size: 1.2rem;
font-weight: 500;
}
.symbol {
font-size: 0.8rem;
padding-left: 0.2rem;
}
}
}
}
pre {
line-height: 1rem;
padding-bottom: 1rem;
}
&.loading {
@include loading-border-top;
&::before {
position: absolute;
}
}
}
@@ -0,0 +1,56 @@
section#contributor-profile {
text-align: center;
header {
z-index: 2;
width: 100%;
text-align: center;
img {
margin: 0 auto;
border: 3px solid $item-border-color;
}
}
.content {
z-index: 1;
width: 100%;
margin: -7.2rem 0 1.5rem;
padding: 6rem 1.2rem 2rem;
border-top: 1px solid $item-border-color;
border-bottom: 1px solid $item-border-color;
background-color: $item-background-color;
h2 {
text-align: center;
margin-bottom: 0.6rem;
}
p.role {
margin-bottom: 2rem;
}
ul.external-accounts {
list-style: none;
li {
display: inline-block;
&+li {
margin-left: 0.6rem;
}
}
}
.actions {
margin-top: 2rem;
}
}
.actions {
.button {
margin-bottom: 0.6rem;
}
}
}
+48
View File
@@ -0,0 +1,48 @@
ul.expense-list {
grid-column-start: span 2;
width: 100%;
margin-top: 0.8rem;
border-collapse: collapse;
li {
display: grid;
grid-template-columns: auto 10rem;
grid-row-gap: 0.5rem;
padding-top: 1.2rem;
border-top: 1px solid $item-border-color;
font-size: 1.2rem;
&:not(:last-child) {
padding-bottom: 1.2rem;
}
}
.description {
grid-row-start: 1;
grid-row-end: 3;
}
.amount {
justify-self: end;
// font-weight: normal;
}
.actions {
justify-self: end;
}
h4 {
font-size: 1.2rem;
font-weight: normal;
line-height: 2rem;
}
p {
line-height: 2rem;
&.description {
font-size: 1rem;
opacity: 0.7;
}
}
}
@@ -0,0 +1,34 @@
.external-accounts a {
display: inline-block;
width: 4rem;
height: 4rem;
padding: 0.8rem;
background-color: rgba(0, 0, 0, 0.3);
color: $body-text-color;
border-radius: 2rem;
span.site {
display: none;
}
svg {
display: block;
position: relative;
width: 100%;
height: 100%;
.fg {
fill: $body-text-color;
}
}
&:hover, &:active {
background-color: rgba(0, 0, 0, 0.5);
svg {
.fg {
fill: $primary-color;
}
}
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
.loading-spinner {
margin-top: 12rem;
text-align: center;
font-size: 1.4rem;
font-size: 1.5rem;
color: $primary-color;
@include media-max(small) {
-114
View File
@@ -1,114 +0,0 @@
ul.proposal-list {
clear: both;
width: 100%;
list-style: none;
li {
display: grid;
grid-template-columns: auto 5rem;
grid-row-gap: 0.5rem;
padding: 1rem 1.2rem;
background-color: rgba(255,255,255,0.1);
font-size: 1.2rem;
border-bottom: 1px solid rgba(255,255,255,0.2);
&:first-of-type {
border-top: 1px solid rgba(255,255,255,0.2);
}
p {
align-self: center;
margin: 0;
font-size: inherit;
line-height: 2rem;
&.kredits-amount, &.voting {
text-align: right;
}
&.description {
grid-column-start: span 2;
}
&.voting {
grid-column-start: span 2;
}
}
span {
font-size: inherit;
}
.description {
line-height: 1.4em;
font-size: 1rem;
}
.category {
color: $blue;
padding-right: 0.2rem;
&.community { color: $red; }
&.dev { color: $pink; }
&.design { color: $yellow; }
&.docs { color: $green; }
&.ops { color: $purple; }
}
.amount {
font-weight: 500;
}
.symbol {
font-size: 1rem;
font-weight: 500;
padding-left: 0.2rem;
}
.recipient {
font-weight: 500;
}
.votes {
font-size: 1rem;
color: $primary-color;
margin-right: 0.5rem;
}
}
}
@media (min-width: 550px) {
ul.proposal-list {
li {
grid-template-columns: auto 10rem;
grid-row-gap: 0.5rem;
p {
&.kredits-amount, &.voting {
text-align: right;
}
}
&.unconfirmed {
p {
&.kredits-amount, &.voting {
text-align: right;
}
&.description {
grid-column-start: span 1;
}
&.voting {
grid-column-start: span 1;
}
}
}
.description {
font-size: inherit;
}
}
}
}
@@ -0,0 +1,29 @@
ul.reimbursement-list {
width: 100%;
> li {
display: grid;
grid-template-columns: auto 12rem;
grid-row-gap: 0.5rem;
padding-top: 1.6rem;
.token-amount {
text-align: right;
img {
height: 1em;
vertical-align: middle;
margin-top: -2px;
}
.amount {
font-size: 1.5rem;
}
.symbol {
font-size: 1rem;
padding-left: 0.2rem;
}
}
}
}
@@ -0,0 +1,17 @@
header#topbar section#user-account {
button {
margin-left: 1.5rem;
}
button + button {
margin-left: 0.6rem;
}
@include media-max(small) {
.anonymous {
display: none;
}
}
}
+5
View File
@@ -13,6 +13,11 @@
h1 {
display: inline-block;
text-transform: uppercase;
a {
color: inherit;
text-decoration: none;
}
}
section#user-account {
+14
View File
@@ -4,4 +4,18 @@ img.avatar {
vertical-align: middle;
margin-right: 0.2rem;
border-radius: 1rem;
&.medium {
margin: 0;
width: 10rem;
height: 10rem;
border-radius: 5rem;
}
&.large {
margin: 0;
width: 256px;
height: 256px;
border-radius: 128px;
}
}
+1 -1
View File
@@ -1 +1 @@
{{loading-spinner}}
<LoadingSpinner />
+2 -12
View File
@@ -1,16 +1,6 @@
<header id="topbar">
<h1>Kosmos Kredits</h1>
<section id="user-account">
{{#if kredits.hasAccounts }}
{{#if kredits.currentUser}}
{{kredits.currentUser.name}}
{{#if kredits.currentUserIsCore}}(core){{/if}}
{{/if}}
{{else}}
Anonymous
{{/if}}
</section>
<h1><LinkTo @route="dashboard">Kosmos Kredits</LinkTo></h1>
<TopbarAccountPanel />
</header>
{{outlet}}
+63
View File
@@ -0,0 +1,63 @@
<main id="budget">
<div id="aside">
<!-- <section id="main&#45;nav"> -->
<!-- <header> -->
<!-- <h2>Budget</h2> -->
<!-- </header> -->
<!-- <div class="content"> -->
<!-- <ul> -->
<!-- <li> -->
<!-- <LinkTo @route="budget.expenses">Expenses</LinkTo> -->
<!-- </li> -->
<!-- <li> -->
<!-- <a href="#">Rewards</a> -->
<!-- </li> -->
<!-- </ul> -->
<!-- </div> -->
<!-- </section> -->
<section id="funds">
<header>
<h2>Community funds</h2>
</header>
<div class="content">
<BudgetBalances />
</div>
</section>
</div>
<div id="content">
{{#if this.reimbursementsUnconfirmed}}
<section id="expenses-unconfirmed">
<header class="with-nav">
<h2>Proposed Reimbursements</h2>
<nav>
<LinkTo @route="reimbursements.new" @title="Submit a reimbursement" class="button small green">add</LinkTo>
</nav>
</header>
<div class="content">
<ReimbursementList @items={{this.reimbursementsUnconfirmed}} />
</div>
</section>
{{/if}}
{{#if this.reimbursementsConfirmed}}
<section id="expenses-confirmed">
<header class="with-nav">
<h2>Confirmed Expenses</h2>
<nav>
<LinkTo @route="reimbursements.new" @title="Submit a reimbursement" class="button small green">add</LinkTo>
</nav>
</header>
<div class="content">
<ReimbursementList @items={{this.reimbursementsConfirmed}} />
</div>
</section>
{{/if}}
</div>
<div id="empty">
</div>
</main>
+1
View File
@@ -0,0 +1 @@
{{outlet}}
View File

Some files were not shown because too many files have changed in this diff Show More