Browse Source

add details on contact-specific page

pull/15/head
Trent Larson 2 years ago
parent
commit
f281e41181
  1. 1
      project.yaml
  2. 2
      src/constants/app.ts
  3. 30
      src/libs/endorserServer.ts
  4. 8
      src/main.ts
  5. 212
      src/views/ContactAmountsView.vue
  6. 219
      src/views/ContactsView.vue

1
project.yaml

@ -30,6 +30,7 @@
- refactor UI :
- .5 Alerts show at the top and can be missed, eg. account data download
- 01 Change alerts into a component (to cut down duplicate code)
- 01 Code for "nav" tabs across the bottom is duplicated on each page.
- .2 Add "copied" feedback when they click "copy" on /account

2
src/constants/app.ts

@ -2,7 +2,7 @@
* Generic strings that could be used throughout the app.
*/
export enum AppString {
APP_NAME = "Kickstart for time",
APP_NAME = "KickStart with Time",
VERSION = "0.1",
DEFAULT_ENDORSER_API_SERVER = "https://test.endorser.ch:8000",
//DEFAULT_ENDORSER_API_SERVER = "http://localhost:3000",

30
src/libs/endorserServer.ts

@ -0,0 +1,30 @@
export const SERVICE_ID = "endorser.ch";
export interface GiveServerRecord {
agentDid: string;
amount: number;
confirmed: number;
description: string;
fullClaim: GiveVerifiableCredential;
handleId: string;
issuedAt: string;
recipientDid: string;
unit: string;
}
export interface GiveVerifiableCredential {
"@context": string;
"@type": string;
agent: { identifier: string };
description?: string;
object: { amountOfThisGood: number; unitCode: string };
recipient: { identifier: string };
}
export interface RegisterVerifiableCredential {
"@context": string;
"@type": string;
agent: { identifier: string };
object: string;
recipient: { identifier: string };
}

8
src/main.ts

@ -12,6 +12,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import {
faCalendar,
faChevronLeft,
faCircle,
faCircleCheck,
faCircleQuestion,
faCircleUser,
@ -19,9 +20,12 @@ import {
faEllipsisVertical,
faEye,
faEyeSlash,
faFileLines,
faFolderOpen,
faHand,
faHouseChimney,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faPen,
faPersonCircleCheck,
@ -40,6 +44,7 @@ import {
library.add(
faCalendar,
faChevronLeft,
faCircle,
faCircleCheck,
faCircleQuestion,
faCircleUser,
@ -47,9 +52,12 @@ library.add(
faEllipsisVertical,
faEye,
faEyeSlash,
faFileLines,
faFolderOpen,
faHand,
faHouseChimney,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faPen,
faPersonCircleCheck,

212
src/views/ContactAmountsView.vue

@ -45,26 +45,232 @@
<section id="Content" class="p-6 pb-24">
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
Transactions with {{ contact?.name }}
Given with {{ contact?.name }}
</h1>
<div>{{ contact?.did }}</div>
<!-- Results List -->
<div>
<div class="border-b border-slate-300 flex">
<div class="w-1/4"></div>
<div class="w-1/4">from them</div>
<div class="w-1/4"></div>
<div class="w-1/4">to them</div>
</div>
<div
class="border-b border-slate-300 flex"
v-for="record in giveRecords"
:key="record.id"
>
<div class="w-1/4">
{{ new Date(record.issuedAt).toLocaleString() }}
</div>
<div class="w-1/4">
<span v-if="record.agentDid == contact.did">
<div class="font-bold">
{{ record.amount }} {{ record.unit }}
<span v-if="record.confirmed" class="tooltip">
<fa icon="circle-check" class="text-green-600 fa-fw ml-1" />
<span class="tooltiptext">Confirmed</span>
</span>
<span v-else class="tooltip">
<fa icon="circle" class="text-blue-600 fa-fw ml-1" />
<span class="tooltiptext">Unconfirmed</span>
</span>
</div>
<br />
{{ record.description }}
</span>
</div>
<div class="w-1/8">
<span v-if="record.agentDid == contact.did">
<fa icon="long-arrow-alt-left" class="text-slate-900 fa-fw ml-1" />
</span>
<span v-else>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<fa icon="long-arrow-alt-right" class="text-slate-900 fa-fw ml-1" />
</span>
</div>
<div class="w-1/4">
<span v-if="record.agentDid != contact.did">
<div class="font-bold">
{{ record.amount }} {{ record.unit }}
<span v-if="record.confirmed" class="tooltip">
<fa icon="circle-check" class="text-green-600 fa-fw ml-1" />
<span class="tooltiptext">Confirmed</span>
</span>
<span v-else class="tooltip">
<fa icon="circle" class="text-slate-600 fa-fw ml-1" />
<span class="tooltiptext">Unconfirmed</span>
</span>
</div>
<br />
{{ record.description }}
</span>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import * as R from "ramda";
import { Options, Vue } from "vue-class-component";
import { Contact } from "@/db/tables/contacts";
import { db } from "@/db";
import { accountsDB, db } from "@/db";
import { accessToken } from "@/libs/crypto";
import { GiveServerRecord } from "@/libs/endorserServer";
import { AppString } from "@/constants/app";
@Options({})
export default class ContactsView extends Vue {
contact: Contact | null = null;
giveRecords: Array<GiveServerRecord> = [];
// 'created' hook runs when the Vue instance is first created
async created() {
await db.open();
const contactDid = this.$route.query.contactDid as string;
this.contact = (await db.contacts.get(contactDid)) || null;
if (this.contact) {
this.loadGives(this.contact);
}
}
async loadGives(contact: Contact) {
// only load the private keys temporarily when needed
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const identity = JSON.parse(accounts[0].identity);
const endorserApiServer = AppString.DEFAULT_ENDORSER_API_SERVER;
// load all the time I have given to them
try {
let result = [];
const url =
endorserApiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(identity.did) +
"&recipientDid=" +
encodeURIComponent(contact.did);
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
result = resp.data.data;
} else {
console.log(
"Got bad response status & data of",
resp.status,
resp.data
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
const url2 =
endorserApiServer +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(contact.did) +
"&recipientDid=" +
encodeURIComponent(identity.did);
const token2 = await accessToken(identity);
const headers2 = {
"Content-Type": "application/json",
Authorization: "Bearer " + token2,
};
const resp2 = await this.axios.get(url2, { headers: headers2 });
if (resp2.status === 200) {
result = R.concat(result, resp2.data.data);
} else {
console.log(
"Got bad response status & data of",
resp2.status,
resp2.data
);
this.alertTitle = "Error With Server";
this.alertMessage =
"Got an error retrieving your given time from the server.";
this.isAlertVisible = true;
}
const sortedResult: Array<GiveServerRecord> = R.sort(
(a, b) => new Date(a).getTime() - new Date(b).getTime(),
result
);
this.giveRecords = sortedResult;
} catch (error) {
this.alertTitle = "Error With Server";
this.alertMessage = error as string;
this.isAlertVisible = true;
}
}
alertTitle = "";
alertMessage = "";
isAlertVisible = false;
public onClickClose() {
this.isAlertVisible = false;
this.alertTitle = "";
this.alertMessage = "";
}
public computedAlertClassNames() {
return {
hidden: !this.isAlertVisible,
"dismissable-alert": true,
"bg-slate-100": true,
"p-5": true,
rounded: true,
"drop-shadow-lg": true,
absolute: true,
"top-3": true,
"inset-x-3": true,
"transition-transform": true,
"ease-in": true,
"duration-300": true,
};
}
}
</script>
<style>
/* Tooltip from https://www.w3schools.com/css/css_tooltip.asp */
/* Tooltip container */
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
}
/* Tooltip text */
.tooltip .tooltiptext {
visibility: hidden;
width: 200px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
}
.tooltip:hover .tooltiptext-left {
visibility: visible;
}
</style>

219
src/views/ContactsView.vue

@ -122,10 +122,10 @@
@click="setVisibility(contact, false)"
>
<fa icon="eye" class="text-slate-900 fa-fw ml-1" />
<span class="tooltiptext">Can see you</span>
<span class="tooltiptext">They can see you</span>
</button>
<button v-else class="tooltip" @click="setVisibility(contact, true)">
<span class="tooltiptext">Cannot see you</span>
<span class="tooltiptext">They cannot see you</span>
<fa icon="eye-slash" class="text-slate-900 fa-fw ml-1" />
</button>
@ -135,11 +135,11 @@
</button>
<button v-if="contact.registered" class="tooltip">
<span class="tooltiptext">Registered</span>
<span class="tooltiptext">They are registered</span>
<fa icon="person-circle-check" class="text-slate-900 fa-fw ml-1" />
</button>
<button v-else @click="register(contact)" class="tooltip">
<span class="tooltiptext">Maybe not registered</span>
<span class="tooltiptext">They may not be registered</span>
<fa
icon="person-circle-question"
class="text-slate-900 fa-fw ml-1"
@ -165,9 +165,9 @@
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
<span class="tooltiptext-left">{{
givenByMeDescriptions[contact.did]
}}</span>
<span class="tooltiptext-left">
{{ givenByMeDescriptions[contact.did] || "Nothing" }}
</span>
<button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@click="onClickAddGive(identity.did, contact.did)"
@ -188,7 +188,7 @@
/* eslint-enable prettier/prettier */
}}
<span class="tooltiptext-left">
{{ givenToMeDescriptions[contact.did] }}
{{ givenToMeDescriptions[contact.did] || "Nothing" }}
</span>
<button
class="text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
@ -197,6 +197,16 @@
+
</button>
</div>
<router-link
:to="{
name: 'contact-amounts',
query: { contactDid: contact.did },
}"
class="tooltip"
>
<fa icon="file-lines" class="text-slate-600 fa-fw ml-1" />
<span class="tooltiptext-left">See All Given Activity</span>
</router-link>
</div>
</div>
</div>
@ -223,44 +233,20 @@ import { IIdentifier } from "@veramo/core";
import { Options, Vue } from "vue-class-component";
import { AppString } from "@/constants/app";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import { accountsDB, db } from "@/db";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken, SimpleSigner } from "@/libs/crypto";
import {
GiveServerRecord,
GiveVerifiableCredential,
RegisterVerifiableCredential,
SERVICE_ID,
} from "@/libs/endorserServer";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer;
const SERVICE_ID = "endorser.ch";
export interface GiveServerRecord {
agentDid: string;
amount: number;
confirmed: number;
description: string;
fullClaim: GiveVerifiableCredential;
handleId: string;
recipientDid: string;
unit: string;
}
export interface GiveVerifiableCredential {
"@context": string;
"@type": string;
agent: { identifier: string };
description?: string;
object: { amountOfThisGood: number; unitCode: string };
recipient: { identifier: string };
}
export interface RegisterVerifiableCredential {
"@context": string;
"@type": string;
agent: { identifier: string };
object: string;
recipient: { identifier: string };
}
@Options({
components: {},
})
@ -305,33 +291,6 @@ export default class ContactsView extends Vue {
);
}
async onClickNewContact(): Promise<void> {
let did = this.contactInput;
let name, publicKeyBase64;
const commaPos1 = this.contactInput.indexOf(",");
if (commaPos1 > -1) {
did = this.contactInput.substring(0, commaPos1).trim();
name = this.contactInput.substring(commaPos1 + 1).trim();
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
if (commaPos2 > -1) {
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
publicKeyBase64 = this.contactInput.substring(commaPos2 + 1).trim();
}
}
// help with potential mistakes while this sharing requires copy-and-paste
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
// it must be all hex (compressed public key), so convert
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
}
const newContact = { did, name, publicKeyBase64 };
await db.contacts.add(newContact);
const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts
);
}
async loadGives() {
if (!this.identity) {
console.error(
@ -346,7 +305,7 @@ export default class ContactsView extends Vue {
try {
const url =
endorserApiServer +
"/api/v2/report/gives?agentId=" +
"/api/v2/report/gives?agentDid=" +
encodeURIComponent(this.identity?.did);
const token = await accessToken(this.identity);
const headers = {
@ -400,7 +359,7 @@ export default class ContactsView extends Vue {
try {
const url =
endorserApiServer +
"/api/v2/report/gives?recipientId=" +
"/api/v2/report/gives?recipientDid=" +
encodeURIComponent(this.identity.did);
const token = await accessToken(this.identity);
const headers = {
@ -450,6 +409,33 @@ export default class ContactsView extends Vue {
}
}
async onClickNewContact(): Promise<void> {
let did = this.contactInput;
let name, publicKeyBase64;
const commaPos1 = this.contactInput.indexOf(",");
if (commaPos1 > -1) {
did = this.contactInput.substring(0, commaPos1).trim();
name = this.contactInput.substring(commaPos1 + 1).trim();
const commaPos2 = this.contactInput.indexOf(",", commaPos1 + 1);
if (commaPos2 > -1) {
name = this.contactInput.substring(commaPos1 + 1, commaPos2).trim();
publicKeyBase64 = this.contactInput.substring(commaPos2 + 1).trim();
}
}
// help with potential mistakes while this sharing requires copy-and-paste
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
// it must be all hex (compressed public key), so convert
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
}
const newContact = { did, name, publicKeyBase64 };
await db.contacts.add(newContact);
const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
allContacts
);
}
async deleteContact(contact: Contact) {
if (
confirm(
@ -647,6 +633,22 @@ export default class ContactsView extends Vue {
}
async onClickAddGive(fromDid: string, toDid: string): Promise<void> {
// if they have unconfirmed amounts, ask to confirm those first
if (toDid == this.identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) {
if (
confirm(
"There are " +
this.givenToMeUnconfirmed[fromDid] +
" unconfirmed hours from them." +
" Would you like to confirm some of those hours?"
)
) {
this.$router.push({
name: "contact-amounts",
query: { contactDid: fromDid },
});
}
}
if (!this.isNumeric(this.hourInput)) {
this.alertTitle = "Input Error";
this.alertMessage =
@ -661,61 +663,36 @@ export default class ContactsView extends Vue {
this.alertMessage = "No identity is available.";
this.isAlertVisible = true;
} else {
// if they have unconfirmed amounts, ask to confirm those first
let wantsToConfirm = false;
if (
toDid == this.identity?.did &&
this.givenToMeUnconfirmed[fromDid] > 0
) {
if (
confirm(
"There are " +
this.givenToMeUnconfirmed[fromDid] +
" unconfirmed hours from them." +
" Would you like to confirm some of those hours?"
)
) {
wantsToConfirm = true;
}
// ask to confirm amount
let toFrom;
if (fromDid == this.identity?.did) {
toFrom = "from you to " + this.nameForDid(this.contacts, toDid);
} else {
toFrom = "from " + this.nameForDid(this.contacts, fromDid) + " to you";
}
if (wantsToConfirm) {
this.$router.push({
name: "contact-amounts",
query: { contactDid: fromDid },
});
let description;
if (this.hourDescriptionInput) {
description = " with description '" + this.hourDescriptionInput + "'";
} else {
// ask to confirm amount
let toFrom;
if (fromDid == this.identity?.did) {
toFrom = "from you to " + this.nameForDid(this.contacts, toDid);
} else {
toFrom =
"from " + this.nameForDid(this.contacts, fromDid) + " to you";
}
let description;
if (this.hourDescriptionInput) {
description = " with description '" + this.hourDescriptionInput + "'";
} else {
description = " with no description";
}
if (
confirm(
"Are you sure you want to record " +
this.hourInput +
" hours " +
toFrom +
description +
"?"
)
) {
this.createAndSubmitGive(
this.identity,
fromDid,
toDid,
parseFloat(this.hourInput),
this.hourDescriptionInput
);
}
description = " with no description";
}
if (
confirm(
"Are you sure you want to record " +
this.hourInput +
" hours " +
toFrom +
description +
"?"
)
) {
this.createAndSubmitGive(
this.identity,
fromDid,
toDid,
parseFloat(this.hourInput),
this.hourDescriptionInput
);
}
}
}

Loading…
Cancel
Save