Compare commits

...

87 Commits

Author SHA1 Message Date
69ae5fe2ff fix syntax & unused code warnings 2023-07-10 19:19:53 -06:00
0b7a35c9b8 list the gives to a plan and gives to which this plan contributed 2023-07-09 20:37:08 -06:00
0257901c5b allow viewing of a project without an ID (and other refactors) 2023-07-09 09:38:28 -06:00
d9d6096275 consolidate the "gave" actions on a projecct 2023-07-09 08:31:11 -06:00
81dd6eb595 Merge pull request 'Updates to contacts UI. Sweep for buildIdentity and buildHeaders' (#35) from contacts-identity into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#35
2023-07-08 22:58:51 -04:00
Matthew Raymer
c61bb88788 More cleanup from project task list 2023-07-08 18:42:53 +08:00
Matthew Raymer
3bd55f3ad2 More cleanup and application of new db loading 2023-07-08 18:31:12 +08:00
Matthew Raymer
3471afdf25 Cleaned up messages and improved the accountsDB lookups 2023-07-08 17:39:20 +08:00
Matthew Raymer
e25a83ff1b Move account counter to mounted event 2023-07-08 17:19:52 +08:00
Matthew Raymer
0fbdb45d3e Project Task update 2023-07-07 20:33:43 +08:00
Matthew Raymer
dc23ba1375 Fix a bug in HomeView and clean up recordGive method 2023-07-07 18:43:16 +08:00
Matthew Raymer
08137eb000 Updates to contacts UI. Sweep for buildIdentity and buildHeaders 2023-07-07 18:28:06 +08:00
Matthew Raymer
5d49965166 Simple fix. Missing reference to QuickNav 2023-07-07 16:22:53 +08:00
8e8aa4356d Update 'project.task.yaml' 2023-07-06 22:20:43 -04:00
59a354027e Update 'src/views/StatisticsView.vue'
Clean up spurious comment.
2023-07-06 21:36:10 -04:00
Matthew Raymer
5dc80ce12a A bit more cleanup. Problem in Contacts to resolve. 2023-07-06 18:33:13 +08:00
Matthew Raymer
754bced2a9 Considerable cleanup. I think I also found the issue from the other day with values not loading from settings. 2023-07-06 18:12:21 +08:00
Matthew Raymer
e3f58bd593 Purge all vue-class-component with vue-facing-decorator.
Make some strike-throughs for project-task
Update package.json
2023-07-06 17:28:08 +08:00
Matthew Raymer
3b41014083 Considerable cleanup and merge 2023-07-06 16:59:50 +08:00
f568149745 Merge pull request 'allow choice of no identity (for testing)' (#32) from choose-no-id into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#32
2023-07-06 02:40:11 -04:00
a27d035e9b Merge branch 'master' into choose-no-id 2023-07-06 02:40:00 -04:00
16d0be681c Merge pull request 'quicknav-component' (#34) from quicknav-component into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#34
2023-07-06 02:38:19 -04:00
Matthew Raymer
5be67fd4c9 Rolled out to all views that had HTML quicknav 2023-07-05 17:59:31 +08:00
Matthew Raymer
dda3ad057d This is the QuickNav component 2023-07-05 17:58:18 +08:00
Matthew Raymer
cf54096326 Looks like GiftedDialog works? A little cleanup. 2023-07-05 16:27:21 +08:00
49c3971cf2 Merge pull request 'First draft of Vue3 version. WIll finish after error-logging merge' (#31) from gifted-dialog-conversion into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#31
2023-07-05 03:07:35 -04:00
Matthew Raymer
80a1185faa Fix for missing scene-container 2023-07-05 15:59:58 +08:00
cd8bc73bac Merge branch 'master' into gifted-dialog-conversion 2023-07-05 03:07:09 -04:00
e42b3ff11d allow choice of no identity (for testing) 2023-07-04 13:15:52 -06:00
d98e95915b Merge pull request 'error-logging' (#29) from error-logging into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#29
2023-07-04 12:15:24 -04:00
Matthew Raymer
4758a740de First draft of Vue3 version. WIll finish after error-logging merge 2023-07-04 20:03:36 +08:00
Matthew Raymer
0a020a4069 Clean up vestigle old alert code 2023-07-04 19:29:12 +08:00
Matthew Raymer
c859778832 Add AlertMessage component 2023-07-04 19:27:57 +08:00
Matthew Raymer
c24022c41c Make the visibility of the Alert indirect 2023-07-04 19:26:05 +08:00
Matthew Raymer
0fd4b86a84 A bit of linting 2023-07-03 21:06:24 +08:00
Matthew Raymer
c31445865e Straggler 2023-07-03 21:05:45 +08:00
Matthew Raymer
0af03227a6 Propagated AlertMessage component 2023-07-03 21:04:53 +08:00
Matthew Raymer
3c977a1f28 First roll out of an AlertMessage control 2023-07-03 20:24:23 +08:00
Matthew Raymer
8d8635a3e6 Added try...catch and linted 2023-07-03 19:44:41 +08:00
Matthew Raymer
bcc6de6fc0 New AlertMessage component 2023-07-03 19:40:53 +08:00
Matthew Raymer
99ea161da0 Removed created() and relocated to mounted() could not replicate issue 2023-07-03 18:48:01 +08:00
Matthew Raymer
3f6dbdebef Moved checkLimits inside try...catch since it has an identity check 2023-07-03 18:46:23 +08:00
Matthew Raymer
b139957e3e Added identity check 2023-07-03 18:44:42 +08:00
6e4f6d090a Merge pull request 'add project search (which just goes to console for now)' (#28) from search-projects into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#28
2023-07-02 20:21:35 -04:00
48227e8cf2 Merge branch 'master' into search-projects 2023-07-02 20:21:19 -04:00
09f02ca4b2 Merge pull request 'Quick links to record a gift, to this person or to a project' (#27) from give-to-project into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#27
2023-07-02 20:20:51 -04:00
9b3823ef0e Merge branch 'master' into give-to-project 2023-07-02 20:06:11 -04:00
cdeece1795 add project search (which just goes to console for now) 2023-07-02 07:55:45 -06:00
c2ebaa0a76 mark the give-to-project task as complete 2023-07-02 06:46:31 -06:00
3f60051599 Merge branch 'master' into give-to-project 2023-07-01 22:33:23 -06:00
a8f1e25986 add error messages for gives 2023-07-01 22:20:10 -06:00
964248e895 Update 'project.task.yaml' 2023-07-01 21:04:06 -04:00
a2b3cebdb3 finish contact selection for gives 2023-07-01 15:45:30 -06:00
bc6e52774c update tasks 2023-07-01 07:13:39 -06:00
643f777d10 start linking gives to projects 2023-07-01 07:07:46 -06:00
ec1d8404ca Merge pull request 'start a walk-through of the test' (#26) from scenarios into master
Reviewed-on: trent_larson/kick-starter-for-time-pwa#26
2023-07-01 08:12:55 -04:00
Jose Olarte III
1d6241abbb Various changes
- Loading animation in Projects view
- Per item icon + spacing fixes in Home view
2023-07-01 16:04:32 +08:00
c40b690878 fix load-project function 2023-06-28 20:28:14 -06:00
c9c81f1e5c fix wording in list of contacts, remove console.log 2023-06-28 20:20:06 -06:00
a94069e70a update tasks from previous conversation, marking milestone:2 done 2023-06-27 20:54:07 -06:00
Matthew Raymer
53f42e1ad3 Updates to fix merge and remove console.log 2023-06-27 20:07:38 +08:00
Matthew Raymer
5f0bbccbe6 Merge branch 'infinitescroll-test' 2023-06-27 19:06:41 +08:00
Matthew Raymer
3ec9056901 Merging remote master into local 2023-06-27 19:06:25 +08:00
Matthew Raymer
6d3ab7c313 Added loading settings (no image yet) plus refactored types a little more. 2023-06-27 18:57:36 +08:00
d9e9a7b740 start a walk-through 2023-06-26 20:00:23 -06:00
ea95382fdf change jwtId to rowid for paging within a derivative/cached table 2023-06-26 19:20:02 -06:00
072b663ec9 change comment whitespace to get past lint errors 2023-06-26 19:18:38 -06:00
Matthew Raymer
6393a20e7e Documentation added 2023-06-26 20:45:13 +08:00
Matthew Raymer
19d934eb28 Do a merge 2023-06-26 20:02:56 +08:00
Matthew Raymer
49ce7d43b0 Refactored and streamlined code. Still having an issue with beforeId? 2023-06-26 19:53:28 +08:00
Matthew Aaron Raymer
6233189a49 Merging next-iteration 2023-06-26 13:59:47 +08:00
Matthew Aaron Raymer
ffb0f2d37f Merge branch 'master' of ssh://173.199.124.46:222/trent_larson/kick-starter-for-time-pwa into allow-new 2023-06-26 13:49:59 +08:00
db7b3fff06 mark another task off (no code changes) 2023-06-25 17:22:47 -06:00
bd75802a0c mark another task fixed (no code changes) 2023-06-25 17:22:06 -06:00
1a86730354 make display on creation page look halfway decent, and switch fully to it 2023-06-25 17:20:22 -06:00
a96728bec5 add better messaging on registration failure 2023-06-25 16:41:32 -06:00
944b0ad759 fix registration, separate ID creation to allow new random ones, and refactor warning and other verbiage 2023-06-24 21:07:21 -06:00
42bf34f549 fix infinitescroll to work off correct ID 2023-06-24 06:45:03 -06:00
Matthew Raymer
071c41b70c Firing properly but no data returned on extended fetch 2023-06-24 17:47:18 +08:00
5747404fd6 add marker in feed to show where they've seen claims, plus other small clean-up 2023-06-23 20:17:54 -06:00
639f630436 remove old texture image that's been replaced 2023-06-23 17:37:09 -06:00
a8794be2ea sllow quick gifting all the way to the server, maybe with hours 2023-06-23 17:00:20 -06:00
Matthew Raymer
0726a8d3ba New modules and InfiniteScroll init 2023-06-23 18:56:26 +08:00
aa2f484a9f add the other activity envisioned on the home page (though not sending data yet) 2023-06-22 20:51:06 -06:00
07e7a70d56 in feed: add token for authorized request, plus better descriptions 2023-06-12 20:29:55 -06:00
6daa515d19 load feed of give records on home screen 2023-06-11 20:37:34 -06:00
d5336dbf1b docs & comments & labels 2023-06-11 17:02:20 -06:00
43 changed files with 5342 additions and 4309 deletions

View File

@@ -20,11 +20,6 @@ npm run build
npm run lint npm run lint
``` ```
### Clear data & restart
Clear cache for localhost, then go to http://localhost:8080/start
(because it'll generate a new one automatically if you start on the `/account` page).
### Test key contents ### Test key contents
See [this page](openssl_signing_console.rst) See [this page](openssl_signing_console.rst)
@@ -59,17 +54,35 @@ playing one of two ways:
### Create multiple identifiers ### Create multiple identifiers
Go to /import-account and import a new one. Then switch identifiers on the Go to /start and create or import a new one. Then switch identifiers on the bottom of the Your Identity page.
bottom of the Your Identity page.
### Create keys with alternate tools ### Create keys with alternate tools
See [this page](openssl_signing_console.rst) See [this page](openssl_signing_console.rst)
### Customize configuration ### Customize Vue configuration
See [Configuration Reference](https://cli.vuejs.org/config/). See [Configuration Reference](https://cli.vuejs.org/config/).
## Scenarios
- Create a new identity as prompted. Go to "Your Identity" screen and copy the ID to the clipboard.
- Go back to /start and import test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` with this this seed phrase:
`seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control`
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
- Go to "Your Contacts" screen and add the ID you copied to the clipboard, and hit "+" to add them.
- Click on the "Registration Unknown" button and register that person to be able to make claims as them.
### Clear data & restart
Clear cache for localhost, then go to http://localhost:8080/start
(because it'll generate a new one automatically if you start on the `/account` page).
## Dependencies ## Dependencies
See https://tea.xyz See https://tea.xyz
@@ -188,6 +201,9 @@ export const createAndStoreIdentifier = async (mnemonicPassword) => {
## Kudos ## Kudos
Gifts make the world go 'round!
* [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80) * [Máximo Fernández](https://medium.com/@maxfarenas) for the 3D [code](https://github.com/maxfer03/vue-three-ns) and [explanatory post](https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80)
* [Many libraries]() such as Veramo.io, Vuejs.org, threejs * [Many tools & libraries]() such as Nodejs.org, IntelliJ Idea, Veramo.io, Vuejs.org, threejs.org
* [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439) * [Bush 3D model](https://sketchfab.com/3d-models/lupine-plant-bf30f1110c174d4baedda0ed63778439)
* [Forest floor image](https://www.goodfreephotos.com/albums/textures/leafy-autumn-forest-floor.jpg)

6060
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,54 +12,53 @@
"@fortawesome/fontawesome-svg-core": "^6.4.0", "@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^3.0.3", "@fortawesome/vue-fontawesome": "^3.0.3",
"@pvermeer/dexie-encrypted-addon": "^2.0.5", "@pvermeer/dexie-encrypted-addon": "^3.0.0",
"@tweenjs/tween.js": "^20.0.3", "@tweenjs/tween.js": "^21.0.0",
"@veramo/core": "^5.1.2", "@veramo/core": "^5.2.0",
"@veramo/credential-w3c": "^5.1.4", "@veramo/credential-w3c": "^5.2.0",
"@veramo/data-store": "^5.1.2", "@veramo/data-store": "^5.2.0",
"@veramo/did-manager": "^5.1.2", "@veramo/did-manager": "^5.1.2",
"@veramo/did-provider-ethr": "^5.1.2", "@veramo/did-provider-ethr": "^5.1.2",
"@veramo/did-resolver": "^5.1.2", "@veramo/did-resolver": "^5.2.0",
"@veramo/key-manager": "^5.1.2", "@veramo/key-manager": "^5.1.2",
"@vueuse/core": "^9.13.0", "@vueuse/core": "^10.2.1",
"@zxing/text-encoding": "^0.9.0", "@zxing/text-encoding": "^0.9.0",
"axios": "^1.3.4", "axios": "^1.4.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"core-js": "^3.29.1", "core-js": "^3.31.1",
"dexie": "^3.2.3", "dexie": "^3.2.4",
"dexie-export-import": "^4.0.7", "dexie-export-import": "^4.0.7",
"did-jwt": "^6.11.5", "did-jwt": "^7.2.4",
"ethereum-cryptography": "^1.2.0", "ethereum-cryptography": "^2.0.0",
"ethereumjs-util": "^7.1.5", "ethereumjs-util": "^7.1.5",
"ethr-did-resolver": "^8.0.0", "ethr-did-resolver": "^8.0.0",
"js-generate-password": "^0.1.7", "js-generate-password": "^0.1.9",
"localstorage-slim": "^2.4.0", "localstorage-slim": "^2.4.0",
"luxon": "^3.3.0", "luxon": "^3.3.0",
"merkletreejs": "^0.3.9", "merkletreejs": "^0.3.10",
"moment": "^2.29.4", "moment": "^2.29.4",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pina": "^0.20.2204228", "pina": "^0.20.2204228",
"pinia-plugin-persistedstate": "^3.1.0", "pinia-plugin-persistedstate": "^3.1.0",
"qr-code-generator-vue3": "^1.4.21", "qr-code-generator-vue3": "^1.4.21",
"ramda": "^0.28.0", "ramda": "^0.29.0",
"readable-stream": "^4.3.0", "readable-stream": "^4.4.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"three": "^0.152.2", "three": "^0.154.0",
"vue": "^3.2.47", "vue": "^3.3.4",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-class-component": "^8.0.0-0", "vue-facing-decorator": "^2.1.20",
"vue-facing-decorator": "^2.1.19",
"vue-property-decorator": "^9.1.2", "vue-property-decorator": "^9.1.2",
"vue-router": "^4.1.6", "vue-router": "^4.2.3",
"web-did-resolver": "^2.0.22" "web-did-resolver": "^2.0.27"
}, },
"devDependencies": { "devDependencies": {
"@types/ramda": "^0.28.23", "@types/ramda": "^0.29.3",
"@types/three": "^0.152.0", "@types/three": "^0.152.1",
"@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/eslint-plugin": "^5.61.0",
"@typescript-eslint/parser": "^5.57.0", "@typescript-eslint/parser": "^5.61.0",
"@vue/cli-plugin-babel": "~5.0.8", "@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8", "@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-pwa": "~5.0.8", "@vue/cli-plugin-pwa": "~5.0.8",
@@ -67,15 +66,15 @@
"@vue/cli-plugin-typescript": "~5.0.8", "@vue/cli-plugin-typescript": "~5.0.8",
"@vue/cli-plugin-vuex": "~5.0.8", "@vue/cli-plugin-vuex": "~5.0.8",
"@vue/cli-service": "~5.0.8", "@vue/cli-service": "~5.0.8",
"@vue/eslint-config-typescript": "^11.0.2", "@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"eslint": "^8.37.0", "eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^5.0.0-alpha.1",
"eslint-plugin-vue": "^9.10.0", "eslint-plugin-vue": "^9.15.1",
"postcss": "^8.4.21", "postcss": "^8.4.24",
"prettier": "^2.8.7", "prettier": "^3.0.0",
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.2",
"typescript": "~5.0.3" "typescript": "~5.1.6"
} }
} }

View File

@@ -1,51 +1,79 @@
- top screens from img/screens.pdf milestone:2 : tasks:
- view all : - .5 audit all console.log calls
- add infinite scroll assignee:matthew - 01 design ideas for simple gives on the Home page
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time - 01 add list of 'give' records for a project on ProjectView UI
- 02 Discover page - display results (currently in console.log) & link, spin when searching
- 08 search by location, endpoint, etc assignee:trent
- 01 add a location for a project via map pin (see API by clicking on "Nearby")
- 01 remove all the "form" fields (or at least investigate to see if that page refresh is desired)
- .2 change "errorMessage" to "alertMessage" on ProjectViewView.vue
- allow type annotations in World.js & landmarks.js (since we get this error: "Types are not supported by current JavaScript version") - 08 Scan QR code to import into contacts.
- replace user-affecting console.log & console.error with error messages (eg. catches)
- 8 Move to vue-facing-decorator
- stats v1 :
- 01 show numeric stats
- 01 link to world for specific stats
- contacts v1 : - contacts v1 :
- 01 Import contact info a la QR code. - 01 Import contact info a la QR code.
- 01 Import all the non-sensitive data (ie. contacts & settings). - 01 Import all the non-sensitive data (ie. contacts & settings).
- .2 move all "identity" references to temporary account access - .2 move all "identity" references to temporary account access assignee:trent
- contacts v+ : - contacts v+ :
- .2 show error to user when adding a duplicate contact - .2 show error to user when adding a duplicate contact
- parse input more robustly (with CSV lib and not commas) - 01 parse input more robustly (with CSV lib and not commas)
- refactor UI : - refactor UI :
- .5 Alerts show at the top and can be missed, eg. account data download - .5 Alerts show at the top and can be missed, eg. account data download
- 01 Change alerts into a component (to cut down duplicate code)
- 01 Change "nav" tabs across the bottom into a component (eliminating duplicate code).
- .5 Fix how icons show on top of bottom bar on ContactAmounts page - .5 Fix how icons show on top of bottom bar on ContactAmounts page
- .2 Hide "Advanced" section in Account page by default
- show pop-up confirming that settings & contacts have been downloaded - show pop-up confirming that settings & contacts have been downloaded
- Ensure each action sent to the server has a confirmation. - Ensure each action sent to the server has a confirmation - registration
- discover screen - Home Feed & Quick Give screen :
- 01 save the feed-viewed status in settings storage ("afterQuery")
- 01 quick action - send action, maybe choose via canvas tool https://github.com/konvajs/vue-konva
- .5 customize favicon - .5 customize favicon
- .5 make advanced features harder to access; advanced build? - 04 allow user to download claims, mine + ones I can see about me from others
- 24 Move to Vite - 24 Move to Vite
- 40 notifications :
- push
- Discuss whether the remaining tasks are worthwhile before MVP release.
- 01 fix images on project page, on discovery page
- .2 fix "Rotary" and static icon to the right on project page
- stats v1 :
- 01 show numeric stats
- 01 link to world for specific stats
- .5 don't load another instance of a bush if it already exists
- maybe - allow type annotations in World.js & landmarks.js (since we get this error - "Types are not supported by current JavaScript version")
- 08 convert to cleaner implementation (maybe Drie -- https://github.com/janvorisek/drie)
- Do we want split first name & last name?
- Release Minimum Viable Product : - Release Minimum Viable Product :
- 08 thorough testing for errors & edge cases
- Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot). - Turn off stats-world or ensure it's usable (eg. cannot zoom out too far and lose world, cannot screenshot).
- Add disclaimers. - Add disclaimers.
- Rename DB to TimeSafari.
- Switch default server to the public server. - Switch default server to the public server.
- Deploy to a server. - Deploy to a server.
- Ensure public server has limits that work for group adoption. - Ensure public server has limits that work for group adoption.
- Test PWA features on Android and iOS. - Test PWA features on Android and iOS.
blocks: ref:https://raw.githubusercontent.com/trentlarson/lives-of-gifts/master/project.yaml#kickstarter%20for%20time
- 40 notifications v+ :
- pull, w/ scheduled runs
- linking between projects or plans :
- show total time given to & from a project
- terminology:
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
- for blocking: blocks, precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
- Stats : - Stats :
- 01 point out user's location on the world - 01 point out user's location on the world
@@ -55,11 +83,6 @@
- badge for amount given/offered to your project - badge for amount given/offered to your project
- set a goal of given/offers - set a goal of given/offers
- linking between projects or plans :
- terminology:
- Fulfills, Feeds, contributes to, supplies, boosts, advances
- Precedes, comes before, is sought by -- vs follows, seeks, builds on ("contributes to" isn't specific enough, "succeeds" has different, possibly confusing meaning)
- automated tests, eg. cypress - automated tests, eg. cypress
- Notifications (wake on the phone, push notifications) - Notifications (wake on the phone, push notifications)
@@ -70,10 +93,11 @@
- Peer DID - Peer DID
- DIDComm - DIDComm
- Write to or read from a different ledger (eg. private ACDC, attest.sh) - Write to or read from a different ledger (eg. private ACDC, attest.sh)
- v video for multiple identities https://youtu.be/p8L87AeD76w
- v video for adding time to contacts https://youtu.be/7Yylczevp10 log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29
- project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

View File

@@ -0,0 +1,47 @@
<template>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
@Component
export default class AlertMessage extends Vue {
@Prop alertTitle = "";
@Prop alertMessage = "";
isAlertVisible = this.alertMessage;
public onClickClose() {
this.isAlertVisible = false;
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped></style>

View File

@@ -0,0 +1,117 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-lg text-center">
{{ message }} {{ giver?.name || "somebody not specified" }}
</h1>
<input
type="text"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
placeholder="What was received"
v-model="description"
/>
<div class="flex flex-row">
<span class="py-4">Hours</span>
<input
type="text"
class="block w-8 rounded border border-slate-400 ml-4 text-center"
v-model="hours"
/>
<div class="flex flex-col px-1">
<div>
<fa icon="square-caret-up" size="2xl" @click="increment()" />
</div>
<div>
<fa icon="square-caret-down" size="2xl" @click="decrement()" />
</div>
</div>
</div>
<p class="text-right">Sign & Send to publish to the world</p>
<div class="text-right">
<button class="rounded border border-slate-400" @click="confirm">
<span class="m-2">Sign & Send</span>
</button>
&nbsp;
<button class="rounded border border-slate-400" @click="cancel">
<span class="m-2">Cancel</span>
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
@Component
export default class GiftedDialog extends Vue {
@Prop message = "";
giver = null;
description = "";
hours = "0";
visible = false;
open(giver) {
// giver: GiverInputInfo
this.giver = giver;
this.visible = true;
}
close() {
this.visible = false;
}
increment() {
this.hours = `${(parseFloat(this.hours) || 0) + 1}`;
}
decrement() {
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
}
@Emit("dialog-result")
confirm() {
const result = {
action: "confirm",
giver: this.giver,
hours: parseFloat(this.hours),
description: this.description,
};
this.close();
this.description = "";
this.giver = null;
this.hours = "0";
return result;
}
@Emit("dialog-result")
cancel() {
const result = { action: "cancel" };
this.close();
return result;
}
}
</script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 50%;
}
</style>

View File

@@ -119,15 +119,11 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Prop, Vue } from "vue-facing-decorator";
@Options({ @Component
props: {
msg: String,
},
})
export default class HelloWorld extends Vue { export default class HelloWorld extends Vue {
msg!: string; @Prop msg!: string;
} }
</script> </script>

View File

@@ -10,23 +10,33 @@ import { Component, Emit, Prop, Vue } from "vue-facing-decorator";
@Component @Component
export default class InfiniteScroll extends Vue { export default class InfiniteScroll extends Vue {
@Prop({ default: 100 }) @Prop({ default: 200 })
readonly distance!: number; readonly distance!: number;
private observer!: IntersectionObserver; private observer!: IntersectionObserver;
private isInitialRender = true;
mounted() { updated() {
const options = { if (!this.observer) {
root: this.$refs.scrollContainer as HTMLElement, const options = {
rootMargin: `0px 0px ${this.distance}px 0px`, root: null,
threshold: 1.0, rootMargin: `0px 0px ${this.distance}px 0px`,
}; threshold: 1.0,
this.observer = new IntersectionObserver(this.handleIntersection, options); };
this.observer.observe(this.$refs.sentinel as HTMLElement); this.observer = new IntersectionObserver(
this.handleIntersection,
options,
);
this.observer.observe(this.$refs.sentinel as HTMLElement);
}
} }
// 'beforeUnmount' hook runs before unmounting the component
beforeUnmount() { beforeUnmount() {
this.observer.disconnect(); if (this.observer) {
this.observer.disconnect();
}
} }
@Emit("reached-bottom") @Emit("reached-bottom")
handleIntersection(entries: IntersectionObserverEntry[]) { handleIntersection(entries: IntersectionObserverEntry[]) {
const entry = entries[0]; const entry = entries[0];

View File

@@ -0,0 +1,93 @@
<template>
<!-- QUICK NAV -->
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li
:class="{
'basis-1/5': true,
'rounded-md': true,
'bg-slate-400 text-white': selected === 'Home',
'text-slate-500': selected !== 'Home',
}"
>
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li
:class="{
'basis-1/5': true,
'rounded-md': true,
'bg-slate-400 text-white': selected === 'Discover',
'text-slate-500': selected !== 'Discover',
}"
>
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li
:class="{
'basis-1/5': true,
'rounded-md': true,
'bg-slate-400 text-white': selected === 'Projects',
'text-slate-500': selected !== 'Projects',
}"
>
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li
:class="{
'basis-1/5': true,
'rounded-md': true,
'bg-slate-400 text-white': selected === 'Contacts',
'text-slate-500': selected !== 'Contacts',
}"
>
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li
:class="{
'basis-1/5': true,
'rounded-md': true,
'bg-slate-400 text-white': selected === 'Profile',
'text-slate-500': selected !== 'Profile',
}"
>
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
@Component
export default class QuickNav extends Vue {
@Prop selected = "";
}
</script>

View File

@@ -5,7 +5,7 @@ function createCamera() {
35, // fov = Field Of View 35, // fov = Field Of View
1, // aspect ratio (dummy value) 1, // aspect ratio (dummy value)
0.1, // near clipping plane 0.1, // near clipping plane
350 // far clipping plane 350, // far clipping plane
); );
// move the camera back so we can view the scene // move the camera back so we can view the scene

View File

@@ -22,14 +22,16 @@ export async function loadLandmarks(vue, world, scene, loop) {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts); const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const token = await accessToken(identity);
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token,
}; };
const identity = JSON.parse(account?.identity || "null");
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
const resp = await axios.get(url, { headers: headers }); const resp = await axios.get(url, { headers: headers });
if (resp.status === 200) { if (resp.status === 200) {
const landmarks = resp.data.data; const landmarks = resp.data.data;
@@ -60,7 +62,11 @@ export async function loadLandmarks(vue, world, scene, loop) {
// calculate positions for each claim, especially because some are random // calculate positions for each claim, especially because some are random
const locations = landmarks.map((claim) => const locations = landmarks.map((claim) =>
locForGive(claim, world.PLATFORM_SIZE, world.PLATFORM_EDGE_FOR_UNKNOWNS) locForGive(
claim,
world.PLATFORM_SIZE,
world.PLATFORM_EDGE_FOR_UNKNOWNS,
),
); );
// eslint-disable-next-line @typescript-eslint/no-this-alias // eslint-disable-next-line @typescript-eslint/no-this-alias
@@ -90,7 +96,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
undefined, undefined,
function (error) { function (error) {
console.error(error); console.error(error);
} },
); );
// calculate when lights shine on appearing claim area // calculate when lights shine on appearing claim area
@@ -118,7 +124,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
.onComplete(() => { .onComplete(() => {
scene.remove(light); scene.remove(light);
light.dispose(); light.dispose();
}) }),
) )
.start(); .start();
world.lights = [...world.lights, light]; world.lights = [...world.lights, light];
@@ -127,18 +133,18 @@ export async function loadLandmarks(vue, world, scene, loop) {
console.error( console.error(
"Got bad server response status & data of", "Got bad server response status & data of",
resp.status, resp.status,
resp.data resp.data,
); );
vue.setAlert( vue.setAlert(
"Error With Server", "Error With Server",
"There was an error retrieving your claims from the server." "There was an error retrieving your claims from the server.",
); );
} }
} catch (error) { } catch (error) {
console.error("Got exception contacting server:", error); console.error("Got exception contacting server:", error);
vue.setAlert( vue.setAlert(
"Error With Server", "Error With Server",
"There was a problem retrieving your claims from the server." "There was a problem retrieving your claims from the server.",
); );
} }
} }

View File

@@ -2,7 +2,7 @@ import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three";
export function createTerrain(props) { export function createTerrain(props) {
const loader = new TextureLoader(); const loader = new TextureLoader();
const height = loader.load("img/textures/forest-floor.png"); const height = loader.load("img/textures/leafy-autumn-forest-floor.jpg");
// w h // w h
const geometry = new PlaneGeometry(props.width, props.height, 64, 64); const geometry = new PlaneGeometry(props.width, props.height, 64, 64);

View File

@@ -41,7 +41,12 @@ const NonsensitiveSchemas = Object.assign({}, ContactsSchema, SettingsSchema);
* https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw * https://stackoverflow.com/questions/72474803/error-the-top-level-await-experiment-is-not-enabled-set-experiments-toplevelaw
*/ */
// create password and place password in localStorage /**
* Create password and place password in localStorage.
*
* It's good practice to keep the data encrypted at rest, so we'll do that even
* if the secret is stored right next to the app.
*/
const secret = const secret =
localStorage.getItem("secret") || Encryption.createRandomEncryptionKey(); localStorage.getItem("secret") || Encryption.createRandomEncryptionKey();

View File

@@ -3,6 +3,8 @@ export type Account = {
dateCreated: string; dateCreated: string;
derivationPath: string; derivationPath: string;
did: string; did: string;
// stringified JSON containing underlying key material of type IIdentifier
// https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts
identity: string; identity: string;
publicKeyHex: string; publicKeyHex: string;
mnemonic: string; mnemonic: string;

View File

@@ -1,10 +1,12 @@
// a singleton // a singleton
export type Settings = { export type Settings = {
id: number; // there's only one entry: MASTER_SETTINGS_KEY id: number; // there's only one entry: MASTER_SETTINGS_KEY
activeDid?: string; activeDid?: string;
apiServer?: string; apiServer?: string;
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
lastViewedClaimId?: string;
showContactGivesInline?: boolean; showContactGivesInline?: boolean;
}; };

View File

@@ -20,7 +20,7 @@ export const newIdentifier = (
address: string, address: string,
publicHex: string, publicHex: string,
privateHex: string, privateHex: string,
derivationPath: string derivationPath: string,
): Omit<IIdentifier, keyof "provider"> => { ): Omit<IIdentifier, keyof "provider"> => {
return { return {
did: DEFAULT_DID_PROVIDER_NAME + ":" + address, did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
@@ -46,7 +46,7 @@ export const newIdentifier = (
* @return {*} {[string, string, string, string]} * @return {*} {[string, string, string, string]}
*/ */
export const deriveAddress = ( export const deriveAddress = (
mnemonic: string mnemonic: string,
): [string, string, string, string] => { ): [string, string, string, string] => {
const UPORT_ROOT_DERIVATION_PATH = "m/7696500'/0'/0'/0'"; const UPORT_ROOT_DERIVATION_PATH = "m/7696500'/0'/0'/0'";
mnemonic = mnemonic.trim().toLowerCase(); mnemonic = mnemonic.trim().toLowerCase();
@@ -134,7 +134,7 @@ export function fromJose(signature: string): {
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature); const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
if (signatureBytes.length < 64 || signatureBytes.length > 65) { if (signatureBytes.length < 64 || signatureBytes.length > 65) {
throw new TypeError( throw new TypeError(
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}` `Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
); );
} }
const r = bytesToHex(signatureBytes.slice(0, 32)); const r = bytesToHex(signatureBytes.slice(0, 32));

View File

@@ -1,6 +1,31 @@
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt";
import { Axios, AxiosResponse } from "axios";
import { Contact } from "@/db/tables/contacts";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
export const SERVICE_ID = "endorser.ch"; export const SERVICE_ID = "endorser.ch";
export interface AgreeVerifiableCredential {
"@context": string;
"@type": string;
// "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: Record<any, any>;
}
export interface GiverInputInfo {
did?: string;
name?: string;
}
export interface ClaimResult {
success: { claimId: string; handleId: string };
error: { code: string; message: string };
}
export interface GenericClaim { export interface GenericClaim {
"@context": string; "@context": string;
"@type": string; "@type": string;
@@ -10,14 +35,6 @@ export interface GenericClaim {
claim: Record<any, any>; claim: Record<any, any>;
} }
export interface AgreeVerifiableCredential {
"@context": string;
"@type": string;
// "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: Record<any, any>;
}
export interface GiveServerRecord { export interface GiveServerRecord {
agentDid: string; agentDid: string;
amount: number; amount: number;
@@ -33,10 +50,11 @@ export interface GiveServerRecord {
export interface GiveVerifiableCredential { export interface GiveVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree "@context"?: string; // optional when embedded, eg. in an Agree
"@type": string; "@type": string;
agent: { identifier: string }; agent?: { identifier: string };
description?: string; description?: string;
fulfills?: { "@type": string; identifier: string };
identifier?: string; identifier?: string;
object: { amountOfThisGood: number; unitCode: string }; object?: { amountOfThisGood: number; unitCode: string };
recipient: { identifier: string }; recipient: { identifier: string };
} }
@@ -47,3 +65,135 @@ export interface RegisterVerifiableCredential {
object: string; object: string;
recipient: { identifier: string }; recipient: { identifier: string };
} }
export interface InternalError {
error: string; // for system logging
userMessage?: string; // for user display
}
// This is used to check for hidden info.
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN";
export function isHiddenDid(did) {
return did === HIDDEN_DID;
}
/**
always returns text, maybe UNNAMED_VISIBLE or UNKNOWN_ENTITY
**/
export function didInfo(did, activeDid, identifiers, contacts) {
const myId: IIdentifier | undefined = R.find(
(i) => i.did === did,
identifiers,
);
if (myId) {
return "You" + (myId.did !== activeDid ? " (Alt ID)" : "");
} else {
const contact: Contact | undefined = R.find((c) => c.did === did, contacts);
if (contact) {
return contact.name || "Someone Unnamed in Contacts";
} else if (!did) {
return "Unspecified Person";
} else if (isHiddenDid(did)) {
return "Someone Not In Network";
} else {
return "Someone Not In Contacts";
}
}
}
/**
* For result, see https://endorser.ch:3000/api-docs/#/claims/post_api_v2_claim
*
* @param identity
* @param fromDid may be null
* @param toDid
* @param description may be null; should have this or hours
* @param hours may be null; should have this or description
*/
export async function createAndSubmitGive(
axios: Axios,
apiServer: string,
identity: IIdentifier,
fromDid: string,
toDid: string,
description: string,
hours: number,
fulfillsProjectHandleId?: string,
): Promise<AxiosResponse<ClaimResult> | InternalError> {
// Make a claim
const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org",
"@type": "GiveAction",
recipient: { identifier: toDid },
};
if (fromDid) {
vcClaim.agent = { identifier: fromDid };
}
if (description) {
vcClaim.description = description;
}
if (hours) {
vcClaim.object = { amountOfThisGood: hours, unitCode: "HUR" };
}
if (fulfillsProjectHandleId) {
vcClaim.fulfills = {
"@type": "PlanAction",
identifier: fulfillsProjectHandleId,
};
}
// Make a payload for the claim
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
},
};
// Create a signature using private key of identity
if (identity.keys[0].privateKeyHex == null) {
return new Promise<InternalError>((resolve, reject) => {
reject({
error: "No private key",
message:
"Your identifier " +
identity.did +
" is not configured correctly. Use a different identifier.",
});
});
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!;
const signer = await SimpleSigner(privateKeyHex);
const alg = undefined;
// Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg,
issuer: identity.did,
signer: signer,
});
// Make the xhr request payload
const payload = JSON.stringify({ jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim";
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return axios.post(url, payload, { headers });
}
// from https://stackoverflow.com/a/175787/845494
//
export function isNumeric(str: string): boolean {
return !isNaN(+str);
}
export function numberOrZero(str: string): number {
return isNumeric(str) ? +str : 0;
}

View File

@@ -81,7 +81,7 @@ function didProviderName(netName: string) {
const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet"; const DEFAULT_DID_PROVIDER_NETWORK_NAME = "mainnet";
export const DEFAULT_DID_PROVIDER_NAME = didProviderName( export const DEFAULT_DID_PROVIDER_NAME = didProviderName(
DEFAULT_DID_PROVIDER_NETWORK_NAME DEFAULT_DID_PROVIDER_NETWORK_NAME,
); );
export const HANDY_APP = false; export const HANDY_APP = false;

View File

@@ -10,12 +10,16 @@ import "./assets/styles/tailwind.css";
import { library } from "@fortawesome/fontawesome-svg-core"; import { library } from "@fortawesome/fontawesome-svg-core";
import { import {
faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleQuestion, faCircleQuestion,
faCircleUser, faCircleUser,
faClock,
faCoins,
faComment,
faCopy, faCopy,
faEllipsisVertical, faEllipsisVertical,
faEye, faEye,
@@ -23,6 +27,7 @@ import {
faFileLines, faFileLines,
faFloppyDisk, faFloppyDisk,
faFolderOpen, faFolderOpen,
faGift,
faHand, faHand,
faHouseChimney, faHouseChimney,
faLongArrowAltLeft, faLongArrowAltLeft,
@@ -36,6 +41,8 @@ import {
faRotate, faRotate,
faShareNodes, faShareNodes,
faSpinner, faSpinner,
faSquareCaretDown,
faSquareCaretUp,
faTrashCan, faTrashCan,
faUser, faUser,
faUsers, faUsers,
@@ -43,12 +50,16 @@ import {
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
library.add( library.add(
faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleQuestion, faCircleQuestion,
faCircleUser, faCircleUser,
faClock,
faCoins,
faComment,
faCopy, faCopy,
faEllipsisVertical, faEllipsisVertical,
faEye, faEye,
@@ -56,6 +67,7 @@ library.add(
faFileLines, faFileLines,
faFloppyDisk, faFloppyDisk,
faFolderOpen, faFolderOpen,
faGift,
faHand, faHand,
faHouseChimney, faHouseChimney,
faLongArrowAltLeft, faLongArrowAltLeft,
@@ -69,10 +81,12 @@ library.add(
faRotate, faRotate,
faShareNodes, faShareNodes,
faSpinner, faSpinner,
faSquareCaretDown,
faSquareCaretUp,
faTrashCan, faTrashCan,
faUser, faUser,
faUsers, faUsers,
faXmark faXmark,
); );
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";

View File

@@ -7,7 +7,7 @@ if (process.env.NODE_ENV === "production") {
ready() { ready() {
console.log( console.log(
"App is being served from cache by a service worker.\n" + "App is being served from cache by a service worker.\n" +
"For more details, visit https://goo.gl/AFskqB" "For more details, visit https://goo.gl/AFskqB",
); );
}, },
registered() { registered() {
@@ -24,7 +24,7 @@ if (process.env.NODE_ENV === "production") {
}, },
offline() { offline() {
console.log( console.log(
"No internet connection found. App is running in offline mode." "No internet connection found. App is running in offline mode.",
); );
}, },
error(error) { error(error) {

View File

@@ -1,33 +1,36 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { accountsDB } from "@/db"; import { accountsDB } from "@/db";
/**
*
* @param to :RouteLocationNormalized
* @param from :RouteLocationNormalized
* @param next :NavigationGuardNext
*/
const enterOrStart = async (to, from, next) => {
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
};
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
path: "/", path: "/",
name: "home", name: "home",
component: () => component: () =>
import(/* webpackChunkName: "start" */ "../views/DiscoverView.vue"), import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
beforeEnter: async (to, from, next) => { beforeEnter: enterOrStart,
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
},
},
{
path: "/about",
name: "about",
component: () =>
import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
}, },
{ {
path: "/account", path: "/account",
name: "account", name: "account",
component: () => component: () =>
import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"), import(/* webpackChunkName: "account" */ "../views/AccountViewView.vue"),
beforeEnter: enterOrStart,
}, },
{ {
path: "/confirm-contact", path: "/confirm-contact",
@@ -58,6 +61,7 @@ const routes: Array<RouteRecordRaw> = [
name: "contacts", name: "contacts",
component: () => component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"), import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
beforeEnter: enterOrStart,
}, },
{ {
path: "/scan-contact", path: "/scan-contact",
@@ -111,6 +115,14 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue" /* webpackChunkName: "new-edit-project" */ "../views/NewEditProjectView.vue"
), ),
}, },
{
path: "/new-identifier",
name: "new-identifier",
component: () =>
import(
/* webpackChunkName: "new-identifier" */ "../views/NewIdentifierView.vue"
),
},
{ {
path: "/project", path: "/project",
name: "project", name: "project",
@@ -122,6 +134,7 @@ const routes: Array<RouteRecordRaw> = [
name: "projects", name: "projects",
component: () => component: () =>
import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"), import(/* webpackChunkName: "projects" */ "../views/ProjectsView.vue"),
beforeEnter: enterOrStart,
}, },
{ {
path: "/seed-backup", path: "/seed-backup",

View File

@@ -1,14 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
@Options({
components: {},
})
export default class AboutView extends Vue {}
</script>

View File

@@ -1,52 +1,5 @@
<template> <template>
<!-- QUICK NAV --> <QuickNav selected="Profile"></QuickNav>
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
@@ -66,20 +19,22 @@
</span> </span>
</div> </div>
<!-- Friend referral requirement notice --> <!-- Registration notice -->
<!-- We won't show any loading indicator; we'll just pop the message in once we know they need it. -->
<div <div
v-if="!loadingLimits && !limits?.nextWeekBeginDateTime"
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4" class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mb-4"
> >
<p class="mb-4"> <p class="mb-4">
<b>Important:</b> before you can create a new project or commit time to <b>Note:</b> Before you can publicly announce a new project or time
one, you need a friend to register you. commitment, a friend needs to register you.
</p> </p>
<button <router-link
id="btnShowQR" :to="{ name: 'contact-qr' }"
class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md" class="inline-block text-md uppercase bg-amber-600 text-white px-4 py-2 rounded-md"
> >
Share Your ID Share Your Info
</button> </router-link>
</div> </div>
<!-- Identity Details --> <!-- Identity Details -->
@@ -181,7 +136,7 @@
<div class="text-slate-500 text-center"> <div class="text-slate-500 text-center">
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code> <b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
</div> </div>
<img src="img/sample-qr-code.png" class="w-full mb-3" /> <img src="/img/sample-qr-code.png" class="w-full mb-3" />
<button <button
value="cancel" value="cancel"
@@ -229,6 +184,13 @@
<button class="text-center text-md text-blue-500" @click="checkLimits()"> <button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits Check Limits
</button> </button>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="ml-2">
Checking... <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="ml-2">
{{ limitsMessage }}
</div>
<div v-if="!!limits?.nextWeekBeginDateTime" class="px-9"> <div v-if="!!limits?.nextWeekBeginDateTime" class="px-9">
<span class="font-bold">Rate Limits</span> <span class="font-bold">Rate Limits</span>
<p> <p>
@@ -254,10 +216,10 @@
/> />
<button <button
v-if="apiServerInput != apiServer" v-if="apiServerInput != apiServer"
class="px-4 rounded bg-slate-200 border border-slate-400" class="px-4 rounded bg-red-500 border border-slate-400"
@click="onClickSaveApiServer()" @click="onClickSaveApiServer()"
> >
<fa icon="floppy-disk" class="fa-fw"></fa> <fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button> </button>
<button <button
class="px-4 rounded bg-slate-200 border border-slate-400" class="px-4 rounded bg-slate-200 border border-slate-400"
@@ -280,7 +242,12 @@
</div> </div>
<div v-if="numAccounts > 0" class="flex py-2"> <div v-if="numAccounts > 0" class="flex py-2">
Switch Account Switch Identifier
<span>
<button class="text-blue-500 px-2" @click="switchAccount(0)">
None
</button>
</span>
<span v-for="accountNum in numAccounts" :key="accountNum"> <span v-for="accountNum in numAccounts" :key="accountNum">
<button class="text-blue-500 px-2" @click="switchAccount(accountNum)"> <button class="text-blue-500 px-2" @click="switchAccount(accountNum)">
#{{ accountNum }} #{{ accountNum }}
@@ -289,47 +256,35 @@
</div> </div>
<div> <div>
<button class="text-blue-500 px-2"> <button class="text-blue-500">
<router-link <router-link
:to="{ name: 'statistics' }" :to="{ name: 'statistics' }"
class="block text-center py-3 px-1" class="block text-center py-3"
> >
See Achievements & Statistics See Achievements & Statistics
</router-link> </router-link>
</button> </button>
</div> </div>
<AlertMessage
<!-- This same popup code is in many files. --> :alertTitle="alertTitle"
<div v-bind:class="computedAlertClassNames()"> :alertMessage="alertMessage"
<button ></AlertMessage>
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import "dexie-export-import"; import "dexie-export-import";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db"; import { db, accountsDB } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { import { accessToken } from "@/libs/crypto";
accessToken,
deriveAddress,
generateSeed,
newIdentifier,
} from "@/libs/crypto";
import { AxiosError } from "axios/index"; import { AxiosError } from "axios/index";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
@@ -343,7 +298,7 @@ interface RateLimits {
nextWeekBeginDateTime: string; nextWeekBeginDateTime: string;
} }
@Component @Component({ components: { AlertMessage, QuickNav } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
Constants = AppString; Constants = AppString;
@@ -357,13 +312,41 @@ export default class AccountViewView extends Vue {
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
limits: RateLimits | null = null; limits: RateLimits | null = null;
limitsMessage = "";
loadingLimits = true; // might as well now that we do it on mount, to avoid flashing the registration message
showContactGives = false; showContactGives = false;
private accounts: AccountsSchema;
showDidCopy = false; showDidCopy = false;
showDerCopy = false; showDerCopy = false;
showB64Copy = false; showB64Copy = false;
showPubCopy = false; showPubCopy = false;
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds // call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text, fn) { doCopyTwoSecRedo(text, fn) {
fn(); fn();
@@ -381,13 +364,19 @@ export default class AccountViewView extends Vue {
return timeStr.substring(0, timeStr.indexOf("T")); return timeStr.substring(0, timeStr.indexOf("T"));
} }
// 'created' hook runs when the Vue instance is first created async beforeCreate() {
accountsDB.open();
this.accounts = accountsDB.accounts;
this.numAccounts = await this.accounts.count();
}
async created() { async created() {
// Uncomment to register this user on the test server. // Uncomment this to register this user on the test server.
// To manage within the vue devtools browser extension https://devtools.vuejs.org/ // To manage within the vue devtools browser extension https://devtools.vuejs.org/
// assign this to a class variable, eg. "registerThisUser = testServerRegisterUser", // assign this to a class variable, eg. "registerThisUser = testServerRegisterUser",
// select a component in the extension, and enter in the console: $vm.ctx.registerThisUser() // select a component in the extension, and enter in the console: $vm.ctx.registerThisUser()
//testServerRegisterUser(); //testServerRegisterUser();
try { try {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
@@ -398,35 +387,8 @@ export default class AccountViewView extends Vue {
this.lastName = settings?.lastName || ""; this.lastName = settings?.lastName || "";
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
await accountsDB.open(); const identity = await this.getIdentity(this.activeDid);
this.numAccounts = await accountsDB.accounts.count();
if (this.numAccounts === 0) {
let address = ""; // 0x... ETH address, without "did:eth:"
let privateHex = "";
const mnemonic = generateSeed();
[address, privateHex, this.publicHex, this.derivationPath] =
deriveAddress(mnemonic);
const newId = newIdentifier(
address,
this.publicHex,
privateHex,
this.derivationPath
);
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: this.derivationPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
this.activeDid = newId.did;
}
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.publicHex = identity.keys[0].publicKeyHex; this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
this.derivationPath = identity.keys[0].meta.derivationPath; this.derivationPath = identity.keys[0].meta.derivationPath;
@@ -434,12 +396,23 @@ export default class AccountViewView extends Vue {
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did, activeDid: identity.did,
}); });
this.checkLimits();
} catch (err) { } catch (err) {
this.alertMessage = if (
"Clear your cache and start over (after data backup). See console log for more info."; err.message ===
console.error("Telling user to clear cache because:", err); "Attempted to load account records with no identity available."
this.alertTitle = "Error Creating Account"; ) {
this.isAlertVisible = true; this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
this.alertMessage =
"Clear your cache and start over (after data backup).";
console.error(
"Telling user to clear cache at page create because:",
err,
);
this.alertTitle = "Error Creating Account";
}
} }
} }
@@ -451,10 +424,12 @@ export default class AccountViewView extends Vue {
}); });
} catch (err) { } catch (err) {
this.alertMessage = this.alertMessage =
"Clear your cache and start over (after data backup). See console log for more info."; "Clear your cache and start over (after data backup).";
console.error("Telling user to clear cache because:", err); console.error(
this.alertTitle = "Error Creating Account"; "Telling user to clear cache after contact setting update because:",
this.isAlertVisible = true; err,
);
this.alertTitle = "Error Updating Contact Setting";
} }
} }
@@ -472,64 +447,73 @@ export default class AccountViewView extends Vue {
this.alertTitle = "Download Started"; this.alertTitle = "Download Started";
this.alertMessage = "See your downloads directory for the backup."; this.alertMessage = "See your downloads directory for the backup.";
this.isAlertVisible = true;
} catch (error) { } catch (error) {
this.alertTitle = "Export Error"; this.alertTitle = "Export Error";
this.alertMessage = "See console logs for more info."; this.alertMessage = "See console logs for more info.";
this.isAlertVisible = true;
console.error("Export Error:", error); console.error("Export Error:", error);
} }
} }
async checkLimits() { async checkLimits() {
const url = this.apiServer + "/api/report/rateLimits"; this.loadingLimits = true;
await accountsDB.open(); this.limitsMessage = "";
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const url = this.apiServer + "/api/report/rateLimits";
const identity = await this.getIdentity(this.activeDid);
const headers = await this.getHeaders(identity);
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
// axios throws an exception on a 400 // axios throws an exception on a 400
if (resp.status === 200) { if (resp.status === 200) {
this.limits = resp.data; this.limits = resp.data;
} }
} catch (error: unknown) { } catch (error: unknown) {
const serverError = error as AxiosError; if (
error.message ===
this.alertTitle = "Error from Server"; "Attempted to load Give records with no identity available."
console.error("Bad response retrieving limits: ", serverError); ) {
// Anybody know how to access items inside "response.data" without this? this.limitsMessage = "No identity.";
// eslint-disable-next-line @typescript-eslint/no-explicit-any this.loadingLimits = false;
const data: any = serverError.response?.data;
if (data.error.message) {
this.alertMessage = data.error.message;
} else { } else {
this.alertMessage = "Bad server response. See logs for details."; const serverError = error as AxiosError;
console.error("Bad response retrieving limits: ", serverError);
// Anybody know how to access items inside "response.data" without this?
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = serverError.response?.data;
this.limitsMessage = data?.error?.message || "Bad server response.";
} }
this.isAlertVisible = true;
} }
this.loadingLimits = false;
} }
async switchAccount(accountNum: number) { async switchAccount(accountNum: number) {
await accountsDB.open(); // 0 means none
const accounts = await accountsDB.accounts.toArray(); if (accountNum === 0) {
const account = accounts[accountNum - 1]; await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: undefined,
});
this.activeDid = "";
this.derivationPath = "";
this.publicHex = "";
this.publicBase64 = "";
} else {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = accounts[accountNum - 1];
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: account.did, activeDid: account.did,
}); });
this.activeDid = account.did; this.activeDid = account.did;
this.derivationPath = account.derivationPath; this.derivationPath = account.derivationPath;
this.publicHex = account.publicKeyHex; this.publicHex = account.publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
}
} }
public showContactGivesClassNames() { public showContactGivesClassNames() {
@@ -554,27 +538,5 @@ export default class AccountViewView extends Vue {
// This same popup code is in many files. // This same popup code is in many files.
alertMessage = ""; alertMessage = "";
alertTitle = ""; alertTitle = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
} }
</script> </script>

View File

@@ -48,9 +48,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
@Options({ @Component({
components: {}, components: {},
}) })
export default class ConfirmContactView extends Vue {} export default class ConfirmContactView extends Vue {}

View File

@@ -109,26 +109,19 @@
</div> </div>
</div> </div>
</div> </div>
<AlertMessage
<!-- This same popup code is in many files. --> :alertTitle="alertTitle"
<div v-bind:class="computedAlertClassNames()"> :alertMessage="alertMessage"
<button ></AlertMessage>
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import * as R from "ramda"; import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
@@ -140,40 +133,75 @@ import {
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import AlertMessage from "@/components/AlertMessage";
@Options({}) @Component({ components: { AlertMessage } })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contact: Contact | null = null; contact: Contact | null = null;
giveRecords: Array<GiveServerRecord> = []; giveRecords: Array<GiveServerRecord> = [];
alertTitle = "";
alertMessage = "";
accounts: AccountsSchema;
numAccounts = 0;
async beforeCreate() {
accountsDB.open();
this.accounts = accountsDB.accounts;
this.numAccounts = await this.accounts.count();
}
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
// 'created' hook runs when the Vue instance is first created
async created() { async created() {
await db.open(); try {
const contactDid = this.$route.query.contactDid as string; await db.open();
this.contact = (await db.contacts.get(contactDid)) || null; const contactDid = this.$route.query.contactDid as string;
this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
if (this.activeDid && this.contact) { if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact); this.loadGives(this.activeDid, this.contact);
}
} catch (err) {
this.alertTitle = "Error";
this.alertMessage =
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.";
} }
} }
async loadGives(activeDid: string, contact: Contact) { async loadGives(activeDid: string, contact: Contact) {
// only load the private keys temporarily when needed
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
// load all the time I have given to them
try { try {
const identity = await this.getIdentity(this.activeDid);
let result = []; let result = [];
const url = const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
@@ -192,12 +220,11 @@ export default class ContactsView extends Vue {
console.error( console.error(
"Got bad response status & data of", "Got bad response status & data of",
resp.status, resp.status,
resp.data resp.data,
); );
this.alertTitle = "Error With Server"; this.alertTitle = "Error With Server";
this.alertMessage = this.alertMessage =
"Got an error retrieving your given time from the server."; "Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
} }
const url2 = const url2 =
@@ -218,24 +245,22 @@ export default class ContactsView extends Vue {
console.error( console.error(
"Got bad response status & data of", "Got bad response status & data of",
resp2.status, resp2.status,
resp2.data resp2.data,
); );
this.alertTitle = "Error With Server"; this.alertTitle = "Error With Server";
this.alertMessage = this.alertMessage =
"Got an error retrieving your given time from the server."; "Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
} }
const sortedResult: Array<GiveServerRecord> = R.sort( const sortedResult: Array<GiveServerRecord> = R.sort(
(a, b) => (a, b) =>
new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(), new Date(b.issuedAt).getTime() - new Date(a.issuedAt).getTime(),
result result,
); );
this.giveRecords = sortedResult; this.giveRecords = sortedResult;
} catch (error) { } catch (error) {
this.alertTitle = "Error With Server"; this.alertTitle = "Error With Server";
this.alertMessage = error as string; this.alertMessage = error as string;
this.isAlertVisible = true;
} }
} }
@@ -264,10 +289,7 @@ export default class ContactsView extends Vue {
}; };
// Create a signature using private key of identity // Create a signature using private key of identity
await accountsDB.open(); const identity = await this.getIdentity(this.activeDid);
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
if (identity.keys[0].privateKeyHex !== null) { if (identity.keys[0].privateKeyHex !== null) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const privateKeyHex: string = identity.keys[0].privateKeyHex!; const privateKeyHex: string = identity.keys[0].privateKeyHex!;
@@ -310,7 +332,6 @@ export default class ContactsView extends Vue {
// Now set that error for the user to see. // Now set that error for the user to see.
this.alertTitle = "Error With Server"; this.alertTitle = "Error With Server";
this.alertMessage = userMessage; this.alertMessage = userMessage;
this.isAlertVisible = true;
} }
} }
} }
@@ -318,33 +339,6 @@ export default class ContactsView extends Vue {
cannotConfirmMessage() { cannotConfirmMessage() {
this.alertTitle = "Not Allowed"; this.alertTitle = "Not Allowed";
this.alertMessage = "Only the recipient can confirm final receipt."; this.alertMessage = "Only the recipient can confirm final receipt.";
this.isAlertVisible = true;
}
// This same popup code is in many files.
alertTitle = "";
alertMessage = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
} }
} }
</script> </script>

View File

@@ -1,57 +1,10 @@
<template> <template>
<!-- QUICK NAV --> <QuickNav selected="Profile"></QuickNav>
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Contact Info Your Contact Info
</h1> </h1>
<!-- <!--
@@ -64,17 +17,10 @@
:dotsOptions="{ type: 'square' }" :dotsOptions="{ type: 'square' }"
class="flex justify-center" class="flex justify-center"
/> />
<AlertMessage
<!-- This same popup code is in many files. --> :alertTitle="alertTitle"
<div v-bind:class="computedAlertClassNames()"> :alertMessage="alertMessage"
<button ></AlertMessage>
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<p>{{ alertMessage }}</p>
</div>
</section> </section>
</template> </template>
@@ -86,6 +32,8 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import * as R from "ramda"; import * as R from "ramda";
import { SimpleSigner } from "@/libs/crypto"; import { SimpleSigner } from "@/libs/crypto";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
@@ -93,6 +41,8 @@ const Buffer = require("buffer/").Buffer;
@Component({ @Component({
components: { components: {
QRCodeVue3, QRCodeVue3,
AlertMessage,
QuickNav,
}, },
}) })
export default class ContactQRScanShow extends Vue { export default class ContactQRScanShow extends Vue {
@@ -100,7 +50,29 @@ export default class ContactQRScanShow extends Vue {
apiServer = ""; apiServer = "";
qrValue = ""; qrValue = "";
// 'created' hook runs when the Vue instance is first created public async getIdentity(activeDid) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async created() { async created() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
@@ -113,8 +85,7 @@ export default class ContactQRScanShow extends Vue {
if (!account) { if (!account) {
this.alertMessage = "You have no identity yet."; this.alertMessage = "You have no identity yet.";
} else { } else {
const identity = JSON.parse(account.identity); const identity = await this.getIdentity(this.activeDid);
const publicKeyHex = identity.keys[0].publicKeyHex; const publicKeyHex = identity.keys[0].publicKeyHex;
const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64"); const publicEncKey = Buffer.from(publicKeyHex, "hex").toString("base64");
const contactInfo = { const contactInfo = {
@@ -141,25 +112,7 @@ export default class ContactQRScanShow extends Vue {
} }
// This same popup code is in many files. // This same popup code is in many files.
alertTitle = "";
alertMessage = ""; alertMessage = "";
public onClickClose() {
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.alertMessage,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
} }
</script> </script>

View File

@@ -87,9 +87,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
@Options({ @Component({
components: {}, components: {},
}) })
export default class ContactScanView extends Vue {} export default class ContactScanView extends Vue {}

View File

@@ -1,48 +1,5 @@
<template> <template>
<!-- QUICK NAV --> <QuickNav selected="Contacts"></QuickNav>
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="users" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
@@ -113,7 +70,7 @@
</div> </div>
<!-- Results List --> <!-- Results List -->
<ul class=""> <ul v-if="contacts.length > 0">
<li <li
class="border-b border-slate-300" class="border-b border-slate-300"
v-for="contact in contacts" v-for="contact in contacts"
@@ -230,19 +187,12 @@
</div> </div>
</li> </li>
</ul> </ul>
<p v-else>This identity has no contacts.</p>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
<!-- This same popup code is in many files. -->
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -250,24 +200,24 @@ import { AxiosError } from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Options, Vue } from "vue-class-component";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { import {
GiveServerRecord,
GiveVerifiableCredential, GiveVerifiableCredential,
RegisterVerifiableCredential, RegisterVerifiableCredential,
SERVICE_ID, SERVICE_ID,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Component, Vue } from "vue-facing-decorator";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
@Options({ @Component({
components: {}, components: { AlertMessage, QuickNav },
}) })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
activeDid = ""; activeDid = "";
@@ -291,8 +241,9 @@ export default class ContactsView extends Vue {
showGiveNumbers = false; showGiveNumbers = false;
showGiveTotals = true; showGiveTotals = true;
showGiveConfirmed = true; showGiveConfirmed = true;
alertTitle = "";
alertMessage = "";
// 'created' hook runs when the Vue instance is first created
async created() { async created() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
@@ -306,130 +257,119 @@ export default class ContactsView extends Vue {
const allContacts = await db.contacts.toArray(); const allContacts = await db.contacts.toArray();
this.contacts = R.sort( this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""), (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts allContacts,
); );
} }
async loadGives() { public async getIdentity(activeDid) {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts); const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
console.error( throw new Error(
"Attempted to load Give records with no identity available." "Attempted to load Give records with no identity available.",
); );
return;
} }
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
public async getHeadersAndIdentity(activeDid) {
const identity = await this.getIdentity(activeDid);
const headers = await this.getHeaders(identity);
return { headers, identity };
}
async loadGives() {
const handleResponse = (resp, descriptions, confirmed, unconfirmed) => {
if (resp.status === 200) {
const allData = resp.data.data;
for (const give of allData) {
if (give.unit === "HUR") {
if (give.amountConfirmed) {
const prevAmount = confirmed[give.agentDid] || 0;
confirmed[give.agentDid] = prevAmount + give.amount;
} else {
const prevAmount = unconfirmed[give.agentDid] || 0;
unconfirmed[give.agentDid] = prevAmount + give.amount;
}
if (!descriptions[give.agentDid] && give.description) {
descriptions[give.agentDid] = give.description;
}
}
}
} else {
console.error(
"Got bad response status & data of",
resp.status,
resp.data,
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your " +
resp.config.url.includes("recipientDid")
? "received"
: "given" + " time from the server.";
}
};
// load all the time I have given
try { try {
const url = const { headers, identity } = await this.getHeadersAndIdentity(
this.activeDid,
);
const givenByUrl =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did); encodeURIComponent(identity.did);
const token = await accessToken(identity); const givenToUrl =
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const resp = await this.axios.get(url, { headers });
//console.log("All gifts you've given:", resp.data);
if (resp.status === 200) {
const contactDescriptions: Record<string, string> = {};
const contactConfirmed: Record<string, number> = {};
const contactUnconfirmed: Record<string, number> = {};
const allData: Array<GiveServerRecord> = resp.data.data;
for (const give of allData) {
if (give.unit == "HUR") {
const recipDid: string = give.recipientDid;
if (give.amountConfirmed) {
const prevAmount = contactConfirmed[recipDid] || 0;
contactConfirmed[recipDid] = prevAmount + give.amount;
} else {
const prevAmount = contactUnconfirmed[recipDid] || 0;
contactUnconfirmed[recipDid] = prevAmount + give.amount;
}
if (!contactDescriptions[recipDid] && give.description) {
// Since many make the tooltip too big, we'll just use the latest.
contactDescriptions[recipDid] = give.description;
}
}
}
//console.log("Done retrieving gives", contactConfirmed);
this.givenByMeDescriptions = contactDescriptions;
this.givenByMeConfirmed = contactConfirmed;
this.givenByMeUnconfirmed = contactUnconfirmed;
} else {
console.error(
"Got bad response status & data of",
resp.status,
resp.data
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
} catch (error) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
this.isAlertVisible = true;
}
// load all the time I have received
try {
const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?recipientDid=" + "/api/v2/report/gives?recipientDid=" +
encodeURIComponent(identity.did); encodeURIComponent(identity.did);
const token = await accessToken(identity);
const headers = { const [givenByMeResp, givenToMeResp] = await Promise.all([
"Content-Type": "application/json", this.axios.get(givenByUrl, { headers }),
Authorization: "Bearer " + token, this.axios.get(givenToUrl, { headers }),
}; ]);
const resp = await this.axios.get(url, { headers });
//console.log("All gifts you've recieved:", resp.data); const givenByMeDescriptions = {};
if (resp.status === 200) { const givenByMeConfirmed = {};
const contactDescriptions: Record<string, string> = {}; const givenByMeUnconfirmed = {};
const contactConfirmed: Record<string, number> = {}; handleResponse(
const contactUnconfirmed: Record<string, number> = {}; givenByMeResp,
const allData: Array<GiveServerRecord> = resp.data.data; givenByMeDescriptions,
for (const give of allData) { givenByMeConfirmed,
if (give.unit == "HUR") { givenByMeUnconfirmed,
if (give.amountConfirmed) { );
const prevAmount = contactConfirmed[give.agentDid] || 0; this.givenByMeDescriptions = givenByMeDescriptions;
contactConfirmed[give.agentDid] = prevAmount + give.amount; this.givenByMeConfirmed = givenByMeConfirmed;
} else { this.givenByMeUnconfirmed = givenByMeUnconfirmed;
const prevAmount = contactUnconfirmed[give.agentDid] || 0;
contactUnconfirmed[give.agentDid] = prevAmount + give.amount; const givenToMeDescriptions = {};
} const givenToMeConfirmed = {};
if (!contactDescriptions[give.agentDid] && give.description) { const givenToMeUnconfirmed = {};
// Since many make the tooltip too big, we'll just use the latest. handleResponse(
contactDescriptions[give.agentDid] = give.description; givenToMeResp,
} givenToMeDescriptions,
} givenToMeConfirmed,
} givenToMeUnconfirmed,
//console.log("Done retrieving receipts", contactConfirmed); );
this.givenToMeDescriptions = contactDescriptions; this.givenToMeDescriptions = givenToMeDescriptions;
this.givenToMeConfirmed = contactConfirmed; this.givenToMeConfirmed = givenToMeConfirmed;
this.givenToMeUnconfirmed = contactUnconfirmed; this.givenToMeUnconfirmed = givenToMeUnconfirmed;
} else {
console.error(
"Got bad response status & data of",
resp.status,
resp.data
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your received time from the server.";
this.isAlertVisible = true;
}
} catch (error) { } catch (error) {
this.alertTitle = "Error With Server"; this.alertTitle = "Error With Server";
this.alertMessage = error as string; this.alertMessage = error as string;
this.isAlertVisible = true;
} }
} }
@@ -456,7 +396,7 @@ export default class ContactsView extends Vue {
const allContacts = this.contacts.concat([newContact]); const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort( this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""), (a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts allContacts,
); );
} }
@@ -467,7 +407,7 @@ export default class ContactsView extends Vue {
this.nameForDid(this.contacts, contact.did) + this.nameForDid(this.contacts, contact.did) +
" with DID " + " with DID " +
contact.did + contact.did +
" ?" " ?",
) )
) { ) {
await db.open(); await db.open();
@@ -481,21 +421,17 @@ export default class ContactsView extends Vue {
confirm( confirm(
"Are you sure you want to use one of your registrations for " + "Are you sure you want to use one of your registrations for " +
this.nameForDid(this.contacts, contact.did) + this.nameForDid(this.contacts, contact.did) +
"?" "?",
) )
) { ) {
await accountsDB.open(); const identity = await this.getIdentity(this.activeDid);
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
// Make a claim
const vcClaim: RegisterVerifiableCredential = { const vcClaim: RegisterVerifiableCredential = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "RegisterAction", "@type": "RegisterAction",
agent: { identifier: identity.did }, agent: { identifier: identity.did },
object: SERVICE_ID, object: SERVICE_ID,
recipient: { identifier: contact.did }, participant: { identifier: contact.did },
}; };
// Make a payload for the claim // Make a payload for the claim
const vcPayload = { const vcPayload = {
@@ -530,13 +466,19 @@ export default class ContactsView extends Vue {
try { try {
const resp = await this.axios.post(url, payload, { headers }); const resp = await this.axios.post(url, payload, { headers });
//console.log("Got resp data:", resp.data); //console.log("Got resp data:", resp.data);
if (resp.data?.success?.handleId) { if (resp.data?.success?.embeddedRecordError) {
this.alertTitle = "Registration Still Unknown";
let message = "There was some problem with the registration.";
if (typeof resp.data.success.embeddedRecordError == "string") {
message += " " + resp.data.success.embeddedRecordError;
}
this.alertMessage = message;
} else if (resp.data?.success?.handleId) {
contact.registered = true; contact.registered = true;
db.contacts.update(contact.did, { registered: true }); db.contacts.update(contact.did, { registered: true });
this.alertTitle = "Registration Success"; this.alertTitle = "Registration Success";
this.alertMessage = contact.name + " has been registered."; this.alertMessage = contact.name + " has been registered.";
this.isAlertVisible = true;
} }
} catch (error) { } catch (error) {
let userMessage = "There was an error. See logs for more info."; let userMessage = "There was an error. See logs for more info.";
@@ -553,7 +495,6 @@ export default class ContactsView extends Vue {
// Now set that error for the user to see. // Now set that error for the user to see.
this.alertTitle = "Error With Server"; this.alertTitle = "Error With Server";
this.alertMessage = userMessage; this.alertMessage = userMessage;
this.isAlertVisible = true;
} }
} }
} }
@@ -564,16 +505,8 @@ export default class ContactsView extends Vue {
this.apiServer + this.apiServer +
"/api/report/" + "/api/report/" +
(visibility ? "canSeeMe" : "cannotSeeMe"); (visibility ? "canSeeMe" : "cannotSeeMe");
await accountsDB.open(); const identity = await this.getIdentity(this.activeDid);
const accounts = await accountsDB.accounts.toArray(); const headers = await this.getHeaders(identity);
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const payload = JSON.stringify({ did: contact.did }); const payload = JSON.stringify({ did: contact.did });
try { try {
@@ -589,12 +522,10 @@ export default class ContactsView extends Vue {
} else { } else {
this.alertMessage = "Bad server response of " + resp.status; this.alertMessage = "Bad server response of " + resp.status;
} }
this.isAlertVisible = true;
} }
} catch (err) { } catch (err) {
this.alertTitle = "Error With Server"; this.alertTitle = "Error With Server";
this.alertMessage = err as string; this.alertMessage = err as string;
this.isAlertVisible = true;
} }
} }
@@ -603,16 +534,6 @@ export default class ContactsView extends Vue {
this.apiServer + this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" + "/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@@ -627,7 +548,6 @@ export default class ContactsView extends Vue {
" can " + " can " +
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity."; "see your activity.";
this.isAlertVisible = true;
} else { } else {
this.alertTitle = "Error With Server"; this.alertTitle = "Error With Server";
if (resp.data.error?.message) { if (resp.data.error?.message) {
@@ -635,12 +555,10 @@ export default class ContactsView extends Vue {
} else { } else {
this.alertMessage = "Bad server response of " + resp.status; this.alertMessage = "Bad server response of " + resp.status;
} }
this.isAlertVisible = true;
} }
} catch (err) { } catch (err) {
this.alertTitle = "Error With Server"; this.alertTitle = "Error With Server";
this.alertMessage = err as string; this.alertMessage = err as string;
this.isAlertVisible = true;
} }
} }
@@ -660,10 +578,7 @@ export default class ContactsView extends Vue {
} }
async onClickAddGive(fromDid: string, toDid: string): Promise<void> { async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
await accountsDB.open(); const identity = await this.getIdentity(this.activeDid);
const accounts = await accountsDB.accounts.toArray();
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
// if they have unconfirmed amounts, ask to confirm those first // if they have unconfirmed amounts, ask to confirm those first
if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) { if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
@@ -672,7 +587,7 @@ export default class ContactsView extends Vue {
"There are " + "There are " +
this.givenToMeUnconfirmed[fromDid] + this.givenToMeUnconfirmed[fromDid] +
" unconfirmed hours from them." + " unconfirmed hours from them." +
" Would you like to confirm some of those hours?" " Would you like to confirm some of those hours?",
) )
) { ) {
this.$router.push({ this.$router.push({
@@ -685,15 +600,12 @@ export default class ContactsView extends Vue {
this.alertTitle = "Input Error"; this.alertTitle = "Input Error";
this.alertMessage = this.alertMessage =
"This is not a valid number of hours: " + this.hourInput; "This is not a valid number of hours: " + this.hourInput;
this.isAlertVisible = true;
} else if (!parseFloat(this.hourInput)) { } else if (!parseFloat(this.hourInput)) {
this.alertTitle = "Input Error"; this.alertTitle = "Input Error";
this.alertMessage = "Giving 0 hours does nothing."; this.alertMessage = "Giving 0 hours does nothing.";
this.isAlertVisible = true;
} else if (!identity) { } else if (!identity) {
this.alertTitle = "Status Error"; this.alertTitle = "Status Error";
this.alertMessage = "No identity is available."; this.alertMessage = "No identity is available.";
this.isAlertVisible = true;
} else { } else {
// ask to confirm amount // ask to confirm amount
let toFrom; let toFrom;
@@ -715,7 +627,7 @@ export default class ContactsView extends Vue {
" hours " + " hours " +
toFrom + toFrom +
description + description +
"?" "?",
) )
) { ) {
this.createAndSubmitGive( this.createAndSubmitGive(
@@ -723,7 +635,7 @@ export default class ContactsView extends Vue {
fromDid, fromDid,
toDid, toDid,
parseFloat(this.hourInput), parseFloat(this.hourInput),
this.hourDescriptionInput this.hourDescriptionInput,
); );
} }
} }
@@ -734,7 +646,7 @@ export default class ContactsView extends Vue {
fromDid: string, fromDid: string,
toDid: string, toDid: string,
amount: number, amount: number,
description: string description: string,
): Promise<void> { ): Promise<void> {
// Make a claim // Make a claim
const vcClaim: GiveVerifiableCredential = { const vcClaim: GiveVerifiableCredential = {
@@ -784,7 +696,7 @@ export default class ContactsView extends Vue {
if (resp.data?.success?.handleId) { if (resp.data?.success?.handleId) {
this.alertTitle = "Done"; this.alertTitle = "Done";
this.alertMessage = "Successfully logged time to the server."; this.alertMessage = "Successfully logged time to the server.";
this.isAlertVisible = true;
if (fromDid === identity.did) { if (fromDid === identity.did) {
const newList = R.clone(this.givenByMeUnconfirmed); const newList = R.clone(this.givenByMeUnconfirmed);
newList[toDid] = (newList[toDid] || 0) + amount; newList[toDid] = (newList[toDid] || 0) + amount;
@@ -810,7 +722,6 @@ export default class ContactsView extends Vue {
// Now set that error for the user to see. // Now set that error for the user to see.
this.alertTitle = "Error With Server"; this.alertTitle = "Error With Server";
this.alertMessage = userMessage; this.alertMessage = userMessage;
this.isAlertVisible = true;
} }
} }
} }
@@ -828,32 +739,6 @@ export default class ContactsView extends Vue {
} }
} }
// This same popup code is in many files.
alertTitle = "";
alertMessage = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
public showGiveAmountsClassNames() { public showGiveAmountsClassNames() {
return { return {
"bg-slate-500": this.showGiveTotals, "bg-slate-500": this.showGiveTotals,

View File

@@ -1,47 +1,5 @@
<template> <template>
<!-- QUICK NAV --> <QuickNav selected="Discover"></QuickNav>
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="users" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
@@ -51,18 +9,20 @@
</h1> </h1>
<!-- Quick Search --> <!-- Quick Search -->
<form id="QuickSearch" class="mb-4 flex"> <div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="search()">
<input <input
type="text" type="text"
v-model="searchTerms"
placeholder="Search…" placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2" class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
/> />
<button <button
@click="search()"
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400" class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
> >
<fa icon="magnifying-glass" class="fa-fw"></fa> <fa icon="magnifying-glass" class="fa-fw"></fa>
</button> </button>
</form> </div>
<!-- Result Tabs --> <!-- Result Tabs -->
<div class="text-center text-slate-500 border-b border-slate-300"> <div class="text-center text-slate-500 border-b border-slate-300">
@@ -70,6 +30,7 @@
<li> <li>
<a <a
href="#" href="#"
@click="searchLocal()"
class="inline-block py-3 rounded-t-lg border-b-2 active text-blue-600 border-blue-600 font-semibold" class="inline-block py-3 rounded-t-lg border-b-2 active text-blue-600 border-blue-600 font-semibold"
> >
Nearby Nearby
@@ -97,7 +58,14 @@
<!-- Results List --> <!-- Results List -->
<ul class=""> <ul class="">
<li class="border-b border-slate-300"> <li class="border-b border-slate-300">
<a href="project-view.html" class="block py-4 flex gap-4"> <a
@click="
onClickLoadProject(
'https://endorser.ch/entity/01H3HER4DTNCR6ZC4VXA3VPWVQ',
)
"
class="block py-4 flex gap-4"
>
<div class="w-12"> <div class="w-12">
<img <img
src="https://picsum.photos/200/200?random=1" src="https://picsum.photos/200/200?random=1"
@@ -115,7 +83,14 @@
</li> </li>
<li class="border-b border-slate-300"> <li class="border-b border-slate-300">
<a href="project-view.html" class="block py-4 flex gap-4"> <a
@click="
onClickLoadProject(
'https://endorser.ch/entity/01H3HER4DTNCR6ZC4VXA3VPWVQ',
)
"
class="block py-4 flex gap-4"
>
<div class="w-12"> <div class="w-12">
<img <img
src="https://picsum.photos/200/200?random=2" src="https://picsum.photos/200/200?random=2"
@@ -133,7 +108,14 @@
</li> </li>
<li class="border-b border-slate-300"> <li class="border-b border-slate-300">
<a href="project-view.html" class="block py-4 flex gap-4"> <a
@click="
onClickLoadProject(
'https://endorser.ch/entity/01H3HER4DTNCR6ZC4VXA3VPWVQ',
)
"
class="block py-4 flex gap-4"
>
<div class="w-12"> <div class="w-12">
<img <img
src="https://picsum.photos/200/200?random=3" src="https://picsum.photos/200/200?random=3"
@@ -151,14 +133,145 @@
</a> </a>
</li> </li>
</ul> </ul>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
@Options({ import { accountsDB, db } from "@/db";
components: {}, import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({
components: { AlertMessage, QuickNav },
}) })
export default class DiscoverView extends Vue {} export default class DiscoverView extends Vue {
activeDid = "";
apiServer = "";
searchTerms = "";
alertMessage = "";
alertTitle = "";
async mounted() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
}
public async buildHeaders() {
const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
public async search() {
const claimContents =
"claimContents=" + encodeURIComponent(this.searchTerms);
const claimType = "claimType=PlanAction";
const queryParams = [claimContents, claimType].join("&");
try {
const response = await fetch(
this.apiServer + "/api/v2/report/claims?" + queryParams,
{
method: "GET",
headers: await this.buildHeaders(),
},
);
if (response.status !== 200) {
const details = await response.text();
throw details;
}
const results = await response.json();
if (results.data) {
console.log("Plans found in that search:", results.data);
} else {
throw JSON.stringify(results);
}
} catch (e) {
console.log("Error with feed load:", e);
this.alertMessage =
e.userMessage || "There was an error retrieving projects.";
this.alertTitle = "Error";
}
}
public async searchLocal() {
const claimContents =
"claimContents=" + encodeURIComponent(this.searchTerms);
const queryParams = [
claimContents,
"minLocLat=40.901000",
"maxLocLat=40.904000",
"westLocLon=-111.914000",
"eastLocLon=-111.909000",
].join("&");
try {
const response = await fetch(
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
{
method: "GET",
headers: await this.buildHeaders(),
},
);
if (response.status !== 200) {
throw await response.text();
}
const results = await response.json();
if (results.data) {
console.log("Plans found in that location:", results.data);
} else {
throw JSON.stringify(results);
}
} catch (e) {
console.log("Error with feed load:", e);
this.alertMessage =
e.userMessage || "There was an error retrieving projects.";
this.alertTitle = "Error";
}
}
/**
* Handle clicking on a project entry found in the list
* @param id of the project
**/
onClickLoadProject(id: string) {
localStorage.setItem("projectId", id);
const route = {
name: "project",
};
this.$router.push(route);
}
}
</script> </script>

View File

@@ -1,52 +1,5 @@
<template> <template>
<!-- QUICK NAV --> <QuickNav selected="Profile"></QuickNav>
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-400">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
@@ -60,14 +13,14 @@
gifts and collaboration. gifts and collaboration.
</p> </p>
<h2 class="text-xl font-semibold">How is this app useful?</h2> <h2 class="text-xl font-semibold">What is the philosophy here?</h2>
<p> <p>
We are building networks of people who want to grow a gifting society. We are building networks of people who want to grow a gifting society.
First of all, you can record ways you've seen people give, and that First of all, you can record ways you've seen people give, and that
leaves a permanent record... one that they show came from you. This is leaves a permanent record -- one that came from you, and the recipient
personally gratifying, but it extends to broader work: volunteers can can prove it was for them. This is personally gratifying, but it extends
get confirmation of activity and selectively show off their to broader work: volunteers can get confirmation of activity and
contributions and network. selectively show off their contributions and network.
</p> </p>
<p> <p>
You can also record projects and plans and invite others to collaborate. You can also record projects and plans and invite others to collaborate.
@@ -83,10 +36,25 @@
the control; this app gives you the control. the control; this app gives you the control.
</p> </p>
<h2 class="text-xl font-semibold">How do I take my first action?</h2>
<p>
You need someone to register you -- usually the person who told you
about this app, on the Contacts
<fa icon="circle-user" class="fa-fw" /> page. After they register you,
and after you have contacts, you can select any contact on the home page
and record your appreciation for... whatever. That is a claim recorded
on a custom ledger. The day after being registered, you'll be able to
register others; later, you can create projects, too.
</p>
<p>
Note that there are limits to how many each person can register, so you
may have to wait.
</p>
<h2 class="text-xl font-semibold">How do I backup all my data?</h2> <h2 class="text-xl font-semibold">How do I backup all my data?</h2>
<p> <p>
There are two parts to backup your data: the identifier secrets and the There are two sets of data to backup: the identifier secrets and the
other data such as settings, contacts, etc. other data that isn't quite a secret such as settings, contacts, etc.
</p> </p>
<div class="px-4"> <div class="px-4">
@@ -100,6 +68,10 @@
<li> <li>
Click on "Backup Identifier Seed" and follow the instructions. Click on "Backup Identifier Seed" and follow the instructions.
</li> </li>
<li>
If you have other identifiers, switch to each one and repeat those
steps.
</li>
</ul> </ul>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
@@ -156,28 +128,6 @@
key. key.
</p> </p>
<h2 class="text-xl font-semibold">
How do I get permission to store claims on the server?
</h2>
<p>
Get registered by someone else with the app; they can register you on
the Contacts <fa icon="circle-user" class="fa-fw" /> page. There are
limits to how many each person can register, so you may have to wait.
</p>
<h2 class="text-xl font-semibold">What do you mean by "claims"?</h2>
<p>
Certain actions in this app are signed by your private keys, and these
are often called "claims". For example, when you give time to a person
or project, you sign a claim declaring that you gave that time. When you
declare a project, you sign a claim declaring it to the world. When you
confirm someone else's claim, you sign a claim of agreement.
</p>
<p>
Some of the data in this app does not involve claims, such as your
contact list and your identifier.
</p>
<h2 class="text-xl font-semibold"> <h2 class="text-xl font-semibold">
I know there is a record from someone, so why can't I see that info? I know there is a record from someone, so why can't I see that info?
</h2> </h2>
@@ -199,8 +149,8 @@
<h2 class="text-xl font-semibold">How do I create another identity?</h2> <h2 class="text-xl font-semibold">How do I create another identity?</h2>
<p> <p>
Go Go
<router-link to="import-account" class="text-blue-500"> <router-link to="start" class="text-blue-500">
import another mnemonic here. create another identity here.
</router-link> </router-link>
</p> </p>
@@ -216,6 +166,14 @@
<p> <p>
{{ package.version }} {{ package.version }}
</p> </p>
<h2 class="text-xl font-semibold">
For any other questions, including remove your data:
</h2>
<p>
Contact us through
<a href="https://communitycred.org">CommunityCred.org</a>.
</p>
</div> </div>
</section> </section>
</template> </template>
@@ -223,8 +181,9 @@
<script lang="ts"> <script lang="ts">
import * as Package from "../../package.json"; import * as Package from "../../package.json";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav";
@Component @Component({ components: { QuickNav } })
export default class Help extends Vue { export default class Help extends Vue {
package = Package; package = Package;
} }

View File

@@ -1,15 +1,357 @@
<template> <template>
<section></section> <QuickNav selected="Home"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Time Safari
</h1>
<div class="mb-8">
<h1 class="text-2xl">Quick Action</h1>
<p>Choose a contact to whom to show appreciation:</p>
<!-- similar contact selection code is in multiple places -->
<div class="px-4">
<button
v-for="contact in allContacts"
:key="contact.did"
@click="openDialog(contact)"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
>
{{ contact.name || "(no name)" }}
</button>
<span v-if="allContacts.length > 0">&nbsp;or&nbsp;</span>
<button @click="openDialog()" class="text-blue-500">
someone not specified
</button>
</div>
</div>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<div>
<h1 class="text-2xl">Latest Activity</h1>
<span :class="{ hidden: isHiddenSpinner }">
<fa icon="spinner" class="fa-spin-pulse"></fa>
Loading&hellip;
</span>
<ul>
<li
class="border-b border-slate-300 py-2"
v-for="record in feedData"
:key="record.jwtId"
>
<div
class="border-b border-dashed border-slate-400 text-orange-400 py-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedId"
>
You've seen all claims below:
</div>
<div class="flex">
<fa
icon="gift"
class="fa-fw flex-none pt-1 pr-2 text-slate-500"
></fa>
<!-- icon values: "coins" = money; "clock" = time; "gift" = others -->
<span class="">{{ this.giveDescription(record) }}</span>
</div>
</li>
</ul>
</div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
import HelloWorld from "@/components/HelloWorld.vue"; // @ is an alias to /src import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive, didInfo } from "@/libs/endorserServer";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Options({ @Component({
components: { components: { GiftedDialog, AlertMessage, QuickNav },
HelloWorld,
},
}) })
export default class HomeView extends Vue {} export default class HomeView extends Vue {
activeDid = "";
allAccounts: Array<Account> = [];
allContacts: Array<Contact> = [];
apiServer = "";
feedAllLoaded = false;
feedData = [];
feedPreviousOldestId = null;
feedLastViewedId = null;
isHiddenSpinner = true;
alertTitle = "";
alertMessage = "";
accounts: AccountsSchema;
numAccounts = 0;
async beforeCreate() {
accountsDB.open();
this.accounts = accountsDB.accounts;
this.numAccounts = await this.accounts.count();
}
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async created() {
try {
await accountsDB.open();
this.allAccounts = await accountsDB.accounts.toArray();
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId;
this.updateAllFeed();
} catch (err) {
this.alertTitle = "Error";
this.alertMessage =
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.";
}
}
public async buildHeaders() {
const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
public async updateAllFeed() {
this.isHiddenSpinner = false;
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId)
.then(async (results) => {
if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data);
this.feedAllLoaded = results.hitLimit;
this.feedPreviousOldestId =
results.data[results.data.length - 1].jwtId;
if (
this.feedLastViewedId == null ||
this.feedLastViewedId < results.data[0].jwtId
) {
await db.open();
db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: results.data[0].jwtId,
});
// but not for this page because we need to remember what it was before
}
}
})
.catch((e) => {
console.log("Error with feed load:", e);
this.alertMessage =
e.userMessage || "There was an error retrieving feed data.";
this.alertTitle = "Error";
});
this.isHiddenSpinner = true;
}
public async retrieveClaims(endorserApiServer, identifier, beforeId) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch(
endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
{
method: "GET",
headers: await this.buildHeaders(),
},
);
if (response.status !== 200) {
throw await response.text();
}
const results = await response.json();
if (results.data) {
return results;
} else {
throw JSON.stringify(results);
}
}
giveDescription(giveRecord) {
let claim = giveRecord.fullClaim;
if (claim.claim) {
claim = claim.claim;
}
// agent.did is for legacy data, before March 2023
const giverDid =
claim.agent?.identifier || claim.agent?.did || giveRecord.issuer;
const giverInfo = didInfo(
giverDid,
this.activeDid,
this.allAccounts,
this.allContacts,
);
const gaveAmount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: claim.description || "something unknown";
// recipient.did is for legacy data, before March 2023
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
const gaveRecipientInfo = gaveRecipientId
? " to " +
didInfo(
gaveRecipientId,
this.activeDid,
this.allAccounts,
this.allContacts,
)
: "";
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
}
displayAmount(code, amt) {
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
}
currencyShortWordForCode(unitCode, single) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
openDialog(giver) {
this.$refs.customDialog.open(giver);
}
handleDialogResult(result) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours);
resolve();
});
} else {
// action was "cancel" so do nothing
}
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
public async recordGive(giverDid, description, hours) {
if (!this.activeDid) {
this.setAlert(
"Error",
"You must select an identity before you can record a give.",
);
return;
}
if (!description && !hours) {
this.setAlert(
"Error",
"You must enter a description or some number of hours.",
);
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
);
if (isGiveCreationError(result)) {
const errorMessage = getGiveCreationErrorMessage(result);
console.log("Error with give result:", result);
this.setAlert(
"Error",
errorMessage || "There was an error recording the give.",
);
} else {
this.setAlert("Success", "That gift was recorded.");
}
} catch (error) {
console.log("Error with give caught:", error);
this.setAlert(
"Error",
getGiveErrorMessage(error) || "There was an error recording the give.",
);
}
}
private setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
}
// Helper functions for readability
isGiveCreationError(result) {
return result.status !== 201 || result.data?.error;
}
getGiveCreationErrorMessage(result) {
return result.data?.error?.message;
}
getGiveErrorMessage(error) {
return error.userMessage || error.response?.data?.error?.message;
}
}
</script> </script>

View File

@@ -43,12 +43,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
import { deriveAddress, newIdentifier } from "../libs/crypto"; import { deriveAddress, newIdentifier } from "../libs/crypto";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Options({ @Component({
components: {}, components: {},
}) })
export default class ImportAccountView extends Vue { export default class ImportAccountView extends Vue {
@@ -72,7 +72,7 @@ export default class ImportAccountView extends Vue {
this.address, this.address,
this.publicHex, this.publicHex,
this.privateHex, this.privateHex,
this.derivationPath this.derivationPath,
); );
try { try {

View File

@@ -49,11 +49,11 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
import { db } from "@/db"; import { db } from "@/db";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Options({ @Component({
components: {}, components: {},
}) })
export default class NewEditAccountView extends Vue { export default class NewEditAccountView extends Vue {

View File

@@ -61,9 +61,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
@Options({ @Component({
components: {}, components: {},
}) })
export default class NewEditCommitmentView extends Vue {} export default class NewEditCommitmentView extends Vue {}

View File

@@ -49,8 +49,9 @@
<span :class="{ hidden: isHiddenSave }">Save Project</span> <span :class="{ hidden: isHiddenSave }">Save Project</span>
<!-- SHOW if in saving state; DISABLE button while in saving state --> <!-- SHOW if in saving state; DISABLE button while in saving state -->
<span :class="{ hidden: isHiddenSpinner }" <span :class="{ hidden: isHiddenSpinner }">
><i class="fa-solid fa-spinner fa-spin-pulse"></i> <!-- icon no worky? -->
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
Saving&hellip;</span Saving&hellip;</span
> >
</button> </button>
@@ -62,32 +63,25 @@
Cancel Cancel
</button> </button>
</div> </div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
<!-- This same popup code is in many files. -->
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as didJwt from "did-jwt"; import * as didJwt from "did-jwt";
import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator";
import { Options, Vue } from "vue-class-component";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { useAppStore } from "@/store/app"; import { useAppStore } from "@/store/app";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import AlertMessage from "@/components/AlertMessage";
interface VerifiableCredential { interface VerifiableCredential {
"@context": string; "@context": string;
@@ -97,8 +91,8 @@ interface VerifiableCredential {
identifier?: string; identifier?: string;
} }
@Options({ @Component({
components: {}, components: { AlertMessage },
}) })
export default class NewEditProjectView extends Vue { export default class NewEditProjectView extends Vue {
activeDid = ""; activeDid = "";
@@ -106,12 +100,44 @@ export default class NewEditProjectView extends Vue {
projectName = ""; projectName = "";
description = ""; description = "";
errorMessage = ""; errorMessage = "";
accounts: AccountsSchema;
numAccounts = 0;
async beforeCreate() {
accountsDB.open();
this.accounts = accountsDB.accounts;
this.numAccounts = await this.accounts.count();
}
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
projectId = localStorage.getItem("projectId") || ""; projectId = localStorage.getItem("projectId") || "";
isHiddenSave = false; isHiddenSave = false;
isHiddenSpinner = true; isHiddenSpinner = true;
// 'created' hook runs when the Vue instance is first created
async created() { async created() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
@@ -119,14 +145,15 @@ export default class NewEditProjectView extends Vue {
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
if (this.projectId) { if (this.projectId) {
await accountsDB.open(); if (this.numAccounts === 0) {
const num_accounts = await accountsDB.accounts.count(); console.error("Error: no account was found.");
if (num_accounts === 0) {
console.error("Problem! Should have a profile!");
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const identity = await this.getIdentity(this.activeDid);
const account = R.find((acc) => acc.did === this.activeDid, accounts); if (!identity) {
const identity = JSON.parse(account?.identity || "undefined"); throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
}
this.LoadProject(identity); this.LoadProject(identity);
} }
} }
@@ -208,7 +235,7 @@ export default class NewEditProjectView extends Vue {
// handleId is new in server v release-1.6.0; remove fullIri when that // handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/ // version shows up here: https://endorser.ch:3000/api-docs/
useAppStore().setProjectId( useAppStore().setProjectId(
resp.data.success.handleId || resp.data.success.fullIri resp.data.success.handleId || resp.data.success.fullIri,
); );
setTimeout( setTimeout(
function (that: Vue) { function (that: Vue) {
@@ -218,17 +245,17 @@ export default class NewEditProjectView extends Vue {
that.$router.push(route); that.$router.push(route);
}, },
2000, 2000,
this this,
); );
} }
} catch (error) { } catch (error) {
let userMessage = "There was an error. See logs for more info."; let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError; const serverError = error as AxiosError;
if (serverError) { if (serverError) {
this.isAlertVisible = true; if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
if (serverError.message) { console.log(serverError);
this.alertTitle = "User Message"; this.alertTitle = "User Message";
userMessage = serverError.message; // This is info for the user. userMessage = serverError.response.data.error.message; // This is info for the user.
this.alertMessage = userMessage; this.alertMessage = userMessage;
} else { } else {
this.alertTitle = "Server Message"; this.alertTitle = "Server Message";
@@ -237,7 +264,7 @@ export default class NewEditProjectView extends Vue {
} else { } else {
console.error( console.error(
"Here's the full error trying to save the claim:", "Here's the full error trying to save the claim:",
error error,
); );
this.alertTitle = "Claim Error"; this.alertTitle = "Claim Error";
this.alertMessage = error as string; this.alertMessage = error as string;
@@ -251,14 +278,11 @@ export default class NewEditProjectView extends Vue {
public async onSaveProjectClick() { public async onSaveProjectClick() {
this.isHiddenSave = true; this.isHiddenSave = true;
this.isHiddenSpinner = false; this.isHiddenSpinner = false;
await accountsDB.open();
const num_accounts = await accountsDB.accounts.count(); if (this.numAccounts === 0) {
if (num_accounts === 0) { console.error("Error: there is no account.");
console.error("Problem! Should have a profile!");
} else { } else {
const accounts = await accountsDB.accounts.toArray(); const identity = await this.getIdentity(this.activeDid);
const account = R.find((acc) => acc.did === this.activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined");
this.SaveProject(identity); this.SaveProject(identity);
} }
} }
@@ -270,27 +294,5 @@ export default class NewEditProjectView extends Vue {
// This same popup code is in many files. // This same popup code is in many files.
alertTitle = ""; alertTitle = "";
alertMessage = ""; alertMessage = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
} }
</script> </script>

View File

@@ -0,0 +1,80 @@
<template>
<QuickNav selected="Profile"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Your Identity
</h1>
<div class="flex justify-center py-12">
<span />
<span v-if="loading">
<span class="text-xl">Creating...&nbsp;</span>
<fa
icon="spinner"
class="fa-spin fa-spin-pulse"
color="green"
size="128"
></fa>
</span>
<span v-else>
<span class="text-xl">Created!</span>
<fa
icon="burst"
class="fa-beat px-12"
color="green"
style="
--fa-animation-duration: 1s;
--fa-animation-direction: reverse;
--fa-animation-iteration-count: 1;
--fa-beat-scale: 6;
"
></fa>
</span>
<span />
</div>
</section>
</template>
<script lang="ts">
import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav";
@Component({ components: { QuickNav } })
export default class AccountViewView extends Vue {
loading = true;
async mounted() {
await accountsDB.open();
const mnemonic = generateSeed();
// address is 0x... ETH address, without "did:eth:"
const [address, privateHex, publicHex, derivationPath] =
deriveAddress(mnemonic);
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
const identity = JSON.stringify(newId);
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: derivationPath,
did: newId.did,
identity: identity,
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
this.loading = false;
setTimeout(() => {
this.$router.push({ name: "account" });
}, 1000);
}
}
</script>

View File

@@ -1,48 +1,5 @@
<template> <template>
<!-- QUICK NAV --> <QuickNav selected="Projects"></QuickNav>
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="hand" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Breadcrumb --> <!-- Breadcrumb -->
@@ -66,7 +23,7 @@
</h1> </h1>
</div> </div>
<div> <div class="text-red-500">
{{ errorMessage }} {{ errorMessage }}
</div> </div>
@@ -108,67 +65,183 @@
</button> </button>
</div> </div>
<!-- Commit --> <div>
<router-link <div v-if="activeDid">
:to="{ name: 'new-edit-commitment' }" <button
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-8" @click="openDialog({ name: 'you', did: activeDid })"
>Make Commitment</router-link class="text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md"
> >
I gave...
<!-- Commitments --> </button>
<div class="bg-slate-100 px-4 py-3 rounded-md"> &horbar; or:
<h3 class="text-sm uppercase font-semibold mb-3">Commitments</h3> </div>
<!-- similar contact selection code is in multiple places -->
<ul class="text-sm border-t border-slate-300"> Record a gift from
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300"> <span v-for="contact in allContacts" :key="contact.did">
<span>[Username]</span> <button @click="openDialog(contact)" class="text-blue-500">
<span &nbsp;{{ contact.name }}</button
>5 hours <fa icon="spinner" class="fa-fw text-slate-400"></fa >,
></span> </span>
</li> <span v-if="allContacts.length > 0">&nbsp;or&nbsp;</span>
<button @click="openDialog()" class="text-blue-500">
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300"> someone not specified
<span>[Username]</span> </button>
<span
>US$ 20.00 <fa icon="circle-check" class="fa-fw text-lime-500"></fa
></span>
</li>
<li class="flex justify-between gap-4 py-1.5 border-b border-slate-300">
<span>[Username]</span>
<span
>0.1 BTC <fa icon="spinner" class="fa-fw text-slate-400"></fa
></span>
</li>
</ul>
</div> </div>
<!-- Gifts to & from this -->
<div class="mt-8 flex justify-around">
<div>
<h1 class="text-xl">Given to this Project</h1>
</div>
<div>
<h1 class="text-xl">... and from this Project</h1>
</div>
</div>
<div class="flex justify-around">
<div class="w-1/2">
<div v-for="give in givesToThis" :key="give.id">
<div class="flex justify-between">
<div class="flex gap-3">
<div class="flex gap-2">
<fa icon="user" class="fa-fw text-slate-400"></fa>
<span>{{
didInfo(give.agentDid, activeDid, accounts, allContacts)
}}</span>
</div>
<div class="flex gap-2" v-if="give.amount">
<fa icon="coins" class="fa-fw text-slate-400"></fa>
<span>{{ give.amount }}</span>
</div>
<div class="flex gap-2" v-if="give.description">
<fa icon="comment" class="fa-fw text-slate-400"></fa>
<span>{{ give.description }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="w-1/2">
<div v-for="give in givesByThis" :key="give.id">
<div class="flex justify-between">
<div class="flex gap-3">
<div class="flex gap-2">
<fa icon="user" class="fa-fw text-slate-400"></fa>
<span>{{
didInfo(give.agentDid, activeDid, accounts, allContacts)
}}</span>
</div>
<div class="flex gap-2" v-if="give.amount">
<fa icon="coins" class="fa-fw text-slate-400"></fa>
<span>{{ give.amount }}</span>
</div>
<div class="flex gap-2">
<fa icon="comment" class="fa-fw text-slate-400"></fa>
<span>{{ give.description }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import * as moment from "moment"; import * as moment from "moment";
import * as R from "ramda"; import { IIdentifier } from "@veramo/core";
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core"; import {
createAndSubmitGive,
didInfo,
GiveServerRecord,
} from "@/libs/endorserServer";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Options({ @Component({
components: {}, components: { GiftedDialog, AlertMessage, QuickNav },
}) })
export default class ProjectViewView extends Vue { export default class ProjectViewView extends Vue {
accounts: AccountsSchema;
activeDid = "";
alertMessage = "";
alertTitle = "";
allContacts: Array<Contact> = [];
apiServer = ""; apiServer = "";
expanded = false;
name = "";
description = ""; description = "";
errorMessage = "";
expanded = false;
givesToThis: Array<GiveServerRecord> = [];
givesByThis: Array<GiveServerRecord> = [];
name = "";
numAccounts = 0;
projectId = localStorage.getItem("projectId") || ""; // handle ID
timeSince = "";
truncatedDesc = ""; truncatedDesc = "";
truncateLength = 40; truncateLength = 40;
timeSince = "";
projectId = localStorage.getItem("projectId") || ""; async beforeCreate() {
errorMessage = ""; accountsDB.open();
this.accounts = accountsDB.accounts;
this.numAccounts = (await this.accounts?.count()) || 0;
}
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.allContacts = await db.contacts.toArray();
this.accounts = accountsDB.accounts;
const accountsArr = await this.accounts?.toArray();
const account = accountsArr.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
this.LoadProject(identity);
}
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load project records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
onEditClick() { onEditClick() {
localStorage.setItem("projectId", this.projectId as string); localStorage.setItem("projectId", this.projectId as string);
@@ -178,6 +251,11 @@ export default class ProjectViewView extends Vue {
this.$router.push(route); this.$router.push(route);
} }
// Isn't there a better way to make this available to the template?
didInfo(did, activeDid, identities, contacts) {
return didInfo(did, activeDid, identities, contacts);
}
expandText() { expandText() {
this.expanded = true; this.expanded = true;
} }
@@ -191,15 +269,19 @@ export default class ProjectViewView extends Vue {
this.apiServer + this.apiServer +
"/api/claim/byHandle/" + "/api/claim/byHandle/" +
encodeURIComponent(this.projectId); encodeURIComponent(this.projectId);
const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token,
}; };
if (identity) {
const token = await accessToken(identity);
headers["Authorization"] = "Bearer " + token;
}
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
if (resp.status === 200) { if (resp.status === 200) {
// feel free to remove this; I haven't yet because it's helpful
console.log("Loaded project: ", resp.data);
const startTime = resp.data.startTime; const startTime = resp.data.startTime;
if (startTime != null) { if (startTime != null) {
const eventDate = new Date(startTime); const eventDate = new Date(startTime);
@@ -224,24 +306,107 @@ export default class ProjectViewView extends Vue {
console.error("Error retrieving project:", error); console.error("Error retrieving project:", error);
} }
} }
const givesInUrl =
this.apiServer +
"/api/v2/report/givesForPlans?planIds=" +
encodeURIComponent(JSON.stringify([this.projectId]));
try {
const resp = await this.axios.get(givesInUrl, { headers });
if (resp.status === 200 && resp.data.data) {
this.givesToThis = resp.data.data;
} else {
this.errorMessage = "Failed to retrieve gives to this project.";
}
} catch (error: unknown) {
console.error("Error retrieving gives to this project:", error);
this.errorMessage =
"Something went wrong retrieving gives to this project.";
}
const givesOutUrl =
this.apiServer +
"/api/v2/report/givesProvidedBy?providerId=" +
encodeURIComponent(this.projectId);
try {
const resp = await this.axios.get(givesOutUrl, { headers });
if (resp.status === 200 && resp.data.data) {
this.givesByThis = resp.data.data;
} else {
this.errorMessage = "Failed to retrieve gives by this project.";
}
} catch (error: unknown) {
console.error("Error retrieving gives by this project:", error);
this.errorMessage = "Something went wrong retrieving gives by project.";
}
} }
// 'created' hook runs when the Vue instance is first created openDialog(contact) {
async created() { this.$refs.customDialog.open(contact);
await db.open(); }
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
await accountsDB.open(); handleDialogResult(result) {
const num_accounts = await accountsDB.accounts.count(); if (result.action === "confirm") {
if (num_accounts === 0) { return new Promise((resolve) => {
console.error("Problem! Should have a profile!"); this.recordGive(result.contact?.did, result.description, result.hours);
resolve();
});
} else { } else {
const accounts = await accountsDB.accounts.toArray(); // action was not "confirm" so do nothing
const account = R.find((acc) => acc.did === activeDid, accounts); }
const identity = JSON.parse(account?.identity || "undefined"); }
this.LoadProject(identity);
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
async recordGive(giverDid, description, hours) {
if (!this.activeDid) {
this.alertTitle = "Error";
this.alertMessage =
"You must select an identity before you can record a give.";
return;
}
if (!description && !hours) {
this.alertTitle = "Error";
this.alertMessage =
"You must enter a description or some number of hours.";
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
this.projectId,
);
if (result.status !== 201 || result.data?.error) {
console.log("Error with give result:", result);
this.alertTitle = "Error";
this.alertMessage =
result.data?.error?.message ||
"There was an error recording the give.";
} else {
this.alertTitle = "Success";
this.alertMessage = "That gift was recorded.";
}
} catch (e) {
console.log("Error with give caught:", e);
this.alertTitle = "Error";
this.alertMessage =
e.userMessage ||
e.response?.data?.error?.message ||
"There was an error recording the give.";
} }
} }
} }

View File

@@ -1,47 +1,5 @@
<template> <template>
<!-- QUICK NAV --> <QuickNav selected="Projects"></QuickNav>
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1"
><fa icon="house-chimney" class="fa-fw"></fa
></router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
><fa icon="magnifying-glass" class="fa-fw"></fa
></router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
><fa icon="folder-open" class="fa-fw"></fa
></router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
><fa icon="users" class="fa-fw"></fa
></router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
><fa icon="circle-user" class="fa-fw"></fa
></router-link>
</li>
</ul>
</nav>
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
@@ -70,52 +28,152 @@
<fa icon="plus" class="fa-fw"></fa> <fa icon="plus" class="fa-fw"></fa>
</button> </button>
<!-- Results List --> <!-- Loading Animation -->
<ul class=""> <div
<li class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
class="border-b border-slate-300" v-if="isLoading"
v-for="project in projects" >
:key="project.handleId" <fa icon="spinner" class="fa-spin-pulse"></fa>
> </div>
<a
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
>
<div class="flex-none w-12">
<img
src="https://picsum.photos/200/200?random=1"
class="w-full rounded"
/>
</div>
<div class="grow overflow-hidden"> <!-- Results List -->
<h2 class="text-base font-semibold">{{ project.name }}</h2> <InfiniteScroll @reached-bottom="loadMoreData">
<div class="text-sm truncate"> <ul>
{{ project.description }} <li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
>
<a
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
>
<div class="flex-none w-12">
<img
src="https://picsum.photos/200/200?random=1"
class="w-full rounded"
/>
</div> </div>
</div>
</a> <div class="grow overflow-hidden">
</li> <h2 class="text-base font-semibold">{{ project.name }}</h2>
</ul> <div class="text-sm truncate">
{{ project.description }}
</div>
</div>
</a>
</li>
</ul>
</InfiniteScroll>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator";
import { Options, Vue } from "vue-class-component";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import InfiniteScroll from "@/components/InfiniteScroll";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Options({ /**
components: {}, * Represents data about a project
**/
interface ProjectData {
/**
* Name of the project
**/
name: string;
/**
* Description of the project
**/
description: string;
/**
* URL referencing information about the project
**/
handleId: string;
/**
* The Identier of the project
**/
rowid: string;
}
@Component({
components: { InfiniteScroll, AlertMessage, QuickNav },
}) })
export default class ProjectsView extends Vue { export default class ProjectsView extends Vue {
apiServer = ""; apiServer = "";
projects: { handleId: string; name: string; description: string }[] = []; projects: ProjectData[] = [];
current: IIdentifier;
isLoading = false;
alertTitle = "";
alertMessage = "";
accounts: AccountsSchema;
numAccounts = 0;
async beforeCreate() {
accountsDB.open();
this.accounts = accountsDB.accounts;
this.numAccounts = await this.accounts.count();
}
/**
* Core project data loader
* @param url the url used to fetch the data
* @param token Authorization token
**/
async dataLoader(url: string, token: string) {
const headers: { [key: string]: string } = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
};
try {
this.isLoading = true;
const resp = await this.axios.get(url, { headers });
if (resp.status === 200 || !resp.data.data) {
const plans: ProjectData[] = resp.data.data;
for (const plan of plans) {
const { name, description, handleId = plan.fullIri, rowid } = plan;
this.projects.push({ name, description, handleId, rowid });
}
} else {
console.log("Bad server response & data:", resp.status, resp.data);
throw Error("Failed to get projects from the server.");
}
} catch (error) {
console.error("Got error loading projects:", error.message);
this.alertTitle = "Error";
this.alertMessage = "Got an error loading projects:" + error.message;
} finally {
this.isLoading = false;
}
}
/**
* Data loader used by infinite scroller
* @param payload is the flag from the InfiniteScroll indicating if it should load
**/
async loadMoreData(payload: boolean) {
if (this.projects.length > 0 && payload) {
const latestProject = this.projects[this.projects.length - 1];
const url = `${this.apiServer}/api/v2/report/plansByIssuer?beforeId=${latestProject.rowid}`;
const token = await accessToken(this.current);
await this.dataLoader(url, token);
}
}
/**
* Handle clicking on a project entry found in the list
* @param id of the project
**/
onClickLoadProject(id: string) { onClickLoadProject(id: string) {
localStorage.setItem("projectId", id); localStorage.setItem("projectId", id);
const route = { const route = {
@@ -124,54 +182,61 @@ export default class ProjectsView extends Vue {
this.$router.push(route); this.$router.push(route);
} }
/**
* Load projects initially
* @param identity of the user
**/
async LoadProjects(identity: IIdentifier) { async LoadProjects(identity: IIdentifier) {
const url = this.apiServer + "/api/v2/report/plansByIssuer"; const url = `${this.apiServer}/api/v2/report/plansByIssuer`;
const token = await accessToken(identity); const token: string = await accessToken(identity);
const headers = { await this.dataLoader(url, token);
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const plans = resp.data.data;
for (let i = 0; i < plans.length; i++) {
const plan = plans[i];
const data = {
name: plan.name,
description: plan.description,
// handleId is new in server v release-1.6.0; remove fullIri when that
// version shows up here: https://endorser.ch:3000/api-docs/
handleId: plan.handleId || plan.fullIri,
};
this.projects.push(data);
}
}
} catch (error) {
console.error("Got error loading projects: ", error);
}
} }
// 'created' hook runs when the Vue instance is first created public async getIdentity(activeDid) {
async created() {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
await accountsDB.open(); await accountsDB.open();
const num_accounts = await accountsDB.accounts.count(); const account = await accountsDB.accounts
if (num_accounts === 0) { .where("did")
console.error("Problem! Should have a profile!"); .equals(activeDid)
} else { .first();
const accounts = await accountsDB.accounts.toArray(); const identity = JSON.parse(account?.identity || "null");
const account = R.find((acc) => acc.did === activeDid, accounts);
const identity = JSON.parse(account?.identity || "undefined"); if (!identity) {
this.LoadProjects(identity); throw new Error(
"Attempted to load project records with no identity available.",
);
}
return identity;
}
/**
* 'created' hook runs when the Vue instance is first created
**/
async created() {
try {
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
const activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
if (this.numAccounts === 0) {
console.error("No accounts found.");
this.alertTitle = "Error";
this.alertMessage = "You need an identity to load your projects.";
} else {
const identity = await this.getIdentity(activeDid);
this.current = identity;
this.LoadProjects(identity);
}
} catch (err) {
console.log("Error initializing:", err);
this.alertTitle = "Error";
this.alertMessage = "Something went wrong loading your projects.";
} }
} }
/**
* Handling clicking on the new project button
**/
onClickNewProject(): void { onClickNewProject(): void {
localStorage.removeItem("projectId"); localStorage.removeItem("projectId");
const route = { const route = {

View File

@@ -1,52 +1,5 @@
<template> <template>
<!-- QUICK NAV --> <QuickNav selected="Profile"></QuickNav>
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
@@ -85,18 +38,10 @@
<p v-if="showSeed">{{ activeAccount.mnemonic }}</p> <p v-if="showSeed">{{ activeAccount.mnemonic }}</p>
</div> </div>
<div v-else>You do not have an active identity.</div> <div v-else>You do not have an active identity.</div>
<AlertMessage
<!-- This same popup code is in many files. --> :alertTitle="alertTitle"
<div v-bind:class="computedAlertClassNames()"> :alertMessage="alertMessage"
<button ></AlertMessage>
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2"
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</section> </section>
</template> </template>
@@ -105,11 +50,15 @@ import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db";
import * as R from "ramda"; import * as R from "ramda";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component @Component({ components: { AlertMessage, QuickNav } })
export default class SeedBackupView extends Vue { export default class SeedBackupView extends Vue {
activeAccount = null; activeAccount = null;
showSeed = false; showSeed = false;
alertMessage = "";
alertTitle = "";
// 'created' hook runs when the Vue instance is first created // 'created' hook runs when the Vue instance is first created
async created() { async created() {
@@ -125,38 +74,11 @@ export default class SeedBackupView extends Vue {
console.error("Got an error loading an identity:", err); console.error("Got an error loading an identity:", err);
this.alertTitle = "Error Loading Account"; this.alertTitle = "Error Loading Account";
this.alertMessage = "Got an error loading your seed data."; this.alertMessage = "Got an error loading your seed data.";
this.isAlertVisible = true;
} }
} }
showSeedPhrase() { showSeedPhrase() {
this.showSeed = true; this.showSeed = true;
} }
// This same popup code is in many files.
alertMessage = "";
alertTitle = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
} }
</script> </script>

View File

@@ -28,14 +28,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Options, Vue } from "vue-class-component"; import { Component, Vue } from "vue-facing-decorator";
@Options({ @Component({
components: {}, components: {},
}) })
export default class StartView extends Vue { export default class StartView extends Vue {
public onClickYes() { public onClickYes() {
this.$router.push({ name: "account" }); this.$router.push({ name: "new-identifier" });
} }
public onClickNo() { public onClickNo() {

View File

@@ -1,52 +1,5 @@
<template> <template>
<!-- QUICK NAV --> <QuickNav selected="Profile"></QuickNav>
<nav id="QuickNav" class="fixed bottom-0 left-0 right-0 bg-slate-200 z-50">
<ul class="flex text-2xl p-2 gap-2">
<!-- Home Feed -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link :to="{ name: 'home' }" class="block text-center py-3 px-1">
<fa icon="house-chimney" class="fa-fw"></fa>
</router-link>
</li>
<!-- Search -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'discover' }"
class="block text-center py-3 px-1"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</router-link>
</li>
<!-- Projects -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'projects' }"
class="block text-center py-3 px-1"
>
<fa icon="folder-open" class="fa-fw"></fa>
</router-link>
</li>
<!-- Contacts -->
<li class="basis-1/5 rounded-md text-slate-500">
<router-link
:to="{ name: 'contacts' }"
class="block text-center py-3 px-1"
>
<fa icon="users" class="fa-fw"></fa>
</router-link>
</li>
<!-- Profile -->
<li class="basis-1/5 rounded-md bg-slate-400 text-white">
<router-link
:to="{ name: 'account' }"
class="block text-center py-3 px-1"
>
<fa icon="circle-user" class="fa-fw"></fa>
</router-link>
</li>
</ul>
</nav>
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24"> <section id="Content" class="p-6 pb-24">
<!-- Heading --> <!-- Heading -->
@@ -61,7 +14,7 @@
<!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. --> <!-- eslint-disable prettier/prettier --><!-- If we format prettier then there is extra space at the start of the line. -->
<li>Each will show at their time of appearance relative to all others.</li> <li>Each will show at their time of appearance relative to all others.</li>
<li>Note that the ones on the left and right edges are randomized <li>Note that the ones on the left and right edges are randomized
because not all their positional data is visible to you. because their data isn't all visible to you.
</li> </li>
<!-- eslint-enable --> <!-- eslint-enable -->
</ul> </ul>
@@ -75,110 +28,52 @@
{{ worldProperties.endTime }} {{ worldProperties.endTime }}
</div> </div>
<div v-if="worldProperties.animationDurationSeconds"> <div v-if="worldProperties.animationDurationSeconds">
<label>Animation duration:&nbsp;</label> <label>Animation Time:&nbsp;</label>
{{ worldProperties.animationDurationSeconds }} seconds {{ worldProperties.animationDurationSeconds }} seconds
</div> </div>
</div> </div>
<button class="float-right" @click="captureGraphics()">Screenshot</button> <button class="float-right" @click="captureGraphics()">Screenshot</button>
<!-- Another place to play with the sizing is in Resizer.setSize -->
<div id="scene-container" class="h-screen"></div> <div id="scene-container" class="h-screen"></div>
<AlertMessage
<div v-bind:class="computedAlertClassNames()"> :alertTitle="alertTitle"
<button :alertMessage="alertMessage"
class="close-button bg-slate-200 w-8 leading-loose rounded-full absolute top-2 right-2" ></AlertMessage>
@click="onClickClose()"
>
<fa icon="xmark"></fa>
</button>
<h4 class="font-bold pr-5">{{ alertTitle }}</h4>
<p>{{ alertMessage }}</p>
</div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js"; import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { World } from "@/components/World/World.js"; import { World } from "@/components/World/World.js";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
interface WorldProperties { interface WorldProperties {
startTime?: string; startTime?: string;
endTime?: string; endTime?: string;
} }
@Component @Component({ components: { AlertMessage, World, QuickNav } })
export default class StatisticsView extends Vue { export default class StatisticsView extends Vue {
world: World; world: World;
worldProperties: WorldProperties = {}; worldProperties: WorldProperties = {};
alertTitle = "";
alertMessage = "";
mounted() { mounted() {
const container = document.querySelector("#scene-container"); try {
const newWorld = new World(container, this); const container = document.querySelector("#scene-container");
newWorld.start(); console.log(container);
this.world = newWorld; const newWorld = new World(container, this);
newWorld.start();
this.world = newWorld;
} catch (err) {
console.log(err);
this.alertTitle = "Mounting error";
this.alertMessage = err.message;
}
} }
public captureGraphics() { public captureGraphics() {
/**
// from https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#examples
// Adds a blank image
const dataBlob = document
.querySelector("#scene-container")
.firstChild.toBlob((blob) => {
const newImg = document.createElement("img");
const url = URL.createObjectURL(blob);
newImg.onload = () => {
// no longer need to read the blob so it's revoked
URL.revokeObjectURL(url);
};
newImg.src = url;
document.body.appendChild(newImg);
});
**/
/**
// Yields a blank page with the iframe below
const dataUrl = document
.querySelector("#scene-container")
.firstChild.toDataURL("image/png");
**/
/**
// Yields a blank page with the iframe below
const dataUrl = this.world.renderer.domElement.toDataURL("image/png");
**/
/**
// Show the image in a new tab
const iframe = `
<iframe
src="${dataUrl}"
frameborder="0"
style="border:0; top:0px; left:0px; bottom:0px; right:0px; width:100%; height:100%;"
allowfullscreen>
</iframe>`;
const win = window.open();
win.document.open();
win.document.write(iframe);
win.document.close();
**/
// from https://stackoverflow.com/a/17407392/845494
// This yields a file with funny formatting.
//const image = const dataUrl.replace("image/png", "image/octet-stream");
/**
// Yields a blank image at the bottom of the page
// from https://discourse.threejs.org/t/save-screenshot-on-server/39900/3
const img = new Image();
img.src = this.world.renderer.domElement.toDataURL();
document.body.appendChild(img);
**/
/** /**
* This yields an SVG that only shows white and black highlights * This yields an SVG that only shows white and black highlights
// from https://stackoverflow.com/questions/27632621/exporting-from-three-js-scene-to-svg-or-other-vector-format // from https://stackoverflow.com/questions/27632621/exporting-from-three-js-scene-to-svg-or-other-vector-format
@@ -186,46 +81,12 @@ export default class StatisticsView extends Vue {
const rendererSVG = new SVGRenderer(); const rendererSVG = new SVGRenderer();
rendererSVG.setSize(window.innerWidth, window.innerHeight); rendererSVG.setSize(window.innerWidth, window.innerHeight);
rendererSVG.render(this.world.scene, this.world.camera); rendererSVG.render(this.world.scene, this.world.camera);
//document.body.appendChild(rendererSVG.domElement);
ExportToSVG(rendererSVG, "test.svg"); ExportToSVG(rendererSVG, "test.svg");
} }
public setWorldProperty(propertyName, propertyValue) { public setWorldProperty(propertyName, propertyValue) {
this.worldProperties[propertyName] = propertyValue; this.worldProperties[propertyName] = propertyValue;
} }
alertTitle = "";
alertMessage = "";
isAlertVisible = false;
public setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
this.isAlertVisible = true;
}
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
fixed: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
} }
function ExportToSVG(rendererSVG, filename) { function ExportToSVG(rendererSVG, filename) {