forked from jsnbuchanan/crowd-funder-for-time-pwa
Compare commits
9 Commits
home-view-
...
openssl
| Author | SHA1 | Date | |
|---|---|---|---|
| 92fcffdfc5 | |||
| 5f5562f5e3 | |||
| 74ed025377 | |||
| f36ecfd8db | |||
| fc70a11bd8 | |||
| 73f890beac | |||
| 67dce9e678 | |||
| 2b66ddfb83 | |||
| 56fc2893a2 |
11
CHANGELOG.md
11
CHANGELOG.md
@@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
## [0.1.2] - 2023.11.01
|
## [0.1.3] - 2023.11
|
||||||
|
### Added
|
||||||
|
- Contact name editing
|
||||||
|
### Changed
|
||||||
|
- Don't show actions on front page if not registered.
|
||||||
|
### Removed
|
||||||
|
- Home page Notiwind test buttons
|
||||||
|
|
||||||
|
|
||||||
|
## [0.1.2] - 2023.11.01 - 7f6c93802911a030a89fe3706e18b5c17151e5bb
|
||||||
### Added
|
### Added
|
||||||
- Basics: create ID, record a give, declare a project, search, and get notifications.
|
- Basics: create ID, record a give, declare a project, search, and get notifications.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ npm run lint
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
###
|
### Web-push
|
||||||
|
|
||||||
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
|
For your own web-push tests, change the 'vapid' URL in App.vue, and install apps on the same domain.
|
||||||
|
|
||||||
@@ -54,8 +54,8 @@ by an existing user:
|
|||||||
On the test server, User #0 has rights to register others, so you can start
|
On the test server, User #0 has rights to register others, so you can start
|
||||||
playing one of two ways:
|
playing one of two ways:
|
||||||
|
|
||||||
- Import the keys for the test User `did:ethr:0x000Ee5654b9742f6Fe18ea970e32b97ee2247B51` by importing this seed phrase:
|
- Import the keys for the test User `did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F` by importing 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`
|
`rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage`
|
||||||
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
|
(Other test users are found [here](https://github.com/trentlarson/endorser-ch/blob/master/test/util.js).)
|
||||||
|
|
||||||
- Alternatively, register someone else under User #0 automatically:
|
- Alternatively, register someone else under User #0 automatically:
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
Prerequisites:
|
JWT Creation & Verification
|
||||||
|
|
||||||
jq
|
To run this in a script, see ./openssl_signing_console.sh
|
||||||
|
|
||||||
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
|
Prerequisites: openssl, jq
|
||||||
|
|
||||||
|
You can create a JWT using a library or by encoding the header and payload base64Url and signing it with a secret using
|
||||||
|
a ES256K algorithm. Here is an example of how you can create a JWT using the jq and openssl command line utilities:
|
||||||
|
|
||||||
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
Here is an example of how you can use openssl to sign a JWT with the ES256K algorithm:
|
||||||
|
|
||||||
@@ -15,20 +18,22 @@ openssl ec -in private.pem -pubout -out public.pem
|
|||||||
|
|
||||||
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
||||||
|
|
||||||
Next, create a payload object as a JSON object containing the claims you want to include in the JWT. For example schema.org :
|
Next, create a payload object as a JSON object containing the claims you want to include in the JWT.
|
||||||
|
For example schema.org :
|
||||||
|
|
||||||
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
||||||
|
|
||||||
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
|
Encode the header and payload objects as base64Url strings. You can use the jq command line utility to do this:
|
||||||
|
|
||||||
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
|
|
||||||
Concatenate the encoded header, payload, and a secret to create the signing input:
|
Concatenate the encoded header, payload, and a secret to create the signing input:
|
||||||
|
|
||||||
signing_input="$header_b64.$payload_b64"
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
Create the signature by signing the signing input with a ES256K algorithm and your secret. You can use the openssl command line utility to do this:
|
Create the signature by signing the signing input with a ES256K algorithm and your secret.
|
||||||
|
You can use the openssl command line utility to do this:
|
||||||
|
|
||||||
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
||||||
|
|
||||||
@@ -43,7 +48,7 @@ Authorization: Bearer $jwt
|
|||||||
|
|
||||||
To verify the JWT, you can use the openssl utility with the public key:
|
To verify the JWT, you can use the openssl utility with the public key:
|
||||||
|
|
||||||
openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature") "$signing_input"
|
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature")
|
||||||
|
|
||||||
This will verify the signature and output Verified OK if the signature is valid. If the signature is not valid, it will output an error.
|
|
||||||
|
|
||||||
|
This will verify the signature and output "Verified OK" if the signature is valid.
|
||||||
|
If the signature is not valid, it will give an error response and output "Verification failure".
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Generate a JWT, with signature verified using OpenSSL
|
||||||
|
#
|
||||||
|
# Prerequisites: openssl, jq
|
||||||
|
#
|
||||||
|
# Usage: source ./openssl_signing_console.sh
|
||||||
|
#
|
||||||
|
# For a more complete explanation, see ./openssl_signing_console.rst
|
||||||
|
#
|
||||||
|
# It's crazy that raw execution only works about 20% of the time!
|
||||||
|
# See https://stackoverflow.com/questions/77505582/why-would-openssl-verify-succeed-every-time-with-source-but-fail-80-of-the
|
||||||
|
|
||||||
|
|
||||||
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
openssl ecparam -name secp256k1 -genkey -noout -out private.pem
|
||||||
openssl ec -in private.pem -pubout -out public.pem
|
openssl ec -in private.pem -pubout -out public.pem
|
||||||
|
|
||||||
@@ -7,19 +19,25 @@ header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}'
|
|||||||
|
|
||||||
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
payload='{"@context": "http://schema.org", "@type": "PlanAction", "identifier": "did:ethr:0xb86913f83A867b5Ef04902419614A6FF67466c12", "name": "Test", "description": "Me"}'
|
||||||
|
|
||||||
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n')
|
header_b64=$(echo -n "$header" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n')
|
payload_b64=$(echo -n "$payload" | jq -c -M . | tr -d '\n' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
|
|
||||||
signing_input="$header_b64.$payload_b64"
|
signing_input="$header_b64.$payload_b64"
|
||||||
|
|
||||||
echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem -out signature.bin
|
signature=$(echo -n "$signing_input" | openssl dgst -sha256 -sign private.pem)
|
||||||
|
|
||||||
# Read binary signature from file and encode it to Base64 URL-Safe format
|
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature")
|
||||||
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_')
|
|
||||||
|
# Also tested this, to no avail.
|
||||||
|
#echo -n "$signature" > sig.out
|
||||||
|
#echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature sig.out
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Read binary signature and encode it to Base64 URL-Safe format
|
||||||
|
signature_b64=$(echo -n "$signature" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_')
|
||||||
|
|
||||||
# Construct the JWT
|
# Construct the JWT
|
||||||
jwt="$signing_input.$signature_b64"
|
jwt="$signing_input.$signature_b64"
|
||||||
|
|
||||||
openssl dgst -sha256 -verify public.pem -signature signature.bin -out verified.txt <(echo -n "$signing_input")
|
echo Resulting JWT: $jwt
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "kickstart-for-time-pwa",
|
"name": "kickstart-for-time-pwa",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "kickstart-for-time-pwa",
|
"name": "kickstart-for-time-pwa",
|
||||||
"version": "0.1.0",
|
"version": "0.1.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||||
|
|||||||
@@ -5,13 +5,6 @@ tasks:
|
|||||||
- 40 notifications :
|
- 40 notifications :
|
||||||
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||||
|
|
||||||
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
|
|
||||||
|
|
||||||
- 01 fix the Discovery map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
|
||||||
- .1 add instructions for map location selection
|
|
||||||
|
|
||||||
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished
|
|
||||||
|
|
||||||
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
|
- 01 Ensure each action sent to the server has a confirmation - eg registration (ie a toast something that dismisses after 5-10s)
|
||||||
|
|
||||||
- Home Feed & Quick Give screen :
|
- Home Feed & Quick Give screen :
|
||||||
@@ -21,8 +14,8 @@ tasks:
|
|||||||
|
|
||||||
- 24 Move to Vite assignee:matthew
|
- 24 Move to Vite assignee:matthew
|
||||||
|
|
||||||
|
- .3 fix the Project-location-selection map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
||||||
- .5 switch so DiscoverView shows anywhere by default, and no number unless search is done (and maybe a better filter UI, including "mine" to consolidate with ProjectsView)
|
- .5 switch so DiscoverView shows anywhere by default, and no number unless search is done (and maybe a better filter UI, including "mine" to consolidate with ProjectsView)
|
||||||
- .2 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
|
|
||||||
- .5 Add infinite scroll to gifts on the home page
|
- .5 Add infinite scroll to gifts on the home page
|
||||||
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
|
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
|
||||||
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
|
- .2 figure out why endorser-mobile search doesn't find recently created PlanAction
|
||||||
@@ -48,7 +41,8 @@ tasks:
|
|||||||
- .5 Display a more appealing confirmation on the map when erasing the marker
|
- .5 Display a more appealing confirmation on the map when erasing the marker
|
||||||
- .5 make a VC details page
|
- .5 make a VC details page
|
||||||
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
|
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
|
||||||
- .1 remove firstName (& lastName) from localStorage
|
- .5 include the hash of the latest commit on help page next to version
|
||||||
|
- .5 remove references to localStorage for projectId (now that it's pulling from the path)
|
||||||
|
|
||||||
- contacts v+ :
|
- contacts v+ :
|
||||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||||
@@ -65,6 +59,7 @@ tasks:
|
|||||||
|
|
||||||
- Release Minimum Viable Product :
|
- Release Minimum Viable Product :
|
||||||
- 08 thorough testing for errors & edge cases
|
- 08 thorough testing for errors & edge cases
|
||||||
|
- 01 ensure ability to recover server remotely, and add redundant access
|
||||||
- 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.
|
||||||
- Switch default server to the public server.
|
- Switch default server to the public server.
|
||||||
@@ -84,6 +79,10 @@ tasks:
|
|||||||
- for subtasks: fulfills (is it really the same?), feeds, contributes to, supplies, boosts, advances
|
- 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)
|
- 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)
|
||||||
|
|
||||||
|
- .5 add "back" button to all screens that aren't part of the bottom tray
|
||||||
|
- .5 fit as many icons as possible on home & project view screens but only going halfway down the page assignee-group:ui
|
||||||
|
- .5 Replace Gifted/Give in ContactsView with GiftedDialog
|
||||||
|
|
||||||
- Stats :
|
- Stats :
|
||||||
- 01 point out user's location on the world
|
- 01 point out user's location on the world
|
||||||
- 01 present a credential selected from the stats
|
- 01 present a credential selected from the stats
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/project",
|
path: "/project/:id?",
|
||||||
name: "project",
|
name: "project",
|
||||||
component: () =>
|
component: () =>
|
||||||
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
|
import(/* webpackChunkName: "project" */ "../views/ProjectViewView.vue"),
|
||||||
@@ -168,6 +168,14 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
|
/* webpackChunkName: "scan-contact" */ "../views/ContactScanView.vue"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/search-area",
|
||||||
|
name: "search-area",
|
||||||
|
component: () =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "search-area" */ "../views/SearchAreaView.vue"
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/seed-backup",
|
path: "/seed-backup",
|
||||||
name: "seed-backup",
|
name: "seed-backup",
|
||||||
|
|||||||
@@ -655,11 +655,11 @@ export default class AccountViewView extends Vue {
|
|||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "toast",
|
type: "success",
|
||||||
title: "Download Started",
|
title: "Download Started",
|
||||||
text: "See your downloads directory for the backup.",
|
text: "See your downloads directory for the backup.",
|
||||||
},
|
},
|
||||||
5000,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
<QuickNav selected="Contacts"></QuickNav>
|
<QuickNav selected="Contacts"></QuickNav>
|
||||||
<section id="Content" class="p-6 pb-24">
|
<section id="Content" class="p-6 pb-24">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div id="ViewBreadcrumb" class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="text-lg text-center font-light relative px-7">
|
<h1
|
||||||
|
id="ViewBreadcrumb"
|
||||||
|
class="text-lg text-center font-light relative px-7"
|
||||||
|
>
|
||||||
<!-- Back -->
|
<!-- Back -->
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'contacts' }"
|
:to="{ name: 'contacts' }"
|
||||||
@@ -11,11 +14,12 @@
|
|||||||
><fa icon="chevron-left" class="fa-fw"></fa
|
><fa icon="chevron-left" class="fa-fw"></fa
|
||||||
></router-link>
|
></router-link>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
||||||
|
Given with {{ contact?.name }}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
|
||||||
Given with {{ contact?.name }}
|
|
||||||
</h1>
|
|
||||||
<div class="flex justify-around">
|
<div class="flex justify-around">
|
||||||
<span />
|
<span />
|
||||||
<span class="justify-around">(Only 50 most recent)</span>
|
<span class="justify-around">(Only 50 most recent)</span>
|
||||||
@@ -358,7 +362,10 @@ export default class ContactsView extends Vue {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
|
/*
|
||||||
|
Tooltip, generated on "title" attributes on "fa" icons
|
||||||
|
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
||||||
|
*/
|
||||||
/* Tooltip container */
|
/* Tooltip container */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -218,7 +218,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p v-else>This identity has no contacts.</p>
|
<p v-else>There are no contacts.</p>
|
||||||
|
|
||||||
<div v-if="contactEdit !== null" class="dialog-overlay">
|
<div v-if="contactEdit !== null" class="dialog-overlay">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
@@ -1050,7 +1050,10 @@ export default class ContactsView extends Vue {
|
|||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
|
/*
|
||||||
|
Tooltip, generated on "title" attributes on "fa" icons
|
||||||
|
Kudos to https://www.w3schools.com/css/css_tooltip.asp
|
||||||
|
*/
|
||||||
/* Tooltip container */
|
/* Tooltip container */
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -67,46 +67,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isLocalActive">
|
<div v-if="isLocalActive">
|
||||||
<div v-if="!isChoosingSearchBox">
|
<div>
|
||||||
<button
|
<button
|
||||||
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
@click="isChoosingSearchBox = true"
|
@click="$router.push({ name: 'search-area' })"
|
||||||
>
|
>
|
||||||
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
|
||||||
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
|
|
||||||
Choose Location Below for Nearby Search
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="storeSearchBox"
|
|
||||||
>
|
|
||||||
Store This Location for Nearby Search
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="searchBox"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="forgetSearchBox"
|
|
||||||
>
|
|
||||||
Delete Stored Location
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="resetLatLong"
|
|
||||||
>
|
|
||||||
Reset Marker
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
|
||||||
@click="cancelSearchBoxSelect"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading Animation -->
|
<!-- Loading Animation -->
|
||||||
@@ -150,50 +118,11 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="isLocalActive && isChoosingSearchBox"
|
|
||||||
style="height: 600px; width: 800px"
|
|
||||||
>
|
|
||||||
<l-map
|
|
||||||
ref="map"
|
|
||||||
:center="[localCenterLat, localCenterLong]"
|
|
||||||
v-model:zoom="localZoom"
|
|
||||||
@click="setMapPoint"
|
|
||||||
>
|
|
||||||
<l-tile-layer
|
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
||||||
layer-type="base"
|
|
||||||
name="OpenStreetMap"
|
|
||||||
/>
|
|
||||||
<l-marker
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
:lat-lng="[localCenterLat, localCenterLong]"
|
|
||||||
@click="isNewMarkerSet = false"
|
|
||||||
/>
|
|
||||||
<l-rectangle
|
|
||||||
v-if="isNewMarkerSet"
|
|
||||||
:bounds="[
|
|
||||||
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
|
|
||||||
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
|
|
||||||
]"
|
|
||||||
:weight="1"
|
|
||||||
/>
|
|
||||||
</l-map>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { LeafletMouseEvent } from "leaflet";
|
|
||||||
import "leaflet/dist/leaflet.css";
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import {
|
|
||||||
LMap,
|
|
||||||
LMarker,
|
|
||||||
LRectangle,
|
|
||||||
LTileLayer,
|
|
||||||
} from "@vue-leaflet/vue-leaflet";
|
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
@@ -204,10 +133,6 @@ import QuickNav from "@/components/QuickNav.vue";
|
|||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
|
||||||
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
|
||||||
const WORLD_ZOOM = 2;
|
|
||||||
const DEFAULT_ZOOM = 2;
|
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -217,13 +142,9 @@ interface Notification {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
LRectangle,
|
|
||||||
QuickNav,
|
QuickNav,
|
||||||
InfiniteScroll,
|
InfiniteScroll,
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
LMap,
|
|
||||||
LMarker,
|
|
||||||
LTileLayer,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class DiscoverView extends Vue {
|
export default class DiscoverView extends Vue {
|
||||||
@@ -238,13 +159,7 @@ export default class DiscoverView extends Vue {
|
|||||||
isChoosingSearchBox = false;
|
isChoosingSearchBox = false;
|
||||||
isLocalActive = true;
|
isLocalActive = true;
|
||||||
isRemoteActive = false;
|
isRemoteActive = false;
|
||||||
isNewMarkerSet = false;
|
|
||||||
localCenterLat = 0;
|
|
||||||
localCenterLong = 0;
|
|
||||||
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
localCount = 0;
|
localCount = 0;
|
||||||
localZoom = DEFAULT_ZOOM;
|
|
||||||
remoteCount = 0;
|
remoteCount = 0;
|
||||||
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
@@ -258,7 +173,6 @@ export default class DiscoverView extends Vue {
|
|||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = settings?.activeDid || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = settings?.apiServer || "";
|
||||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||||
this.resetLatLong();
|
|
||||||
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
@@ -462,128 +376,6 @@ export default class DiscoverView extends Vue {
|
|||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
setMapPoint(event: LeafletMouseEvent) {
|
|
||||||
if (this.isNewMarkerSet) {
|
|
||||||
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
|
|
||||||
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
|
|
||||||
} else {
|
|
||||||
// marker is not set
|
|
||||||
this.localCenterLat = event.latlng.lat;
|
|
||||||
this.localCenterLong = event.latlng.lng;
|
|
||||||
|
|
||||||
let latDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
let longDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
// Guess at a size for the bounding box.
|
|
||||||
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
|
|
||||||
const bounds = event.target.boxZoom?._map?.getBounds();
|
|
||||||
if (bounds) {
|
|
||||||
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
|
|
||||||
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
|
|
||||||
}
|
|
||||||
this.localLatDiff = latDiff;
|
|
||||||
this.localLongDiff = longDiff;
|
|
||||||
this.isNewMarkerSet = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetLatLong() {
|
|
||||||
if (this.searchBox?.bbox) {
|
|
||||||
const bbox = this.searchBox.bbox;
|
|
||||||
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
|
||||||
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
|
||||||
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
|
|
||||||
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
|
|
||||||
this.localZoom = WORLD_ZOOM;
|
|
||||||
this.isNewMarkerSet = true;
|
|
||||||
} else {
|
|
||||||
this.isNewMarkerSet = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async storeSearchBox() {
|
|
||||||
if (this.localCenterLong || this.localCenterLat) {
|
|
||||||
try {
|
|
||||||
const newSearchBox = {
|
|
||||||
name: "Local",
|
|
||||||
bbox: {
|
|
||||||
eastLong: this.localCenterLong + this.localLongDiff,
|
|
||||||
maxLat: this.localCenterLat + this.localLatDiff,
|
|
||||||
minLat: this.localCenterLat - this.localLatDiff,
|
|
||||||
westLong: this.localCenterLong - this.localLongDiff,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await db.open();
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
searchBoxes: [newSearchBox],
|
|
||||||
});
|
|
||||||
this.searchBox = newSearchBox;
|
|
||||||
this.isChoosingSearchBox = false;
|
|
||||||
this.searchLocal();
|
|
||||||
} catch (err) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Updating Search Settings",
|
|
||||||
text: "Try going to a different page and then coming back.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Telling user to retry the location search setting because:",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "warning",
|
|
||||||
title: "No Location Selected",
|
|
||||||
text: "Select a location on the map.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async forgetSearchBox() {
|
|
||||||
try {
|
|
||||||
await db.open();
|
|
||||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
|
||||||
searchBoxes: [],
|
|
||||||
});
|
|
||||||
this.searchBox = null;
|
|
||||||
this.localCenterLat = 0;
|
|
||||||
this.localCenterLong = 0;
|
|
||||||
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
|
||||||
this.localZoom = DEFAULT_ZOOM;
|
|
||||||
this.isChoosingSearchBox = false;
|
|
||||||
this.isNewMarkerSet = false;
|
|
||||||
this.searchLocal();
|
|
||||||
} catch (err) {
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error Updating Search Settings",
|
|
||||||
text: "Try going to a different page and then coming back.",
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
console.error(
|
|
||||||
"Telling user to retry the location search setting because:",
|
|
||||||
err,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public cancelSearchBoxSelect() {
|
|
||||||
this.isChoosingSearchBox = false;
|
|
||||||
this.localZoom = WORLD_ZOOM;
|
|
||||||
}
|
|
||||||
|
|
||||||
public computedLocalTabClassNames() {
|
public computedLocalTabClassNames() {
|
||||||
return {
|
return {
|
||||||
"inline-block": true,
|
"inline-block": true,
|
||||||
|
|||||||
@@ -50,6 +50,11 @@
|
|||||||
<label for="includeLocation">Include Location</label>
|
<label for="includeLocation">Include Location</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
<div v-if="includeLocation" style="height: 600px; width: 800px">
|
||||||
|
<div class="px-2 py-2">
|
||||||
|
For your security, we recommend you choose a location nearby but not
|
||||||
|
exactly at the place.
|
||||||
|
</div>
|
||||||
|
|
||||||
<l-map
|
<l-map
|
||||||
ref="map"
|
ref="map"
|
||||||
v-model:zoom="zoom"
|
v-model:zoom="zoom"
|
||||||
|
|||||||
@@ -137,10 +137,12 @@
|
|||||||
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
|
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
Given to this Project
|
Given To This Project
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<ul class="text-sm border-t border-slate-300">
|
<div v-if="givesToThis.length === 0">(None yet. Record one above.)</div>
|
||||||
|
|
||||||
|
<ul v-else class="text-sm border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
v-for="give in givesToThis"
|
v-for="give in givesToThis"
|
||||||
:key="give.id"
|
:key="give.id"
|
||||||
@@ -164,31 +166,33 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
|
||||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
…and from this Project
|
Contributions By This Project
|
||||||
</h3>
|
</h3>
|
||||||
|
<button
|
||||||
|
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
||||||
|
class="text-blue-500"
|
||||||
|
>
|
||||||
|
{{ fulfilledByThis.name }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul class="text-sm border-t border-slate-300">
|
<div
|
||||||
<li
|
v-if="fulfillersToThis.length > 0"
|
||||||
v-for="give in givesByThis"
|
class="bg-slate-100 px-4 py-3 rounded-md"
|
||||||
:key="give.id"
|
>
|
||||||
class="py-1.5 border-b border-slate-300"
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||||
>
|
Contributions To This Project
|
||||||
<div class="flex justify-between gap-4">
|
</h3>
|
||||||
<span
|
<ul>
|
||||||
><fa icon="user" class="fa-fw text-slate-400"></fa>
|
<li v-for="plan in fulfillersToThis" :key="plan.handleId">
|
||||||
{{ didInfo(give.agentDid, activeDid, allMyDids, allContacts) }}
|
<button
|
||||||
</span>
|
@click="onClickLoadProject(plan.handleId)"
|
||||||
<span v-if="give.amount"
|
class="text-blue-500"
|
||||||
><fa icon="coins" class="fa-fw text-slate-400"></fa>
|
>
|
||||||
{{ give.amount }}
|
{{ plan.name }}
|
||||||
</span>
|
</button>
|
||||||
</div>
|
|
||||||
<div v-if="give.description" class="text-slate-500">
|
|
||||||
<fa icon="comment" class="fa-fw text-slate-400"></fa>
|
|
||||||
{{ give.description }}
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,6 +222,7 @@ import {
|
|||||||
didInfo,
|
didInfo,
|
||||||
GiverInputInfo,
|
GiverInputInfo,
|
||||||
GiveServerRecord,
|
GiveServerRecord,
|
||||||
|
PlanServerRecord,
|
||||||
} from "@/libs/endorserServer";
|
} from "@/libs/endorserServer";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
@@ -242,8 +247,9 @@ export default class ProjectViewView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
description = "";
|
description = "";
|
||||||
expanded = false;
|
expanded = false;
|
||||||
|
fulfilledByThis: PlanServerRecord | null = null;
|
||||||
|
fulfillersToThis: Array<PlanServerRecord> = [];
|
||||||
givesToThis: Array<GiveServerRecord> = [];
|
givesToThis: Array<GiveServerRecord> = [];
|
||||||
givesByThis: Array<GiveServerRecord> = [];
|
|
||||||
latitude = 0;
|
latitude = 0;
|
||||||
longitude = 0;
|
longitude = 0;
|
||||||
name = "";
|
name = "";
|
||||||
@@ -266,7 +272,12 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.allMyDids = accountsArr.map((acc) => acc.did);
|
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
const identity = JSON.parse(account?.identity || "null");
|
||||||
this.LoadProject(identity);
|
|
||||||
|
const pathParam = window.location.pathname.substring("/project/".length);
|
||||||
|
if (pathParam) {
|
||||||
|
this.projectId = decodeURIComponent(pathParam);
|
||||||
|
}
|
||||||
|
this.LoadProject(this.projectId, identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
public async getIdentity(activeDid: string) {
|
||||||
@@ -320,11 +331,11 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.expanded = false;
|
this.expanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async LoadProject(identity: IIdentifier) {
|
async LoadProject(projectId: string, identity: IIdentifier) {
|
||||||
|
this.projectId = projectId;
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
this.apiServer +
|
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId);
|
||||||
"/api/claim/byHandle/" +
|
|
||||||
encodeURIComponent(this.projectId);
|
|
||||||
const headers: RawAxiosRequestHeaders = {
|
const headers: RawAxiosRequestHeaders = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
};
|
};
|
||||||
@@ -389,7 +400,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
const givesInUrl =
|
const givesInUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/givesForPlans?planIds=" +
|
"/api/v2/report/givesForPlans?planIds=" +
|
||||||
encodeURIComponent(JSON.stringify([this.projectId]));
|
encodeURIComponent(JSON.stringify([projectId]));
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(givesInUrl, { headers });
|
const resp = await this.axios.get(givesInUrl, { headers });
|
||||||
if (resp.status === 200 && resp.data.data) {
|
if (resp.status === 200 && resp.data.data) {
|
||||||
@@ -422,21 +433,21 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const givesOutUrl =
|
const fulfilledByUrl =
|
||||||
this.apiServer +
|
this.apiServer +
|
||||||
"/api/v2/report/givesProvidedBy?providerId=" +
|
"/api/v2/report/planFulfilledByPlan?planHandleId=" +
|
||||||
encodeURIComponent(this.projectId);
|
encodeURIComponent(projectId);
|
||||||
try {
|
try {
|
||||||
const resp = await this.axios.get(givesOutUrl, { headers });
|
const resp = await this.axios.get(fulfilledByUrl, { headers });
|
||||||
if (resp.status === 200 && resp.data.data) {
|
if (resp.status === 200) {
|
||||||
this.givesByThis = resp.data.data;
|
this.fulfilledByThis = resp.data.data;
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Failed to retrieve gives by this project.",
|
text: "Failed to retrieve plans fulfilled by this project.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
@@ -448,15 +459,64 @@ export default class ProjectViewView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: "Something went wrong retrieving gives by project.",
|
text: "Something went wrong retrieving plans fulfilled by this project.",
|
||||||
},
|
},
|
||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
console.error(
|
console.error(
|
||||||
"Error retrieving gives by this project:",
|
"Error retrieving plans fulfilled by this project:",
|
||||||
serverError.message,
|
serverError.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fulfillersToUrl =
|
||||||
|
this.apiServer +
|
||||||
|
"/api/v2/report/planFulfillersToPlan?planHandleId=" +
|
||||||
|
encodeURIComponent(projectId);
|
||||||
|
try {
|
||||||
|
const resp = await this.axios.get(fulfillersToUrl, { headers });
|
||||||
|
if (resp.status === 200) {
|
||||||
|
this.fulfillersToThis = resp.data.data;
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to retrieve plan fulfillers to this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const serverError = error as AxiosError;
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Something went wrong retrieving plan fulfillers to this project.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Error retrieving plan fulfillers to this project:",
|
||||||
|
serverError.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle clicking on a project entry found in the list
|
||||||
|
* @param id of the project
|
||||||
|
**/
|
||||||
|
async onClickLoadProject(projectId: string) {
|
||||||
|
localStorage.setItem("projectId", projectId);
|
||||||
|
const route = {
|
||||||
|
path: "/project/" + encodeURIComponent(projectId),
|
||||||
|
};
|
||||||
|
this.$router.push(route);
|
||||||
|
this.LoadProject(projectId, await this.getIdentity(this.activeDid));
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpenStreetMapUrl() {
|
getOpenStreetMapUrl() {
|
||||||
|
|||||||
281
src/views/SearchAreaView.vue
Normal file
281
src/views/SearchAreaView.vue
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<template>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="p-6 pb-24">
|
||||||
|
<!-- Breadcrumb -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<!-- Back -->
|
||||||
|
<div class="text-lg text-center font-light relative px-7">
|
||||||
|
<h1
|
||||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||||
|
@click="$router.back()"
|
||||||
|
>
|
||||||
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Heading -->
|
||||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||||
|
Area for Nearby Search
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-2 py-4">
|
||||||
|
This location is only stored on your device. It is used to show you more
|
||||||
|
appropriate projects but is not stored on any servers.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button v-if="!searchBox && !isNewMarkerSet" class="m-4 px-4 py-2">
|
||||||
|
Click to Choose a Location for Nearby Search
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="storeSearchBox"
|
||||||
|
>
|
||||||
|
Store This Location for Nearby Search
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="searchBox"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="forgetSearchBox"
|
||||||
|
>
|
||||||
|
Delete Stored Location
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="searchBox"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="resetLatLong"
|
||||||
|
>
|
||||||
|
Reset Marker
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||||
|
@click="isNewMarkerSet = false"
|
||||||
|
>
|
||||||
|
Erase Marker
|
||||||
|
</button>
|
||||||
|
<div v-if="isNewMarkerSet">
|
||||||
|
Click on the pin to erase it. Click anywhere else to set a different
|
||||||
|
different corner.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="height: 600px; width: 800px">
|
||||||
|
<l-map
|
||||||
|
ref="map"
|
||||||
|
:center="[localCenterLat, localCenterLong]"
|
||||||
|
v-model:zoom="localZoom"
|
||||||
|
@click="setMapPoint"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
:lat-lng="[localCenterLat, localCenterLong]"
|
||||||
|
@click="isNewMarkerSet = false"
|
||||||
|
/>
|
||||||
|
<l-rectangle
|
||||||
|
v-if="isNewMarkerSet"
|
||||||
|
:bounds="[
|
||||||
|
[localCenterLat - localLatDiff, localCenterLong - localLongDiff],
|
||||||
|
[localCenterLat + localLatDiff, localCenterLong + localLongDiff],
|
||||||
|
]"
|
||||||
|
:weight="1"
|
||||||
|
/>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { LeafletMouseEvent } from "leaflet";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import {
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LRectangle,
|
||||||
|
LTileLayer,
|
||||||
|
} from "@vue-leaflet/vue-leaflet";
|
||||||
|
|
||||||
|
import { db } from "@/db/index";
|
||||||
|
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
|
|
||||||
|
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
||||||
|
const WORLD_ZOOM = 2;
|
||||||
|
const DEFAULT_ZOOM = 2;
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
group: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
LRectangle,
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LTileLayer,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class DiscoverView extends Vue {
|
||||||
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
|
isChoosingSearchBox = false;
|
||||||
|
isNewMarkerSet = false;
|
||||||
|
|
||||||
|
// "local" vars are for the currently selected map box
|
||||||
|
localCenterLat = 0;
|
||||||
|
localCenterLong = 0;
|
||||||
|
localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
localZoom = DEFAULT_ZOOM;
|
||||||
|
|
||||||
|
// searchBox reflects what is stored in the database
|
||||||
|
searchBox: { name: string; bbox: BoundingBox } | null = null;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await db.open();
|
||||||
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
|
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||||
|
this.resetLatLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
setMapPoint(event: LeafletMouseEvent) {
|
||||||
|
if (this.isNewMarkerSet) {
|
||||||
|
this.localLatDiff = Math.abs(event.latlng.lat - this.localCenterLat);
|
||||||
|
this.localLongDiff = Math.abs(event.latlng.lng - this.localCenterLong);
|
||||||
|
} else {
|
||||||
|
// marker is not set
|
||||||
|
this.localCenterLat = event.latlng.lat;
|
||||||
|
this.localCenterLong = event.latlng.lng;
|
||||||
|
|
||||||
|
let latDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
let longDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
// Guess at a size for the bounding box.
|
||||||
|
// This doesn't seem like the right approach but it's the only way I can find to get the screen bounds.
|
||||||
|
const bounds = event.target.boxZoom?._map?.getBounds();
|
||||||
|
if (bounds) {
|
||||||
|
latDiff = Math.abs(bounds._northEast.lat - bounds._southWest.lat) / 8;
|
||||||
|
longDiff = Math.abs(bounds._northEast.lng - bounds._southWest.lng) / 8;
|
||||||
|
}
|
||||||
|
this.localLatDiff = latDiff;
|
||||||
|
this.localLongDiff = longDiff;
|
||||||
|
this.isNewMarkerSet = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetLatLong() {
|
||||||
|
if (this.searchBox?.bbox) {
|
||||||
|
const bbox = this.searchBox.bbox;
|
||||||
|
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
|
||||||
|
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
|
||||||
|
this.localLatDiff = (bbox.maxLat - bbox.minLat) / 2;
|
||||||
|
this.localLongDiff = (bbox.eastLong - bbox.westLong) / 2;
|
||||||
|
this.localZoom = WORLD_ZOOM;
|
||||||
|
this.isNewMarkerSet = true;
|
||||||
|
} else {
|
||||||
|
this.isNewMarkerSet = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async storeSearchBox() {
|
||||||
|
if (this.localCenterLong || this.localCenterLat) {
|
||||||
|
try {
|
||||||
|
const newSearchBox = {
|
||||||
|
name: "Local",
|
||||||
|
bbox: {
|
||||||
|
eastLong: this.localCenterLong + this.localLongDiff,
|
||||||
|
maxLat: this.localCenterLat + this.localLatDiff,
|
||||||
|
minLat: this.localCenterLat - this.localLatDiff,
|
||||||
|
westLong: this.localCenterLong - this.localLongDiff,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
searchBoxes: [newSearchBox],
|
||||||
|
});
|
||||||
|
this.searchBox = newSearchBox;
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Saved",
|
||||||
|
text: "That has been saved in your preferences.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Search Settings",
|
||||||
|
text: "Try going to a different page and then coming back.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to retry the location search setting because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "No Location Selected",
|
||||||
|
text: "Select a location on the map.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async forgetSearchBox() {
|
||||||
|
try {
|
||||||
|
await db.open();
|
||||||
|
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
searchBoxes: [],
|
||||||
|
});
|
||||||
|
this.searchBox = null;
|
||||||
|
this.localCenterLat = 0;
|
||||||
|
this.localCenterLong = 0;
|
||||||
|
this.localLatDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
this.localLongDiff = DEFAULT_LAT_LONG_DIFF;
|
||||||
|
this.localZoom = DEFAULT_ZOOM;
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
this.isNewMarkerSet = false;
|
||||||
|
} catch (err) {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Updating Search Settings",
|
||||||
|
text: "Try going to a different page and then coming back.",
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"Telling user to retry the location search setting because:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelSearchBoxSelect() {
|
||||||
|
this.isChoosingSearchBox = false;
|
||||||
|
this.localZoom = WORLD_ZOOM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -16,14 +16,14 @@
|
|||||||
{
|
{
|
||||||
group: 'alert',
|
group: 'alert',
|
||||||
type: 'toast',
|
type: 'toast',
|
||||||
text: 'I\'m a toast. Don\'t mind me.',
|
text: 'I\'m a toast. Without a timeout, I\'m stuck.',
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="font-bold uppercase bg-slate-400 text-white px-3 py-2 rounded-md mr-2"
|
class="font-bold uppercase bg-slate-900 text-white px-3 py-2 rounded-md mr-2"
|
||||||
>
|
>
|
||||||
Toast (self-dismiss)
|
Toast
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ from the SERVICE.
|
|||||||
The SERVICE will provide context and obtain explicit permission before prompting
|
The SERVICE will provide context and obtain explicit permission before prompting
|
||||||
for notification permission:
|
for notification permission:
|
||||||
|
|
||||||
In order to provide this context and explict permission a two-step opt-in process
|
In order to provide this context and explicit permission, a two-step opt-in process
|
||||||
where the user is first presented with a pre-permission dialog box that explains
|
first presents the user with a pre-permission dialog box that explains
|
||||||
what the notifications are for and why they are useful. This may help reduce the
|
what the notifications are for and why they are useful. This may help reduce the
|
||||||
possibility of users clicking "don't allow".
|
possibility of users clicking "don't allow".
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ The `sw.js` file contains the logic for what a service worker should do.
|
|||||||
It executes in a separate thread of execution from the web page but provides a
|
It executes in a separate thread of execution from the web page but provides a
|
||||||
means of communicating between itself and the web page via messages.
|
means of communicating between itself and the web page via messages.
|
||||||
|
|
||||||
Note that there is a scope can specify what network requests it may
|
Note that there is a scope that can specify what network requests it may
|
||||||
intercept.
|
intercept.
|
||||||
|
|
||||||
The Vue project already has its own service worker but it is possible to
|
The Vue project already has its own service worker but it is possible to
|
||||||
|
|||||||
Reference in New Issue
Block a user