Trent Larson
12 months ago
36 changed files with 11428 additions and 5991 deletions
@ -1,25 +1,39 @@ |
|||
#!/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 |
|||
|
|||
|
|||
# Generate a key and extract the public part |
|||
openssl ecparam -name secp256k1 -genkey -noout -out private.pem |
|||
openssl ec -in private.pem -pubout -out public.pem |
|||
|
|||
# Use test data |
|||
header='{"alg":"ES256K", "issuer": "", "typ":"JWT"}' |
|||
|
|||
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') |
|||
payload_b64=$(echo -n "$payload" | 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' | base64 | tr -d '=' | tr '+' '-' | tr '/' '_') |
|||
|
|||
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 | openssl base64 -e) |
|||
|
|||
# Read binary signature from file and encode it to Base64 URL-Safe format |
|||
signature_b64=$(base64 -w 0 < signature.bin | tr -d '=' | tr '+' '-' | tr '/' '_') |
|||
echo -n "$signing_input" | openssl dgst -sha256 -verify public.pem -signature <(echo -n "$signature" | openssl base64 -d) |
|||
|
|||
# Construct the JWT |
|||
jwt="$signing_input.$signature_b64" |
|||
|
|||
openssl dgst -sha256 -verify public.pem -signature signature.bin -out verified.txt <(echo -n "$signing_input") |
|||
|
|||
|
|||
|
|||
# Read binary signature and encode it to Base64 URL-Safe format |
|||
signature_b64=$(echo -n "$signature" | base64 | tr -d '=' | tr '+' '-' | tr '/' '_') |
|||
|
|||
# Construct the JWT |
|||
jwt="$signing_input.$signature_b64" |
|||
|
|||
echo Resulting JWT: $jwt |
|||
|
@ -1,177 +0,0 @@ |
|||
|
|||
> kickstart-for-time-pwa@0.1.0 build |
|||
> vue-cli-service build |
|||
|
|||
All browser targets in the browserslist configuration have supported ES module. |
|||
Therefore we don't build two separate bundles for differential loading. |
|||
|
|||
|
|||
WARNING Compiled with 5 warnings6:06:43 PM |
|||
|
|||
[eslint] |
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/components/World/components/objects/landmarks.js |
|||
98:11 warning Unexpected console statement no-console |
|||
133:7 warning Unexpected console statement no-console |
|||
144:5 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/router/index.ts |
|||
210:3 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/AccountViewView.vue |
|||
362:7 warning Unexpected console statement no-console |
|||
375:7 warning Unexpected console statement no-console |
|||
404:7 warning Unexpected console statement no-console |
|||
516:7 warning Unexpected console statement no-console |
|||
536:7 warning Unexpected console statement no-console |
|||
630:5 warning Unexpected console statement no-console |
|||
682:7 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactAmountsView.vue |
|||
206:9 warning Unexpected console statement no-console |
|||
233:9 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactGiftingView.vue |
|||
244:9 warning Unexpected console statement no-console |
|||
267:7 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ContactsView.vue |
|||
340:9 warning Unexpected console statement no-console |
|||
577:9 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/DiscoverView.vue |
|||
315:9 warning Unexpected console statement no-console |
|||
343:7 warning Unexpected console statement no-console |
|||
390:9 warning Unexpected console statement no-console |
|||
423:7 warning Unexpected console statement no-console |
|||
532:9 warning Unexpected console statement no-console |
|||
575:7 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/HomeView.vue |
|||
349:9 warning Unexpected console statement no-console |
|||
498:9 warning Unexpected console statement no-console |
|||
521:7 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/IdentitySwitcherView.vue |
|||
142:7 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ImportAccountView.vue |
|||
123:9 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ImportDerivedAccountView.vue |
|||
159:7 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/NewEditProjectView.vue |
|||
183:9 warning Unexpected console statement no-console |
|||
215:7 warning Unexpected console statement no-console |
|||
297:13 warning Unexpected console statement no-console |
|||
320:11 warning Unexpected console statement no-console |
|||
345:7 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ProjectViewView.vue |
|||
387:9 warning Unexpected console statement no-console |
|||
421:7 warning Unexpected console statement no-console |
|||
457:7 warning Unexpected console statement no-console |
|||
552:9 warning Unexpected console statement no-console |
|||
554:11 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/ProjectsView.vue |
|||
131:9 warning Unexpected console statement no-console |
|||
144:7 warning Unexpected console statement no-console |
|||
221:9 warning Unexpected console statement no-console |
|||
237:7 warning Unexpected console statement no-console |
|||
|
|||
/home/matthew/projects/kick-starter-for-time-pwa/src/views/SeedBackupView.vue |
|||
94:7 warning Unexpected console statement no-console |
|||
|
|||
✖ 44 problems (0 errors, 44 warnings) |
|||
|
|||
|
|||
You may use special comments to disable some warnings. |
|||
Use // eslint-disable-next-line to ignore the next line. |
|||
Use /* eslint-disable */ to ignore all warnings in a file. |
|||
warning |
|||
|
|||
/models/lupine_plant/textures/lambert2SG_baseColor.png is 3.75 MB, and won't be precached. Configure maximumFileSizeToCacheInBytes to change this limit. |
|||
|
|||
warning |
|||
|
|||
/models/lupine_plant/textures/lambert2SG_normal.png is 4.91 MB, and won't be precached. Configure maximumFileSizeToCacheInBytes to change this limit. |
|||
|
|||
warning |
|||
|
|||
asset size limit: The following asset(s) exceed the recommended size limit (244 KiB). |
|||
This can impact web performance. |
|||
Assets: |
|||
js/project.44f30c9f.js (318 KiB) |
|||
js/statistics.8a97010a.js (586 KiB) |
|||
js/chunk-vendors.a4845bfb.js (411 KiB) |
|||
js/705.f6a6ce2a.js (252 KiB) |
|||
img/textures/leafy-autumn-forest-floor.jpg (705 KiB) |
|||
models/lupine_plant/textures/lambert2SG_baseColor.png (3.58 MiB) |
|||
models/lupine_plant/textures/lambert2SG_normal.png (4.69 MiB) |
|||
|
|||
warning |
|||
|
|||
entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance. |
|||
Entrypoints: |
|||
app (447 KiB) |
|||
js/chunk-vendors.a4845bfb.js |
|||
css/app.8f21529c.css |
|||
js/app.8833cebc.js |
|||
|
|||
|
|||
File Size Gzipped |
|||
|
|||
dist/js/statistics.8a97010a.js 585.72 KiB 148.80 KiB |
|||
dist/js/chunk-vendors.a4845bfb.js 411.44 KiB 137.82 KiB |
|||
dist/js/project.44f30c9f.js 317.61 KiB 78.67 KiB |
|||
dist/js/705.f6a6ce2a.js 251.66 KiB 87.12 KiB |
|||
dist/js/891.33615e4f.js 147.32 KiB 42.09 KiB |
|||
dist/js/153.e2c8e249.js 146.26 KiB 42.21 KiB |
|||
dist/js/820.13565d16.js 66.10 KiB 18.33 KiB |
|||
dist/js/contact-qr.e170ec33.js 54.85 KiB 15.63 KiB |
|||
dist/js/772.7b4c53a7.js 30.29 KiB 7.21 KiB |
|||
dist/js/361.898a4525.js 27.40 KiB 8.19 KiB |
|||
dist/js/account.77d86130.js 17.51 KiB 5.93 KiB |
|||
dist/js/app.8833cebc.js 17.31 KiB 5.84 KiB |
|||
dist/js/contacts.3fc90ff8.js 16.94 KiB 5.52 KiB |
|||
dist/js/discover.24106939.js 15.30 KiB 5.22 KiB |
|||
dist/js/536.3bb13201.js 15.23 KiB 4.84 KiB |
|||
dist/workbox-5b385ed2.js 14.11 KiB 4.93 KiB |
|||
dist/js/home.218b99dd.js 13.89 KiB 4.97 KiB |
|||
dist/js/help.50d3117b.js 12.49 KiB 4.38 KiB |
|||
dist/js/projects.417a6cb7.js 8.71 KiB 3.00 KiB |
|||
dist/js/contact-amounts.a32b0ccd.js 8.44 KiB 3.25 KiB |
|||
dist/js/229.120e09bf.js 7.99 KiB 2.72 KiB |
|||
dist/js/identity-switcher.c7937333.js 7.44 KiB 2.52 KiB |
|||
dist/js/new-edit-project.0552181b.js 7.36 KiB 3.11 KiB |
|||
dist/js/300.dcaeb2a3.js 6.56 KiB 3.24 KiB |
|||
dist/js/seed-backup.76a0f7b3.js 3.99 KiB 1.97 KiB |
|||
dist/js/import-derive.c688d4b8.js 3.81 KiB 1.82 KiB |
|||
dist/js/import-account.c3fa35fd.js 3.54 KiB 1.66 KiB |
|||
dist/js/new-edit-account.bb763be2.js 3.39 KiB 1.51 KiB |
|||
dist/js/431.5a6d64e0.js 3.38 KiB 2.56 KiB |
|||
dist/service-worker.js 3.37 KiB 1.38 KiB |
|||
dist/js/scan-contact.46be989a.js 2.79 KiB 1.18 KiB |
|||
dist/js/start.091a7740.js 2.70 KiB 1.30 KiB |
|||
dist/js/new-identifier.bb379420.js 2.12 KiB 1.18 KiB |
|||
dist/js/93.b873dbbf.js 2.08 KiB 1.61 KiB |
|||
dist/js/new-edit-commitment.9248d367.j 1.96 KiB 1.05 KiB |
|||
s |
|||
dist/js/confirm-contact.02004d1d.js 1.89 KiB 1.04 KiB |
|||
dist/js/858.ae4c08ec.js 0.97 KiB 0.78 KiB |
|||
dist/css/app.8f21529c.css 18.41 KiB 4.39 KiB |
|||
dist/css/discover.73ee9bd3.css 14.77 KiB 6.25 KiB |
|||
dist/css/new-edit-project.73ee9bd3.css 14.77 KiB 6.25 KiB |
|||
dist/css/contacts.abb5e493.css 0.40 KiB 0.23 KiB |
|||
dist/css/contact-amounts.5b26ccd4.css 0.31 KiB 0.20 KiB |
|||
dist/css/home.828bc66e.css 0.25 KiB 0.19 KiB |
|||
dist/css/project.828bc66e.css 0.25 KiB 0.19 KiB |
|||
dist/css/statistics.828bc66e.css 0.25 KiB 0.19 KiB |
|||
|
|||
Images and other types of assets omitted. |
|||
Build at: 2023-09-07T10:06:43.972Z - Hash: 2b39fcd4d0e78263 - Time: 32016ms |
|||
|
|||
DONE Build complete. The dist directory is ready to be deployed. |
|||
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html |
|||
|
@ -0,0 +1,317 @@ |
|||
<template> |
|||
<div v-if="visible" class="dialog-overlay"> |
|||
<div class="dialog"> |
|||
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1> |
|||
<input |
|||
type="text" |
|||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" |
|||
placeholder="Description, prerequisites, terms, etc." |
|||
v-model="description" |
|||
/> |
|||
<div class="flex flex-row mb-6"> |
|||
<span |
|||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2" |
|||
> |
|||
Hours |
|||
</span> |
|||
<div |
|||
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" |
|||
@click="decrement()" |
|||
> |
|||
<fa icon="chevron-left" /> |
|||
</div> |
|||
<input |
|||
type="text" |
|||
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center" |
|||
v-model="hours" |
|||
/> |
|||
<div |
|||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2" |
|||
@click="increment()" |
|||
> |
|||
<fa icon="chevron-right" /> |
|||
</div> |
|||
</div> |
|||
<div class="flex flex-row mb-6"> |
|||
<span |
|||
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2" |
|||
> |
|||
Expiration |
|||
</span> |
|||
<input |
|||
type="text" |
|||
class="w-full border border-slate-400 px-2 py-2 rounded-r" |
|||
:placeholder="'Date, eg. ' + new Date().toISOString().slice(0, 10)" |
|||
v-model="expirationDateInput" |
|||
/> |
|||
</div> |
|||
<p class="text-center mb-2 italic">Sign & Send to publish to the world</p> |
|||
<button |
|||
class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2" |
|||
@click="confirm" |
|||
> |
|||
Sign & Send |
|||
</button> |
|||
<button |
|||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md" |
|||
@click="cancel" |
|||
> |
|||
Cancel |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script lang="ts"> |
|||
import { Vue, Component, Prop } from "vue-facing-decorator"; |
|||
import { createAndSubmitOffer } from "@/libs/endorserServer"; |
|||
import { accountsDB, db } from "@/db/index"; |
|||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; |
|||
import { Account } from "@/db/tables/accounts"; |
|||
|
|||
interface Notification { |
|||
group: string; |
|||
type: string; |
|||
title: string; |
|||
text: string; |
|||
} |
|||
|
|||
@Component |
|||
export default class OfferDialog extends Vue { |
|||
$notify!: (notification: Notification, timeout?: number) => void; |
|||
|
|||
@Prop message = ""; |
|||
@Prop projectId = ""; |
|||
|
|||
activeDid = ""; |
|||
apiServer = ""; |
|||
|
|||
description = ""; |
|||
expirationDateInput = ""; |
|||
hours = "0"; |
|||
visible = false; |
|||
|
|||
async created() { |
|||
try { |
|||
await db.open(); |
|||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; |
|||
this.apiServer = settings?.apiServer || ""; |
|||
this.activeDid = settings?.activeDid || ""; |
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|||
} catch (err: any) { |
|||
console.log("Error retrieving settings from database:", err); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: |
|||
err.message || |
|||
"There was an error retrieving the latest sweet, sweet action.", |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
} |
|||
|
|||
open() { |
|||
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)}`; |
|||
} |
|||
|
|||
cancel() { |
|||
this.close(); |
|||
this.description = ""; |
|||
this.hours = "0"; |
|||
} |
|||
|
|||
async confirm() { |
|||
this.close(); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "toast", |
|||
text: "Recording the offer...", |
|||
title: "", |
|||
}, |
|||
1000, |
|||
); |
|||
// this is asynchronous, but we don't need to wait for it to complete |
|||
this.recordOffer( |
|||
this.description, |
|||
parseFloat(this.hours), |
|||
this.expirationDateInput, |
|||
).then(() => { |
|||
this.description = ""; |
|||
this.hours = "0"; |
|||
}); |
|||
} |
|||
|
|||
public async getIdentity(activeDid: string) { |
|||
await accountsDB.open(); |
|||
const account = (await accountsDB.accounts |
|||
.where("did") |
|||
.equals(activeDid) |
|||
.first()) as Account; |
|||
const identity = JSON.parse(account?.identity || "null"); |
|||
|
|||
if (!identity) { |
|||
throw new Error( |
|||
"Attempted to load Offer records for DID ${activeDid} but no identity was found", |
|||
); |
|||
} |
|||
return identity; |
|||
} |
|||
|
|||
/** |
|||
* |
|||
* @param description may be an empty string |
|||
* @param hours may be 0 |
|||
*/ |
|||
public async recordOffer( |
|||
description?: string, |
|||
hours?: number, |
|||
expirationDateInput?: string, |
|||
) { |
|||
if (!this.activeDid) { |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: "You must select an identity before you can record an offer.", |
|||
}, |
|||
-1, |
|||
); |
|||
return; |
|||
} |
|||
|
|||
if (!description && !hours) { |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: "You must enter a description or some number of hours.", |
|||
}, |
|||
-1, |
|||
); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
const identity = await this.getIdentity(this.activeDid); |
|||
const result = await createAndSubmitOffer( |
|||
this.axios, |
|||
this.apiServer, |
|||
identity, |
|||
description, |
|||
hours, |
|||
expirationDateInput, |
|||
this.projectId, |
|||
); |
|||
|
|||
if ( |
|||
result.type === "error" || |
|||
this.isOfferCreationError(result.response) |
|||
) { |
|||
const errorMessage = this.getOfferCreationErrorMessage(result); |
|||
console.log("Error with offer creation result:", result); |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: errorMessage || "There was an error creating the offer.", |
|||
}, |
|||
-1, |
|||
); |
|||
} else { |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "success", |
|||
title: "Success", |
|||
text: "That offer was recorded.", |
|||
}, |
|||
10000, |
|||
); |
|||
} |
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|||
} catch (error: any) { |
|||
console.log("Error with offer recordation caught:", error); |
|||
const message = |
|||
error.userMessage || |
|||
error.response?.data?.error?.message || |
|||
"There was an error recording the offer."; |
|||
this.$notify( |
|||
{ |
|||
group: "alert", |
|||
type: "danger", |
|||
title: "Error", |
|||
text: message, |
|||
}, |
|||
-1, |
|||
); |
|||
} |
|||
} |
|||
|
|||
// Helper functions for readability |
|||
|
|||
/** |
|||
* @param result response "data" from the server |
|||
* @returns true if the result indicates an error |
|||
*/ |
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|||
isOfferCreationError(result: any) { |
|||
return result.status !== 201 || result.data?.error; |
|||
} |
|||
|
|||
/** |
|||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data") |
|||
* @returns best guess at an error message |
|||
*/ |
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|||
getOfferCreationErrorMessage(result: any) { |
|||
return ( |
|||
result.error?.userMessage || |
|||
result.error?.error || |
|||
result.response?.data?.error?.message |
|||
); |
|||
} |
|||
} |
|||
</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; |
|||
padding: 1.5rem; |
|||
} |
|||
|
|||
.dialog { |
|||
background-color: white; |
|||
padding: 1rem; |
|||
border-radius: 0.5rem; |
|||
width: 100%; |
|||
max-width: 500px; |
|||
} |
|||
</style> |
@ -0,0 +1,286 @@ |
|||
<template> |
|||
<QuickNav /> |
|||
|
|||
<!-- 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"; |
|||
import QuickNav from "@/components/QuickNav.vue"; |
|||
|
|||
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: { |
|||
QuickNav, |
|||
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, |
|||
); |
|||
this.$router.back(); |
|||
} 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> |
@ -1,33 +1,65 @@ |
|||
const notifications = require("./safari-notifications.js"); |
|||
/* eslint-env serviceworker */ |
|||
/* global workbox */ |
|||
importScripts( |
|||
"https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js", |
|||
); |
|||
|
|||
self.addEventListener("install", (event) => { |
|||
console.error(event); |
|||
importScripts( |
|||
"safari-notifications.js", |
|||
"nacl.js", |
|||
"noble-curves.js", |
|||
"noble-hashes.js", |
|||
); |
|||
}); |
|||
|
|||
self.addEventListener("push", function (event) { |
|||
let payload; |
|||
if (event.data) { |
|||
payload = JSON.parse(event.data.text()); |
|||
} |
|||
|
|||
const title = payload ? payload.title : "Custom Title"; |
|||
const options = { |
|||
body: payload ? payload.body : "Custom body text", |
|||
icon: payload ? payload.icon : "icon.png", |
|||
badge: payload ? payload.badge : "badge.png", |
|||
}; |
|||
|
|||
event.waitUntil(self.registration.showNotification(title, options)); |
|||
event.waitUntil( |
|||
(async () => { |
|||
try { |
|||
let payload; |
|||
if (event.data) { |
|||
payload = JSON.parse(event.data.text()); |
|||
} |
|||
const message = await self.getNotificationCount(); |
|||
console.error(message); |
|||
const title = payload ? payload.title : "Custom Title"; |
|||
const options = { |
|||
body: message, |
|||
icon: payload ? payload.icon : "icon.png", |
|||
badge: payload ? payload.badge : "badge.png", |
|||
}; |
|||
await self.registration.showNotification(title, options); |
|||
} catch (error) { |
|||
console.error("Error in processing the push event:", error); |
|||
} |
|||
})(), |
|||
); |
|||
}); |
|||
|
|||
self.addEventListener("message", (event) => { |
|||
if (event.data && event.data.type === "SEND_LOCAL_DATA") { |
|||
self.secret = event.data.data; |
|||
event.ports[0].postMessage({ success: true }); |
|||
} |
|||
}); |
|||
|
|||
self.addEventListener("message", function (event) { |
|||
const data = event.data; |
|||
|
|||
const result = notifications.getNotificationCount() |
|||
self.addEventListener("activate", (event) => { |
|||
event.waitUntil(clients.claim()); |
|||
console.log("Service worker activated", event); |
|||
}); |
|||
|
|||
switch (data.command) { |
|||
case "account": |
|||
break; |
|||
self.addEventListener("fetch", (event) => { |
|||
console.log(event.request); |
|||
}); |
|||
|
|||
default: |
|||
console.log("Unknown command:", data.command); |
|||
} |
|||
self.addEventListener("error", (event) => { |
|||
console.error("Error in Service Worker:", event.message); |
|||
console.error("File:", event.filename); |
|||
console.error("Line:", event.lineno); |
|||
console.error("Column:", event.colno); |
|||
console.error("Error Object:", event.error); |
|||
}); |
|||
|
|||
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST); |
|||
|
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue