assign boundaries for local search #63

Merged
anomalist merged 18 commits from search-bbox into master 1 year ago
  1. 3
      README.md
  2. 33240
      package-lock.json
  3. 2
      package.json
  4. 10
      project.task.yaml
  5. 47
      src/components/AlertMessage.vue
  6. 2
      src/components/EntityIcon.vue
  7. 12
      src/components/GiftedDialog.vue
  8. 11
      src/db/tables/settings.ts
  9. 129
      src/libs/endorserServer.ts
  10. 23
      src/router/index.ts
  11. 104
      src/views/AccountViewView.vue
  12. 32
      src/views/ContactAmountsView.vue
  13. 130
      src/views/ContactGiftingView.vue
  14. 15
      src/views/ContactQRScanShowView.vue
  15. 88
      src/views/ContactsView.vue
  16. 315
      src/views/DiscoverView.vue
  17. 2
      src/views/HelpView.vue
  18. 119
      src/views/HomeView.vue
  19. 57
      src/views/IdentitySwitcherView.vue
  20. 2
      src/views/ImportAccountView.vue
  21. 12
      src/views/ImportDerivedAccountView.vue
  22. 2
      src/views/NewEditAccountView.vue
  23. 43
      src/views/NewEditProjectView.vue
  24. 4
      src/views/NewIdentifierView.vue
  25. 99
      src/views/ProjectViewView.vue
  26. 46
      src/views/ProjectsView.vue
  27. 30
      src/views/SeedBackupView.vue
  28. 2
      src/views/StartView.vue
  29. 42
      src/views/StatisticsView.vue
  30. 14
      tsconfig.json

3
README.md

@ -99,6 +99,9 @@ See https://tea.xyz
### Reference Material ### Reference Material
* Notifications can be type of `toast` (self-dismiss), `info`, `success`, `warning`, and `danger`.
They are done via [notiwind](https://www.npmjs.com/package/notiwind) and set up in App.vue.
``` ```
// reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83 // reference material from https://github.com/trentlarson/endorser-mobile/blob/8dc8e0353e0cc80ffa7ed89ded15c8b0da92726b/src/utility/idUtility.ts#L83

33240
package-lock.json

File diff suppressed because it is too large

2
package.json

@ -52,11 +52,11 @@
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vue-facing-decorator": "^2.1.20", "vue-facing-decorator": "^2.1.20",
"vue-property-decorator": "^9.1.2",
"vue-router": "^4.2.3", "vue-router": "^4.2.3",
"web-did-resolver": "^2.0.27" "web-did-resolver": "^2.0.27"
}, },
"devDependencies": { "devDependencies": {
"@types/leaflet": "^1.9.4",
"@types/ramda": "^0.29.3", "@types/ramda": "^0.29.3",
"@types/three": "^0.152.1", "@types/three": "^0.152.1",
"@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/eslint-plugin": "^5.61.0",

10
project.task.yaml

@ -1,14 +1,17 @@
tasks: tasks:
- fix "any" warnings
- fix missing updateAllFeed in ContactGiftingView page
- check that Anonymous users jdenticon are nulls (not "Anonymous")
- test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView) - test alerts on all pages -- or refactor to new "notify" (since AlertMessage refactoring may require a change, et. ContactQRScanShowView)
- .2 bug - on contacts view, click on "to" & "from" and nothing happens - .2 bug - on contacts view, click on "to" & "from" and nothing happens
- 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 add my bounding box(es) of interest for searches on Nearby part of Discovery page
- .5 search by a bounding box(s) of interest for local projects (see API by clicking on "Nearby")
- 01 Replace Gifted/Give in ContactsView with GiftedDialog 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
- 08 Scan QR code to import into contacts assignee:matthew - 08 Scan QR code to import into contacts assignee:matthew
- SEE: https://github.com/gruhn/vue-qrcode-reader - SEE: https://github.com/gruhn/vue-qrcode-reader
@ -24,6 +27,7 @@ tasks:
- 24 Move to Vite assignee:matthew - 24 Move to Vite assignee:matthew
- .2 Edit Plan does not have icons across the bottom assignee-group:ui
- .5 include the hash of the latest commit, and maybe a version - .5 include the hash of the latest commit, and maybe a version
- .5 add link to further project / people when a project pays ahead - .5 add link to further project / people when a project pays ahead
- .5 add project ID to the URL, to make a project publicly-accessible - .5 add project ID to the URL, to make a project publicly-accessible
@ -102,6 +106,8 @@ tasks:
- 40 notifications v+ : - 40 notifications v+ :
- pull, w/ scheduled runs - pull, w/ scheduled runs
- 01 On nearby search, if user starts changing their box but cancels and goes back to the map it is zoomed far out. Fix to fit the box better.
log: log:
- videos for multiple identities https://youtu.be/p8L87AeD76w and for adding time to contacts https://youtu.be/7Yylczevp10 done:2023-03-29 - 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 - project lists, contact totals & actions, multiple identifiers, stats-world, activity feed, rename of this project file (use "--follow --") milestone:2 done:2023-06-27

47
src/components/AlertMessage.vue

@ -1,47 +0,0 @@
<template>
<div v-bind:class="computedAlertClassNames()">
<button
class="close-button bg-amber-400 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-amber-200": 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>

2
src/components/EntityIcon.vue

@ -8,7 +8,7 @@ import { toSvg } from "jdenticon";
@Component @Component
export default class EntityIcon extends Vue { export default class EntityIcon extends Vue {
@Prop entityId = ""; @Prop entityId = "";
@Prop iconSize = ""; @Prop iconSize = 0;
generateIdenticon() { generateIdenticon() {
const svgString = toSvg(this.entityId, this.iconSize); const svgString = toSvg(this.entityId, this.iconSize);

12
src/components/GiftedDialog.vue

@ -52,18 +52,18 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop, Emit } from "vue-facing-decorator"; import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
import { GiverInputInfo, GiverOutputInfo } from "@/libs/endorserServer";
@Component @Component
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
@Prop message = ""; @Prop message = "";
giver = null; giver?: GiverInputInfo;
description = ""; description = "";
hours = "0"; hours = "0";
visible = false; visible = false;
open(giver) { open(giver: GiverInputInfo) {
// giver: GiverInputInfo
this.giver = giver; this.giver = giver;
this.visible = true; this.visible = true;
} }
@ -81,7 +81,7 @@ export default class GiftedDialog extends Vue {
} }
@Emit("dialog-result") @Emit("dialog-result")
confirm() { confirm(): GiverOutputInfo {
const result = { const result = {
action: "confirm", action: "confirm",
giver: this.giver, giver: this.giver,
@ -90,14 +90,14 @@ export default class GiftedDialog extends Vue {
}; };
this.close(); this.close();
this.description = ""; this.description = "";
this.giver = null; this.giver = undefined;
this.hours = "0"; this.hours = "0";
return result; return result;
} }
@Emit("dialog-result") @Emit("dialog-result")
cancel() { cancel(): GiverOutputInfo {
const result = { action: "cancel" }; const result = { action: "cancel" };
this.close(); this.close();
return result; return result;

11
src/db/tables/settings.ts

@ -1,3 +1,10 @@
export type BoundingBox = {
eastLong: number;
maxLat: number;
minLat: number;
westLong: number;
};
// 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
@ -7,6 +14,10 @@ export type Settings = {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
lastViewedClaimId?: string; lastViewedClaimId?: string;
searchBoxes?: Array<{
name: string;
bbox: BoundingBox;
}>;
showContactGivesInline?: boolean; showContactGivesInline?: boolean;
}; };

129
src/libs/endorserServer.ts

@ -21,6 +21,13 @@ export interface GiverInputInfo {
name?: string; name?: string;
} }
export interface GiverOutputInfo {
action: string;
giver?: GiverInputInfo;
description?: string;
hours?: number;
}
export interface ClaimResult { export interface ClaimResult {
success: { claimId: string; handleId: string }; success: { claimId: string; handleId: string };
error: { code: string; message: string }; error: { code: string; message: string };
@ -55,7 +62,30 @@ export interface GiveVerifiableCredential {
fulfills?: { "@type": string; identifier: 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 };
}
export interface PlanVerifiableCredential {
"@context": "https://schema.org";
"@type": "PlanAction";
name: string;
description: string;
identifier?: string;
location?: {
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
};
}
export interface PlanServerRecord {
agentDid?: string; // optional, if the issuer wants someone else to manage as well
description: string;
endTime?: string;
issuerDid: string;
handleId: string;
locLat?: number;
locLon?: number;
startTime?: string;
url?: string;
} }
export interface RegisterVerifiableCredential { export interface RegisterVerifiableCredential {
@ -63,7 +93,7 @@ export interface RegisterVerifiableCredential {
"@type": string; "@type": string;
agent: { identifier: string }; agent: { identifier: string };
object: string; object: string;
recipient: { identifier: string }; participant: { identifier: string };
} }
export interface InternalError { export interface InternalError {
@ -75,7 +105,7 @@ export interface InternalError {
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6 // See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN"; const HIDDEN_DID = "did:none:HIDDEN";
export function isHiddenDid(did) { export function isHiddenDid(did: string) {
return did === HIDDEN_DID; return did === HIDDEN_DID;
} }
@ -88,7 +118,7 @@ export function didInfo(
allMyDids: Array<string>, allMyDids: Array<string>,
contacts: Array<Contact>, contacts: Array<Contact>,
): string { ): string {
const myId: string | undefined = R.find(R.equals(did), allMyDids, did); const myId: string | undefined = R.find(R.equals(did), allMyDids);
if (myId) { if (myId) {
return "You" + (myId !== activeDid ? " (Alt ID)" : ""); return "You" + (myId !== activeDid ? " (Alt ID)" : "");
} else { } else {
@ -105,6 +135,22 @@ export function didInfo(
} }
} }
export interface ResultWithType {
type: string;
}
export interface SuccessResult extends ResultWithType {
type: "success";
response: AxiosResponse<ClaimResult>;
}
export interface ErrorResult {
type: "error";
error: InternalError;
}
export type CreateAndSubmitGiveResult = SuccessResult | ErrorResult;
/** /**
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim * For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
* *
@ -118,18 +164,21 @@ export async function createAndSubmitGive(
axios: Axios, axios: Axios,
apiServer: string, apiServer: string,
identity: IIdentifier, identity: IIdentifier,
fromDid: string, fromDid?: string,
toDid: string, toDid?: string,
description: string, description?: string,
hours: number, hours?: number,
fulfillsProjectHandleId?: string, fulfillsProjectHandleId?: string,
): Promise<AxiosResponse<ClaimResult> | InternalError> { ): Promise<CreateAndSubmitGiveResult> {
try {
// Make a claim // Make a claim
const vcClaim: GiveVerifiableCredential = { const vcClaim: GiveVerifiableCredential = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "GiveAction", "@type": "GiveAction",
recipient: { identifier: toDid },
}; };
if (toDid) {
vcClaim.recipient = { identifier: toDid };
}
if (fromDid) { if (fromDid) {
vcClaim.agent = { identifier: fromDid }; vcClaim.agent = { identifier: fromDid };
} }
@ -153,24 +202,30 @@ export async function createAndSubmitGive(
credentialSubject: vcClaim, credentialSubject: vcClaim,
}, },
}; };
// Create a signature using private key of identity // Create a signature using private key of identity
if (identity.keys[0].privateKeyHex == null) { const firstKey = identity.keys[0];
return new Promise<InternalError>((resolve, reject) => { if (!firstKey || !firstKey.privateKeyHex) {
reject({ throw {
error: "No private key", error: "No private key",
message: message: `Your identifier ${identity.did} is not configured correctly. Use a different identifier.`,
"Your identifier " + };
identity.did + }
" is not configured correctly. Use a different identifier.",
}); const privateKeyHex = firstKey.privateKeyHex;
});
if (!privateKeyHex) {
throw {
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 signer = await SimpleSigner(privateKeyHex);
const alg = undefined; const alg = undefined;
// Create a JWT for the request // Create a JWT for the request
const vcJwt: string = await didJwt.createJWT(vcPayload, { const vcJwt: string = await didJwt.createJWT(vcPayload, {
alg: alg, alg: alg,
issuer: identity.did, issuer: identity.did,
@ -187,7 +242,37 @@ export async function createAndSubmitGive(
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}; };
return axios.post(url, payload, { headers }); const response = await axios.post(url, payload, { headers });
return {
type: "success",
response,
};
} catch (error: unknown) {
let errorMessage: string;
if (error instanceof Error) {
// If it's a JavaScript Error object
errorMessage = error.message;
} else if (
typeof error === "object" &&
error !== null &&
"message" in error
) {
// If it's an object that has a 'message' property
errorMessage = (error as { message: string }).message;
} else {
// Unknown error shape, default message
errorMessage = "Unknown error";
}
return {
type: "error",
error: {
error: errorMessage,
userMessage: "Failed to create and submit the claim.",
},
};
}
} }
// from https://stackoverflow.com/a/175787/845494 // from https://stackoverflow.com/a/175787/845494

23
src/router/index.ts

@ -1,5 +1,11 @@
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; import {
import { accountsDB } from "@/db"; createRouter,
createWebHistory,
NavigationGuardNext,
RouteLocationNormalized,
RouteRecordRaw,
} from "vue-router";
import { accountsDB } from "@/db/index";
/** /**
* *
@ -7,7 +13,11 @@ import { accountsDB } from "@/db";
* @param from :RouteLocationNormalized * @param from :RouteLocationNormalized
* @param next :NavigationGuardNext * @param next :NavigationGuardNext
*/ */
const enterOrStart = async (to, from, next) => { const enterOrStart = async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
await accountsDB.open(); await accountsDB.open();
const num_accounts = await accountsDB.accounts.count(); const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) { if (num_accounts > 0) {
@ -190,7 +200,12 @@ const router = createRouter({
routes, routes,
}); });
const errorHandler = (error, to, from) => { const errorHandler = (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error: any,
to: RouteLocationNormalized,
from: RouteLocationNormalized,
) => {
// Handle the error here // Handle the error here
console.error("Caught in top level error handler:", error, to, from); console.error("Caught in top level error handler:", error, to, from);

104
src/views/AccountViewView.vue

@ -170,6 +170,33 @@
</button> </button>
</dialog> </dialog>
<div class="flex py-2">
<button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits
</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">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
registrations counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
</p>
</div>
</div>
<h3 <h3
class="text-sm uppercase font-semibold mb-3" class="text-sm uppercase font-semibold mb-3"
@click="showAdvanced = !showAdvanced" @click="showAdvanced = !showAdvanced"
@ -202,36 +229,6 @@
<div class="ml-2">Show amounts given with contacts</div> <div class="ml-2">Show amounts given with contacts</div>
</label> </label>
<div class="flex py-2">
<button
class="text-center text-md text-blue-500"
@click="checkLimits()"
>
Check Limits
</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">
<span class="font-bold">Rate Limits</span>
<p>
You have done {{ limits.doneClaimsThisWeek }} claims out of
{{ limits.maxClaimsPerWeek }} for this week. Your claims counter
resets at {{ readableTime(limits.nextWeekBeginDateTime) }}
</p>
<p>
You have done {{ limits.doneRegistrationsThisMonth }} registrations
out of {{ limits.maxRegistrationsPerMonth }} for this month. Your
registrations counter resets at
{{ readableTime(limits.nextMonthBeginDateTime) }}
</p>
</div>
</div>
<div class="flex py-2"> <div class="flex py-2">
<router-link <router-link
:to="{ name: 'identity-switcher' }" :to="{ name: 'identity-switcher' }"
@ -286,32 +283,37 @@
</button> </button>
</div> </div>
</div> </div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios/index";
import "dexie-export-import"; import "dexie-export-import";
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/index";
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 { AxiosError } from "axios/index"; import QuickNav from "@/components/QuickNav.vue";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
// 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;
@Component({ components: { AlertMessage, QuickNav } }) interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
Constants = AppString; Constants = AppString;
activeDid = ""; activeDid = "";
@ -337,7 +339,7 @@ export default class AccountViewView extends Vue {
alertMessage = ""; alertMessage = "";
alertTitle = ""; alertTitle = "";
public async getIdentity(activeDid) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@ -347,7 +349,7 @@ export default class AccountViewView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity) { public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -357,7 +359,7 @@ export default class AccountViewView extends Vue {
} }
// 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: string, fn: () => void) {
fn(); fn();
useClipboard() useClipboard()
.copy(text) .copy(text)
@ -409,7 +411,8 @@ export default class AccountViewView extends Vue {
}); });
this.checkLimitsFor(identity); this.checkLimitsFor(identity);
} }
} catch (err) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
if ( if (
err.message === err.message ===
"Attempted to load account records with no identity available." "Attempted to load account records with no identity available."
@ -512,7 +515,8 @@ export default class AccountViewView extends Vue {
if (resp.status === 200) { if (resp.status === 200) {
this.limits = resp.data; this.limits = resp.data;
} }
} catch (error: unknown) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if ( if (
error.message === error.message ===
"Attempted to load Give records with no identity available." "Attempted to load Give records with no identity available."
@ -523,13 +527,9 @@ export default class AccountViewView extends Vue {
const serverError = error as AxiosError; const serverError = error as AxiosError;
console.error("Bad response retrieving limits: ", serverError); console.error("Bad response retrieving limits: ", serverError);
const data: ErrorResponse | undefined = const data = (serverError.response &&
serverError.response && serverError.response.data; serverError.response.data) as ErrorResponse;
if (data && data.error && data.error.message) { this.limitsMessage = data?.error?.message || "Bad server response.";
this.limitsMessage = data.error.message;
} else {
this.limitsMessage = "Bad server response.";
}
} }
} }
@ -579,7 +579,7 @@ export default class AccountViewView extends Vue {
this.apiServer = this.apiServerInput; this.apiServer = this.apiServerInput;
} }
setApiServerInput(value) { setApiServerInput(value: string) {
this.apiServerInput = value; this.apiServerInput = value;
} }
} }

32
src/views/ContactAmountsView.vue

@ -90,10 +90,6 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
@ -101,7 +97,7 @@
import * as R from "ramda"; import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db/index";
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";
@ -113,17 +109,24 @@ 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"; import QuickNav from "@/components/QuickNav.vue";
import QuickNav from "@/components/QuickNav"; import { IIdentifier } from "@veramo/core";
@Component({ components: { AlertMessage, QuickNav } }) interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contact: Contact | null = null; contact: Contact | null = null;
giveRecords: Array<GiveServerRecord> = []; giveRecords: Array<GiveServerRecord> = [];
alertTitle = "";
alertMessage = "";
numAccounts = 0; numAccounts = 0;
async beforeCreate() { async beforeCreate() {
@ -131,7 +134,7 @@ export default class ContactsView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public async getIdentity(activeDid) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@ -147,7 +150,7 @@ export default class ContactsView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity) { public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -169,7 +172,8 @@ export default class ContactsView extends Vue {
if (this.activeDid && this.contact) { if (this.activeDid && this.contact) {
this.loadGives(this.activeDid, this.contact); this.loadGives(this.activeDid, this.contact);
} }
} catch (err) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -187,7 +191,7 @@ export default class ContactsView extends Vue {
async loadGives(activeDid: string, contact: Contact) { async loadGives(activeDid: string, contact: Contact) {
try { try {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
let result = []; let result: Array<GiveServerRecord> = [];
const url = const url =
this.apiServer + this.apiServer +
"/api/v2/report/gives?agentDid=" + "/api/v2/report/gives?agentDid=" +

130
src/views/ContactGiftingView.vue

@ -76,48 +76,53 @@
message="Received from" message="Received from"
> >
</GiftedDialog> </GiftedDialog>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts"; 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 { createAndSubmitGive } from "@/libs/endorserServer"; import {
import { Account } from "@/db/tables/accounts"; createAndSubmitGive,
CreateAndSubmitGiveResult,
ErrorResult,
GiverInputInfo,
GiverOutputInfo,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage"; import QuickNav from "@/components/QuickNav.vue";
import QuickNav from "@/components/QuickNav"; import EntityIcon from "@/components/EntityIcon.vue";
import EntityIcon from "@/components/EntityIcon"; import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon },
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
allAccounts: Array<Account> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
apiServer = ""; apiServer = "";
isHiddenSpinner = true; accounts: typeof AccountsSchema;
alertTitle = "";
alertMessage = "";
accounts: AccountsSchema;
numAccounts = 0; numAccounts = 0;
async beforeCreate() { async beforeCreate() {
accountsDB.open(); accountsDB.open();
this.accounts = accountsDB.accounts; this.numAccounts = await accountsDB.accounts.count();
this.numAccounts = await this.accounts.count();
} }
public async getIdentity(activeDid) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@ -133,7 +138,7 @@ export default class HomeView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity) { public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -144,23 +149,20 @@ export default class HomeView extends Vue {
async created() { async created() {
try { try {
await accountsDB.open();
this.allAccounts = await accountsDB.accounts.toArray();
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId; // eslint-disable-next-line @typescript-eslint/no-explicit-any
this.updateAllFeed(); } catch (err: any) {
} catch (err) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text:
err.userMessage || err.message ||
"There was an error retrieving the latest sweet, sweet action.", "There was an error retrieving the latest sweet, sweet action.",
}, },
-1, -1,
@ -168,37 +170,20 @@ export default class HomeView extends Vue {
} }
} }
public async buildHeaders() { openDialog(giver: GiverInputInfo) {
const headers = { "Content-Type": "application/json" }; (this.$refs.customDialog as GiftedDialog).open(giver);
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)); handleDialogResult(result: GiverOutputInfo) {
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
openDialog(giver) {
this.$refs.customDialog.open(giver);
}
handleDialogResult(result) {
if (result.action === "confirm") { if (result.action === "confirm") {
return new Promise((resolve) => { return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours); this.recordGive(
resolve(); result.giver?.did,
result.description,
result.hours,
).then(() => {
resolve(null);
});
}); });
} else { } else {
// action was "cancel" so do nothing // action was "cancel" so do nothing
@ -211,7 +196,11 @@ export default class HomeView extends Vue {
* @param description may be an empty string * @param description may be an empty string
* @param hours may be 0 * @param hours may be 0
*/ */
public async recordGive(giverDid, description, hours) { public async recordGive(
giverDid?: string,
description?: string,
hours?: number,
) {
if (!this.activeDid) { if (!this.activeDid) {
this.$notify( this.$notify(
{ {
@ -250,8 +239,8 @@ export default class HomeView extends Vue {
hours, hours,
); );
if (isGiveCreationError(result)) { if (this.isGiveCreationError(result)) {
const errorMessage = getGiveCreationErrorMessage(result); const errorMessage = this.getGiveCreationErrorMessage(result);
console.log("Error with give result:", result); console.log("Error with give result:", result);
this.$notify( this.$notify(
{ {
@ -273,39 +262,34 @@ export default class HomeView extends Vue {
-1, -1,
); );
} }
} catch (error) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log("Error with give caught:", error); console.log("Error with give caught:", error);
const message =
error.userMessage ||
error.response?.data?.error?.message ||
"There was an error recording the Give.";
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text: message,
getGiveErrorMessage(error) ||
"There was an error recording the give.",
}, },
-1, -1,
); );
} }
} }
private setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
}
// Helper functions for readability // Helper functions for readability
isGiveCreationError(result) { isGiveCreationError(result: CreateAndSubmitGiveResult) {
return result.status !== 201 || result.data?.error; return result.type == "error";
}
getGiveCreationErrorMessage(result) {
return result.data?.error?.message;
} }
getGiveErrorMessage(error) { getGiveCreationErrorMessage(result: CreateAndSubmitGiveResult) {
return error.userMessage || error.response?.data?.error?.message; return (result as ErrorResult).error?.userMessage;
} }
} }
</script> </script>

15
src/views/ContactQRScanShowView.vue

@ -23,17 +23,24 @@
<script lang="ts"> <script lang="ts">
import QRCodeVue3 from "qr-code-generator-vue3"; import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; 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 QuickNav from "@/components/QuickNav"; import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
// 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;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { components: {
QRCodeVue3, QRCodeVue3,
@ -41,11 +48,13 @@ const Buffer = require("buffer/").Buffer;
}, },
}) })
export default class ContactQRScanShow extends Vue { export default class ContactQRScanShow extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
qrValue = ""; qrValue = "";
public async getIdentity(activeDid) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const account: Account | undefined = R.find( const account: Account | undefined = R.find(

88
src/views/ContactsView.vue

@ -121,19 +121,21 @@
</button> </button>
<button <button
v-if="contact.registered"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="Registered"
>
<fa icon="person-circle-check" class="fa-fw" />
</button>
<button
v-else
@click="register(contact)" @click="register(contact)"
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md" class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
title="Registration unknown"
> >
<fa icon="person-circle-question" class="fa-fw" /> <fa
v-if="contact.registered"
icon="person-circle-check"
class="fa-fw"
title="Registered"
/>
<fa
v-else
icon="person-circle-question"
class="fa-fw"
title="Registration Unknown"
/>
</button> </button>
<button <button
@ -202,10 +204,6 @@
</li> </li>
</ul> </ul>
<p v-else>This identity has no contacts.</p> <p v-else>This identity has no contacts.</p>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
@ -214,27 +212,36 @@ 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 { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db/index";
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 { Component, Vue } from "vue-facing-decorator";
import AlertMessage from "@/components/AlertMessage"; import QuickNav from "@/components/QuickNav.vue";
import QuickNav from "@/components/QuickNav"; import EntityIcon from "@/components/EntityIcon.vue";
import EntityIcon from "@/components/EntityIcon";
// 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;
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { AlertMessage, QuickNav, EntityIcon }, components: { QuickNav, EntityIcon },
}) })
export default class ContactsView extends Vue { export default class ContactsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
@ -256,8 +263,6 @@ export default class ContactsView extends Vue {
showGiveNumbers = false; showGiveNumbers = false;
showGiveTotals = true; showGiveTotals = true;
showGiveConfirmed = true; showGiveConfirmed = true;
alertTitle = "";
alertMessage = "";
async created() { async created() {
await db.open(); await db.open();
@ -276,7 +281,7 @@ export default class ContactsView extends Vue {
); );
} }
public async getIdentity(activeDid) { public async getIdentity(activeDid: string) {
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);
@ -290,7 +295,7 @@ export default class ContactsView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity) { public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -299,7 +304,7 @@ export default class ContactsView extends Vue {
return headers; return headers;
} }
public async getHeadersAndIdentity(activeDid) { public async getHeadersAndIdentity(activeDid: string) {
const identity = await this.getIdentity(activeDid); const identity = await this.getIdentity(activeDid);
const headers = await this.getHeaders(identity); const headers = await this.getHeaders(identity);
@ -308,11 +313,11 @@ export default class ContactsView extends Vue {
async loadGives() { async loadGives() {
const handleResponse = ( const handleResponse = (
resp, resp: { status: number; data: { data: GiveServerRecord[] } },
descriptions, descriptions: Record<string, string>,
confirmed, confirmed: Record<string, number>,
unconfirmed, unconfirmed: Record<string, number>,
useRecipient, useRecipient: boolean,
) => { ) => {
if (resp.status === 200) { if (resp.status === 200) {
const allData = resp.data.data; const allData = resp.data.data;
@ -344,9 +349,8 @@ export default class ContactsView extends Vue {
title: "Error With Server", title: "Error With Server",
text: text:
"Got an error retrieving your " + "Got an error retrieving your " +
resp.config.url.includes("recipientDid") (useRecipient ? "given" : "received") +
? "received" " time from the server.",
: "given" + " time from the server.",
}, },
-1, -1,
); );
@ -457,6 +461,9 @@ 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) +
(contact.registered
? " -- especially since they are already marked as registered"
: "") +
"?", "?",
) )
) { ) {
@ -608,6 +615,8 @@ export default class ContactsView extends Vue {
this.apiServer + this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" + "/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did); encodeURIComponent(contact.did);
const identity = await this.getIdentity(this.activeDid);
const headers = await this.getHeaders(identity);
try { try {
const resp = await this.axios.get(url, { headers }); const resp = await this.axios.get(url, { headers });
@ -685,11 +694,17 @@ export default class ContactsView extends Vue {
// 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) {
const isare = this.givenToMeUnconfirmed[fromDid] == 1 ? "is" : "are";
const hours = this.givenToMeUnconfirmed[fromDid] == 1 ? "hour" : "hours";
if ( if (
confirm( confirm(
"There are " + "There " +
isare +
" " +
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?",
) )
) { ) {
@ -697,6 +712,7 @@ export default class ContactsView extends Vue {
name: "contact-amounts", name: "contact-amounts",
query: { contactDid: fromDid }, query: { contactDid: fromDid },
}); });
return;
} }
} }
if (!this.isNumeric(this.hourInput)) { if (!this.isNumeric(this.hourInput)) {
@ -747,7 +763,9 @@ export default class ContactsView extends Vue {
confirm( confirm(
"Are you sure you want to record " + "Are you sure you want to record " +
this.hourInput + this.hourInput +
" hours " + " hour" +
(this.hourInput == "1" ? "" : "s") +
" " +
toFrom + toFrom +
description + description +
"?", "?",

315
src/views/DiscoverView.vue

@ -9,7 +9,7 @@
</h1> </h1>
<!-- Quick Search --> <!-- Quick Search -->
<div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="search()"> <div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchAll()">
<input <input
type="text" type="text"
v-model="searchTerms" v-model="searchTerms"
@ -17,7 +17,7 @@
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()" @click="searchAll()"
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>
@ -32,6 +32,8 @@
href="#" href="#"
@click=" @click="
projects = []; projects = [];
isLocalActive = true;
isRemoteActive = false;
searchLocal(); searchLocal();
" "
v-bind:class="computedLocalTabClassNames()" v-bind:class="computedLocalTabClassNames()"
@ -49,7 +51,9 @@
v-bind:class="computedRemoteTabClassNames()" v-bind:class="computedRemoteTabClassNames()"
@click=" @click="
projects = []; projects = [];
search(); isRemoteActive = true;
isLocalActive = false;
searchAll();
" "
> >
Remote Remote
@ -62,6 +66,49 @@
</ul> </ul>
</div> </div>
<div v-if="isLocalActive">
<div v-if="!isChoosingSearchBox">
<button
class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="isChoosingSearchBox = true"
>
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
</button>
</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>
<!-- Loading Animation --> <!-- Loading Animation -->
<div <div
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="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
@ -71,7 +118,7 @@
</div> </div>
<!-- Results List --> <!-- Results List -->
<InfiniteScroll @reached-bottom="loadMoreData"> <InfiniteScroll @reached-bottom="loadMoreData" v-if="!isChoosingSearchBox">
<ul> <ul>
<li <li
class="border-b border-slate-300" class="border-b border-slate-300"
@ -103,42 +150,103 @@
</li> </li>
</ul> </ul>
</InfiniteScroll> </InfiniteScroll>
<AlertMessage
:alertTitle="alertTitle" <div
:alertMessage="alertMessage" v-if="isLocalActive && isChoosingSearchBox"
></AlertMessage> 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 {
import { accountsDB, db } from "@/db"; LMap,
LMarker,
LRectangle,
LTileLayer,
} from "@vue-leaflet/vue-leaflet";
import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { didInfo } from "@/libs/endorserServer"; import { didInfo, ProjectData } from "@/libs/endorserServer";
import AlertMessage from "@/components/AlertMessage"; import QuickNav from "@/components/QuickNav.vue";
import QuickNav from "@/components/QuickNav"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
import InfiniteScroll from "@/components/InfiniteScroll"; import EntityIcon from "@/components/EntityIcon.vue";
import EntityIcon from "@/components/EntityIcon";
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({ @Component({
components: { AlertMessage, QuickNav, InfiniteScroll, EntityIcon }, components: {
LRectangle,
QuickNav,
InfiniteScroll,
EntityIcon,
LMap,
LMarker,
LTileLayer,
},
}) })
export default class DiscoverView extends Vue { export default class DiscoverView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
searchTerms = ""; searchTerms = "";
alertMessage = "";
alertTitle = "";
projects: ProjectData[] = []; projects: ProjectData[] = [];
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;
isLoading = false; isLoading = false;
// make this function available to the Vue template // make this function available to the Vue template
@ -149,6 +257,9 @@ export default class DiscoverView extends Vue {
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 || "";
this.searchBox = settings?.searchBoxes?.[0] || null;
this.resetLatLong();
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
await accountsDB.open(); await accountsDB.open();
@ -158,8 +269,10 @@ export default class DiscoverView extends Vue {
this.searchLocal(); this.searchLocal();
} }
public async buildHeaders() { public async buildHeaders(): Promise<HeadersInit> {
const headers = { "Content-Type": "application/json" }; const headers: HeadersInit = {
"Content-Type": "application/json",
};
if (this.activeDid) { if (this.activeDid) {
await accountsDB.open(); await accountsDB.open();
@ -180,16 +293,13 @@ export default class DiscoverView extends Vue {
return headers; return headers;
} }
public async search(beforeId?: string) { public async searchAll(beforeId?: string) {
let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms); let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms);
if (beforeId) { if (beforeId) {
queryParams = queryParams + `&beforeId=${beforeId}`; queryParams = queryParams + `&beforeId=${beforeId}`;
} }
this.isRemoteActive = true;
this.isLocalActive = false;
try { try {
this.isLoading = true; this.isLoading = true;
const response = await fetch( const response = await fetch(
@ -202,12 +312,13 @@ export default class DiscoverView extends Vue {
if (response.status !== 200) { if (response.status !== 200) {
const details = await response.text(); const details = await response.text();
console.log("Problem with full search:", details);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: `There was a problem accessing the server. Please try again later. (${details})`, text: `There was a problem accessing the server. Try again later.`,
}, },
-1, -1,
); );
@ -220,14 +331,15 @@ export default class DiscoverView extends Vue {
const plans: ProjectData[] = results.data; const plans: ProjectData[] = results.data;
if (plans) { if (plans) {
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId, rowid, issuerDid } = plan; const { name, description, handleId, rowid } = plan;
this.projects.push({ name, description, handleId, rowid, issuerDid }); this.projects.push({ name, description, handleId, rowid });
} }
this.remoteCount = this.projects.length; this.remoteCount = this.projects.length;
} else { } else {
throw JSON.stringify(results); throw JSON.stringify(results);
} }
} catch (e) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
console.log("Error with feed load:", e); console.log("Error with feed load:", e);
this.$notify( this.$notify(
{ {
@ -244,14 +356,19 @@ export default class DiscoverView extends Vue {
} }
public async searchLocal(beforeId?: string) { public async searchLocal(beforeId?: string) {
if (!this.searchBox) {
this.projects = [];
return;
}
const claimContents = const claimContents =
"claimContents=" + encodeURIComponent(this.searchTerms); "claimContents=" + encodeURIComponent(this.searchTerms);
let queryParams = [ let queryParams = [
claimContents, claimContents,
"minLocLat=40.901000", "minLocLat=" + this.searchBox.bbox.minLat,
"maxLocLat=40.904000", "maxLocLat=" + this.searchBox.bbox.maxLat,
"westLocLon=-111.914000", "westLocLon=" + this.searchBox.bbox.westLong,
"eastLocLon=-111.909000", "eastLocLon=" + this.searchBox.bbox.eastLong,
].join("&"); ].join("&");
if (beforeId) { if (beforeId) {
@ -260,8 +377,6 @@ export default class DiscoverView extends Vue {
try { try {
this.isLoading = true; this.isLoading = true;
this.isLocalActive = true;
this.isRemoteActive = false;
const response = await fetch( const response = await fetch(
this.apiServer + "/api/v2/report/plansByLocation?" + queryParams, this.apiServer + "/api/v2/report/plansByLocation?" + queryParams,
{ {
@ -272,12 +387,13 @@ export default class DiscoverView extends Vue {
if (response.status !== 200) { if (response.status !== 200) {
const details = await response.text(); const details = await response.text();
console.log("Problem with nearby search:", details);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: `There was a problem accessing the server. Please try again later. (${details})`, text: "There was a problem accessing the server. Try again later.",
}, },
-1, -1,
); );
@ -302,7 +418,8 @@ export default class DiscoverView extends Vue {
} else { } else {
throw JSON.stringify(results); throw JSON.stringify(results);
} }
} catch (e) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
console.log("Error with feed load:", e); console.log("Error with feed load:", e);
this.$notify( this.$notify(
{ {
@ -328,7 +445,7 @@ export default class DiscoverView extends Vue {
if (this.isLocalActive) { if (this.isLocalActive) {
this.searchLocal(latestProject["rowid"]); this.searchLocal(latestProject["rowid"]);
} else if (this.isRemoteActive) { } else if (this.isRemoteActive) {
this.search(latestProject["rowid"]); this.searchAll(latestProject["rowid"]);
} }
} }
} }
@ -345,6 +462,128 @@ 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,

2
src/views/HelpView.vue

@ -181,7 +181,7 @@
<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"; import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class Help extends Vue { export default class Help extends Vue {

119
src/views/HomeView.vue

@ -196,40 +196,49 @@
</li> </li>
</ul> </ul>
</div> </div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db"; import { db, accountsDB } from "@/db/index";
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 { createAndSubmitGive, didInfo } from "@/libs/endorserServer"; import {
createAndSubmitGive,
didInfo,
GiverInputInfo,
GiverOutputInfo,
GiveServerRecord,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage"; import QuickNav from "@/components/QuickNav.vue";
import QuickNav from "@/components/QuickNav"; import EntityIcon from "@/components/EntityIcon.vue";
import EntityIcon from "@/components/EntityIcon"; import { IIdentifier } from "@veramo/core";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon },
}) })
export default class HomeView extends Vue { export default class HomeView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
feedAllLoaded = false; feedAllLoaded = false;
feedData = []; feedData = [];
feedPreviousOldestId = null; feedPreviousOldestId?: string;
feedLastViewedId = null; feedLastViewedId?: string;
isHiddenSpinner = true; isHiddenSpinner = true;
alertTitle = "";
alertMessage = "";
numAccounts = 0; numAccounts = 0;
async beforeCreate() { async beforeCreate() {
@ -237,7 +246,7 @@ export default class HomeView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public async getIdentity(activeDid) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@ -253,7 +262,7 @@ export default class HomeView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity) { public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -275,7 +284,8 @@ export default class HomeView extends Vue {
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId; this.feedLastViewedId = settings?.lastViewedClaimId;
this.updateAllFeed(); this.updateAllFeed();
} catch (err) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -291,7 +301,9 @@ export default class HomeView extends Vue {
} }
public async buildHeaders() { public async buildHeaders() {
const headers = { "Content-Type": "application/json" }; const headers: HeadersInit = {
"Content-Type": "application/json",
};
if (this.activeDid) { if (this.activeDid) {
await accountsDB.open(); await accountsDB.open();
@ -314,7 +326,7 @@ export default class HomeView extends Vue {
public async updateAllFeed() { public async updateAllFeed() {
this.isHiddenSpinner = false; this.isHiddenSpinner = false;
await this.retrieveClaims(this.apiServer, null, this.feedPreviousOldestId) await this.retrieveClaims(this.apiServer, this.feedPreviousOldestId)
.then(async (results) => { .then(async (results) => {
if (results.data.length > 0) { if (results.data.length > 0) {
this.feedData = this.feedData.concat(results.data); this.feedData = this.feedData.concat(results.data);
@ -349,7 +361,7 @@ export default class HomeView extends Vue {
this.isHiddenSpinner = true; this.isHiddenSpinner = true;
} }
public async retrieveClaims(endorserApiServer, identifier, beforeId) { public async retrieveClaims(endorserApiServer: string, beforeId?: string) {
const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId; const beforeQuery = beforeId == null ? "" : "&beforeId=" + beforeId;
const response = await fetch( const response = await fetch(
endorserApiServer + "/api/v2/report/gives?" + beforeQuery, endorserApiServer + "/api/v2/report/gives?" + beforeQuery,
@ -372,13 +384,13 @@ export default class HomeView extends Vue {
} }
} }
giveDescription(giveRecord) { giveDescription(giveRecord: GiveServerRecord) {
let claim = giveRecord.fullClaim; // claim.claim happen for some claims wrapped in a Verifiable Credential
if (claim.claim) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
claim = claim.claim; const claim = (giveRecord.fullClaim as any).claim || giveRecord.fullClaim;
}
// agent.did is for legacy data, before March 2023 // agent.did is for legacy data, before March 2023
const giverDid = claim.agent?.identifier || claim.agent?.did; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const giverDid = claim.agent?.identifier || (claim.agent as any)?.did;
const giverInfo = didInfo( const giverInfo = didInfo(
giverDid, giverDid,
this.activeDid, this.activeDid,
@ -389,7 +401,9 @@ export default class HomeView extends Vue {
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood) ? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: claim.description || "something unknown"; : claim.description || "something unknown";
// recipient.did is for legacy data, before March 2023 // recipient.did is for legacy data, before March 2023
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did; const gaveRecipientId =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
claim.recipient?.identifier || (claim.recipient as any)?.did;
const gaveRecipientInfo = gaveRecipientId const gaveRecipientInfo = gaveRecipientId
? " to " + ? " to " +
didInfo( didInfo(
@ -402,23 +416,28 @@ export default class HomeView extends Vue {
return giverInfo + " gave " + gaveAmount + gaveRecipientInfo; return giverInfo + " gave " + gaveAmount + gaveRecipientInfo;
} }
displayAmount(code, amt) { displayAmount(code: string, amt: number) {
return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1); return "" + amt + " " + this.currencyShortWordForCode(code, amt === 1);
} }
currencyShortWordForCode(unitCode, single) { currencyShortWordForCode(unitCode: string, single: boolean) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
} }
openDialog(giver) { openDialog(giver: GiverInputInfo) {
this.$refs.customDialog.open(giver); (this.$refs.customDialog as GiftedDialog).open(giver);
} }
handleDialogResult(result) { handleDialogResult(result: GiverOutputInfo) {
if (result.action === "confirm") { if (result.action === "confirm") {
return new Promise((resolve) => { return new Promise((resolve) => {
this.recordGive(result.giver?.did, result.description, result.hours); this.recordGive(
resolve(); result.giver?.did,
result.description,
result.hours,
).then(() => {
resolve(null);
});
}); });
} else { } else {
// action was "cancel" so do nothing // action was "cancel" so do nothing
@ -431,7 +450,11 @@ export default class HomeView extends Vue {
* @param description may be an empty string * @param description may be an empty string
* @param hours may be 0 * @param hours may be 0
*/ */
public async recordGive(giverDid, description, hours) { public async recordGive(
giverDid?: string,
description?: string,
hours?: number,
) {
if (!this.activeDid) { if (!this.activeDid) {
this.$notify( this.$notify(
{ {
@ -493,39 +516,35 @@ export default class HomeView extends Vue {
-1, -1,
); );
} }
} catch (error) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.log("Error with give caught:", error); console.log("Error with give caught:", error);
const message =
error.userMessage ||
error.response?.data?.error?.message ||
"There was an error recording the give.";
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text: message,
this.getGiveErrorMessage(error) ||
"There was an error recording the give.",
}, },
-1, -1,
); );
} }
} }
private setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
}
// Helper functions for readability // Helper functions for readability
isGiveCreationError(result) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error; return result.status !== 201 || result.data?.error;
} }
getGiveCreationErrorMessage(result) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
getGiveCreationErrorMessage(result: any) {
return result.data?.error?.message; return result.data?.error?.message;
} }
getGiveErrorMessage(error) {
return error.userMessage || error.response?.data?.error?.message;
}
} }
</script> </script>

57
src/views/IdentitySwitcherView.vue

@ -62,34 +62,38 @@
> >
No Identity No Identity
</a> </a>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts"; import { AccountsSchema } from "@/db/tables/accounts";
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.vue";
import QuickNav from "@/components/QuickNav";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { AlertMessage, QuickNav } }) @Component({ components: { QuickNav } })
export default class IdentitySwitcherView extends Vue { export default class IdentitySwitcherView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
Constants = AppString; Constants = AppString;
public accounts: AccountsSchema; public accounts: typeof AccountsSchema;
public activeDid; public activeDid = "";
public firstName; public apiServer = "";
public lastName; public apiServerInput = "";
public alertTitle; public firstName = "";
public alertMessage; public lastName = "";
public otherIdentities = []; public otherIdentities: Array<{ did: string }> = [];
public showContactGives = false;
public async getIdentity(activeDid) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@ -126,31 +130,20 @@ export default class IdentitySwitcherView extends Vue {
} }
} }
} catch (err) { } catch (err) {
if (
err.message ===
"Attempted to load account records with no identity available."
) {
this.limitsMessage = "No identity.";
this.loadingLimits = false;
} else {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error Creating Account", title: "Error Loading Accounts",
text: "Clear your cache and start over (after data backup).", text: "Clear your cache and start over (after data backup).",
}, },
-1, -1,
); );
console.error( console.error("Telling user to clear cache at page create because:", err);
"Telling user to clear cache at page create because:",
err,
);
}
} }
} }
async switchAccount(did: string) { async switchAccount(did?: string) {
// 0 means none // 0 means none
if (did === "0") { if (did === "0") {
did = undefined; did = undefined;
@ -159,7 +152,7 @@ export default class IdentitySwitcherView extends Vue {
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did, activeDid: did,
}); });
this.activeDid = did; this.activeDid = did || "";
this.otherIdentities = []; this.otherIdentities = [];
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();

2
src/views/ImportAccountView.vue

@ -67,7 +67,7 @@ import {
deriveAddress, deriveAddress,
newIdentifier, newIdentifier,
} from "../libs/crypto"; } from "../libs/crypto";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({ @Component({

12
src/views/ImportDerivedAccountView.vue

@ -73,7 +73,7 @@ import {
deriveAddress, deriveAddress,
newIdentifier, newIdentifier,
} from "../libs/crypto"; } from "../libs/crypto";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({ @Component({
@ -87,9 +87,9 @@ export default class ImportAccountView extends Vue {
async mounted() { async mounted() {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
const seedDids = {}; const seedDids: Record<string, Array<string>> = {};
accounts.forEach((account) => { accounts.forEach((account) => {
const prevDids = seedDids[account.mnemonic] || []; const prevDids: Array<string> = seedDids[account.mnemonic] || [];
seedDids[account.mnemonic] = prevDids.concat([account.did]); seedDids[account.mnemonic] = prevDids.concat([account.did]);
}); });
this.didArrays = Object.values(seedDids); this.didArrays = Object.values(seedDids);
@ -107,9 +107,9 @@ export default class ImportAccountView extends Vue {
public async incrementDerivation() { public async incrementDerivation() {
await accountsDB.open(); await accountsDB.open();
// find the maximum derivation path for the selected DIDs // find the maximum derivation path for the selected DIDs
const selectedArray: Array<string> = this.didArrays.find( const selectedArray: Array<string> =
(dids) => dids[0] === this.selectedArrayFirstDid, this.didArrays.find((dids) => dids[0] === this.selectedArrayFirstDid) ||
); [];
const allMatchingAccounts = await accountsDB.accounts const allMatchingAccounts = await accountsDB.accounts
.where("did") .where("did")
.anyOf(...selectedArray) .anyOf(...selectedArray)

2
src/views/NewEditAccountView.vue

@ -49,7 +49,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { db } from "@/db"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
@Component({ @Component({

43
src/views/NewEditProjectView.vue

@ -97,10 +97,6 @@
Cancel Cancel
</button> </button>
</div> </div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
@ -111,20 +107,27 @@ import * as didJwt from "did-jwt";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet"; import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db/index";
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"; import { PlanVerifiableCredential } from "@/libs/endorserServer";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { AlertMessage, LMap, LMarker, LTileLayer }, components: { LMap, LMarker, LTileLayer },
}) })
export default class NewEditProjectView extends Vue { export default class NewEditProjectView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
alertTitle = "";
alertMessage = "";
apiServer = ""; apiServer = "";
description = ""; description = "";
errorMessage = ""; errorMessage = "";
@ -140,7 +143,7 @@ export default class NewEditProjectView extends Vue {
this.numAccounts = await accountsDB.accounts.count(); this.numAccounts = await accountsDB.accounts.count();
} }
public async getIdentity(activeDid) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@ -156,7 +159,7 @@ export default class NewEditProjectView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity) { public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -215,7 +218,7 @@ export default class NewEditProjectView extends Vue {
private async SaveProject(identity: IIdentifier) { private async SaveProject(identity: IIdentifier) {
// Make a claim // Make a claim
const vcClaim: VerifiableCredential = { const vcClaim: PlanVerifiableCredential = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "PlanAction", "@type": "PlanAction",
name: this.projectName, name: this.projectName,
@ -270,19 +273,15 @@ export default class NewEditProjectView extends Vue {
// version shows up here: https://api.endorser.ch/api-docs/ // version shows up here: https://api.endorser.ch/api-docs/
if (resp.data?.success?.handleId || resp.data?.success?.fullIri) { if (resp.data?.success?.handleId || resp.data?.success?.fullIri) {
this.errorMessage = ""; this.errorMessage = "";
this.alertTitle = "";
this.alertMessage = "";
// 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://api.endorser.ch/api-docs/ // version shows up here: https://api.endorser.ch/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: NewEditProjectView) {
const route = { that.$router.push({ name: "project" });
name: "project",
};
that.$router.push(route);
}, },
2000, 2000,
this, this,
@ -290,11 +289,13 @@ export default class NewEditProjectView extends Vue {
} }
} catch (error) { } catch (error) {
let userMessage = "There was an error saving the project."; let userMessage = "There was an error saving the project.";
const serverError = error as AxiosError; const serverError = error as AxiosError<{
error?: { message?: string };
}>;
if (serverError) { if (serverError) {
if (Object.prototype.hasOwnProperty.call(serverError, "message")) { if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
console.log(serverError); console.log(serverError);
userMessage = serverError.response.data.error.message; // This is info for the user. userMessage = serverError.response?.data?.error?.message || ""; // This is info for the user.
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

4
src/views/NewIdentifierView.vue

@ -40,10 +40,10 @@
<script lang="ts"> <script lang="ts">
import "dexie-export-import"; import "dexie-export-import";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db/index";
import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import QuickNav from "@/components/QuickNav"; import QuickNav from "@/components/QuickNav.vue";
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {

99
src/views/ProjectViewView.vue

@ -200,40 +200,45 @@
message="Received from" message="Received from"
> >
</GiftedDialog> </GiftedDialog>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios"; import { AxiosError, RawAxiosRequestHeaders } from "axios";
import * as moment from "moment"; import * as moment from "moment";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db/index";
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 } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
GiverInputInfo,
GiverOutputInfo,
GiveServerRecord, GiveServerRecord,
ResultWithType,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import AlertMessage from "@/components/AlertMessage"; import QuickNav from "@/components/QuickNav.vue";
import QuickNav from "@/components/QuickNav"; import EntityIcon from "@/components/EntityIcon.vue";
import EntityIcon from "@/components/EntityIcon";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { GiftedDialog, AlertMessage, QuickNav, EntityIcon }, components: { GiftedDialog, QuickNav, EntityIcon },
}) })
export default class ProjectViewView extends Vue { export default class ProjectViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
activeDid = ""; activeDid = "";
alertMessage = "";
alertTitle = "";
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
apiServer = ""; apiServer = "";
@ -266,7 +271,7 @@ export default class ProjectViewView extends Vue {
this.LoadProject(identity); this.LoadProject(identity);
} }
public async getIdentity(activeDid) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")
@ -282,7 +287,7 @@ export default class ProjectViewView extends Vue {
return identity; return identity;
} }
public async getHeaders(identity) { public async getHeaders(identity: IIdentifier) {
const token = await accessToken(identity); const token = await accessToken(identity);
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@ -300,7 +305,12 @@ export default class ProjectViewView extends Vue {
} }
// Isn't there a better way to make this available to the template? // Isn't there a better way to make this available to the template?
didInfo(did, activeDid, dids, contacts) { didInfo(
did: string,
activeDid: string,
dids: Array<string>,
contacts: Array<Contact>,
) {
return didInfo(did, activeDid, dids, contacts); return didInfo(did, activeDid, dids, contacts);
} }
@ -317,7 +327,7 @@ export default class ProjectViewView extends Vue {
this.apiServer + this.apiServer +
"/api/claim/byHandle/" + "/api/claim/byHandle/" +
encodeURIComponent(this.projectId); encodeURIComponent(this.projectId);
const headers = { const headers: RawAxiosRequestHeaders = {
"Content-Type": "application/json", "Content-Type": "application/json",
}; };
if (identity) { if (identity) {
@ -451,8 +461,9 @@ export default class ProjectViewView extends Vue {
} }
} }
openDialog(contact) { openDialog(contact: GiverInputInfo) {
this.$refs.customDialog.open(contact); const dialog: GiftedDialog = this.$refs.customDialog as GiftedDialog;
dialog.open(contact);
} }
getOpenStreetMapUrl() { getOpenStreetMapUrl() {
@ -469,11 +480,16 @@ export default class ProjectViewView extends Vue {
); );
} }
handleDialogResult(result) { handleDialogResult(result: GiverOutputInfo) {
if (result.action === "confirm") { if (result.action === "confirm") {
return new Promise((resolve) => { return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours); this.recordGive(
resolve(); result.giver?.did,
result.description,
result.hours,
).then(() => {
resolve(null);
});
}); });
} else { } else {
// action was not "confirm" so do nothing // action was not "confirm" so do nothing
@ -486,7 +502,7 @@ export default class ProjectViewView extends Vue {
* @param description may be an empty string * @param description may be an empty string
* @param hours may be 0 * @param hours may be 0
*/ */
async recordGive(giverDid, description, hours) { async recordGive(giverDid?: string, description?: string, hours?: number) {
if (!this.activeDid) { if (!this.activeDid) {
this.$notify( this.$notify(
{ {
@ -510,10 +526,7 @@ export default class ProjectViewView extends Vue {
}, },
-1, -1,
); );
return; } else {
}
try {
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
@ -525,21 +538,7 @@ export default class ProjectViewView extends Vue {
hours, hours,
this.projectId, this.projectId,
); );
if (result.type == "success") {
if (result.status !== 201 || result.data?.error) {
console.log("Error with give result:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
result.data?.error?.message ||
"There was an error recording the give.",
},
-1,
);
} else {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -549,22 +548,28 @@ export default class ProjectViewView extends Vue {
}, },
-1, -1,
); );
} else {
console.log("Error with give creation:", result);
if (result.type != "error") {
console.log(
"... and it has an unexpected result type of",
(result as ResultWithType).type,
);
} }
} catch (e) { const message =
console.log("Error with give caught:", e); result?.error?.userMessage ||
"There was an error recording the Give.";
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text: message,
e.userMessage ||
e.response?.data?.error?.message ||
"There was an error recording the give.",
}, },
-1, -1,
); );
} }
} }
} }
}
</script> </script>

46
src/views/ProjectsView.vue

@ -67,28 +67,33 @@
</li> </li>
</ul> </ul>
</InfiniteScroll> </InfiniteScroll>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db/index";
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 InfiniteScroll from "@/components/InfiniteScroll.vue";
import AlertMessage from "@/components/AlertMessage"; import QuickNav from "@/components/QuickNav.vue";
import QuickNav from "@/components/QuickNav"; import EntityIcon from "@/components/EntityIcon.vue";
import EntityIcon from "@/components/EntityIcon"; import { ProjectData } from "@/libs/endorserServer";
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ @Component({
components: { InfiniteScroll, AlertMessage, QuickNav, EntityIcon }, components: { InfiniteScroll, QuickNav, EntityIcon },
}) })
export default class ProjectsView extends Vue { export default class ProjectsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
apiServer = ""; apiServer = "";
projects: ProjectData[] = []; projects: ProjectData[] = [];
current: IIdentifier; current: IIdentifier;
@ -119,21 +124,30 @@ export default class ProjectsView extends Vue {
if (resp.status === 200 || !resp.data.data) { if (resp.status === 200 || !resp.data.data) {
const plans: ProjectData[] = resp.data.data; const plans: ProjectData[] = resp.data.data;
for (const plan of plans) { for (const plan of plans) {
const { name, description, handleId = plan.fullIri, rowid } = plan; const { name, description, handleId, rowid } = plan;
this.projects.push({ name, description, handleId, rowid }); this.projects.push({ name, description, handleId, rowid });
} }
} else { } else {
console.log("Bad server response & data:", resp.status, resp.data); console.log("Bad server response & data:", resp.status, resp.data);
throw Error("Failed to get projects from the server."); this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to get projects from the server. Try again later.",
},
-1,
);
} }
} catch (error) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
console.error("Got error loading projects:", error.message); } catch (error: any) {
console.error("Got error loading projects:", error.message || error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: "Got an error loading projects: " + error.message, text: "Got an error loading projects.",
}, },
-1, -1,
); );
@ -177,7 +191,7 @@ export default class ProjectsView extends Vue {
await this.dataLoader(url, token); await this.dataLoader(url, token);
} }
public async getIdentity(activeDid) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = await accountsDB.accounts
.where("did") .where("did")

30
src/views/SeedBackupView.vue

@ -50,28 +50,34 @@
</div> </div>
</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
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB, db } from "@/db"; import { accountsDB, db } from "@/db/index";
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.vue";
import QuickNav from "@/components/QuickNav";
@Component({ components: { AlertMessage, QuickNav } }) interface Account {
mnemonic: string;
}
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { QuickNav } })
export default class SeedBackupView extends Vue { export default class SeedBackupView extends Vue {
activeAccount = null; $notify!: (notification: Notification, timeout?: number) => void;
activeAccount: Account | null | undefined = null;
numAccounts = 0; numAccounts = 0;
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() {
@ -84,7 +90,7 @@ export default class SeedBackupView extends Vue {
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
this.numAccounts = accounts.length; this.numAccounts = accounts.length;
this.activeAccount = R.find((acc) => acc.did === activeDid, accounts); this.activeAccount = R.find((acc) => acc.did === activeDid, accounts);
} catch (err) { } catch (err: unknown) {
console.error("Got an error loading an identity:", err); console.error("Got an error loading an identity:", err);
this.$notify( this.$notify(
{ {

2
src/views/StartView.vue

@ -37,7 +37,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB } from "@/db"; import { accountsDB } from "@/db/index";
@Component({ @Component({
components: {}, components: {},

42
src/views/StatisticsView.vue

@ -34,25 +34,35 @@
</div> </div>
<button class="float-right" @click="captureGraphics()">Screenshot</button> <button class="float-right" @click="captureGraphics()">Screenshot</button>
<div id="scene-container" class="h-screen"></div> <div id="scene-container" class="h-screen"></div>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { SVGRenderer } from "three/addons/renderers/SVGRenderer.js"; import { SVGRenderer } from "three/examples/jsm/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.vue";
import QuickNav from "@/components/QuickNav";
@Component({ components: { AlertMessage, World, QuickNav } }) interface RendererSVGType {
domElement: Element;
}
interface Dictionary<T> {
[key: string]: T;
}
interface Notification {
group: string;
type: string;
title: string;
text: string;
}
@Component({ components: { World, QuickNav } })
export default class StatisticsView extends Vue { export default class StatisticsView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
world: World; world: World;
worldProperties: WorldProperties = {}; worldProperties: Dictionary<number> = {};
alertTitle = "";
alertMessage = "";
mounted() { mounted() {
try { try {
@ -60,14 +70,14 @@ export default class StatisticsView extends Vue {
const newWorld = new World(container, this); const newWorld = new World(container, this);
newWorld.start(); newWorld.start();
this.world = newWorld; this.world = newWorld;
} catch (err) { } catch (err: unknown) {
console.log(err); const error = err as Error;
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Mounting Error", title: "Mounting Error",
text: err.message, text: error.message,
}, },
-1, -1,
); );
@ -85,12 +95,12 @@ export default class StatisticsView extends Vue {
ExportToSVG(rendererSVG, "test.svg"); ExportToSVG(rendererSVG, "test.svg");
} }
public setWorldProperty(propertyName, propertyValue) { public setWorldProperty(propertyName: string, propertyValue: number) {
this.worldProperties[propertyName] = propertyValue; this.worldProperties[propertyName] = propertyValue;
} }
} }
function ExportToSVG(rendererSVG, filename) { function ExportToSVG(rendererSVG: RendererSVGType, filename: string) {
const XMLS = new XMLSerializer(); const XMLS = new XMLSerializer();
const svgfile = XMLS.serializeToString(rendererSVG.domElement); const svgfile = XMLS.serializeToString(rendererSVG.domElement);
const svgData = svgfile; const svgData = svgfile;

14
tsconfig.json

@ -1,8 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"allowJs": true,
"resolveJsonModule": true,
"target": "esnext", "target": "esnext",
"module": "esnext", "module": "esnext",
"strict": true, "strict": true,
"strictPropertyInitialization": false,
"jsx": "preserve", "jsx": "preserve",
"moduleResolution": "node", "moduleResolution": "node",
"experimentalDecorators": true, "experimentalDecorators": true,
@ -12,14 +15,17 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true, "useDefineForClassFields": true,
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": "./src",
"types": [ "types": [
"webpack-env" "webpack-env"
], ],
"paths": { "paths": {
"@/*": [ "@/components/*": ["components/*"],
"src/*" "@/views/*": ["views/*"],
] "@/db/*": ["db/*"],
"@/libs/*": ["libs/*"],
"@/constants/*": ["constants/*"],
"@/store/*": ["store/*"],
}, },
"lib": [ "lib": [
"esnext", "esnext",

Loading…
Cancel
Save