Browse Source

Merge pull request 'home-gifting-improvements' (#43) from home-gifting-improvements into master

Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/kick-starter-for-time-pwa/pulls/43
kb/add-usage-guide
anomalist 1 year ago
parent
commit
015704c94e
  1. 54
      src/components/GiftedDialog.vue
  2. 2
      src/main.ts
  3. 8
      src/router/index.ts
  4. 265
      src/views/ContactGiftingView.vue
  5. 62
      src/views/HomeView.vue

54
src/components/GiftedDialog.vue

@ -1,43 +1,53 @@
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-lg text-center"> <h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giver?.name || "somebody not specified" }} {{ message }} {{ giver?.name || "somebody not specified" }}
</h1> </h1>
<input <input
type="text" type="text"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received" placeholder="What was received"
v-model="description" v-model="description"
/> />
<div class="flex flex-row"> <div class="flex flex-row mb-6">
<span class="py-4">Hours</span> <span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center px-2 py-2"
>Hours</span
>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="decrement()"
>
<fa icon="chevron-left" />
</div>
<input <input
type="text" type="text"
class="block w-8 rounded border border-slate-400 ml-4 text-center" class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="hours" v-model="hours"
/> />
<div class="flex flex-col px-1"> <div
<div> class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
<fa icon="square-caret-up" size="2xl" @click="increment()" /> @click="increment()"
</div> >
<div> <fa icon="chevron-right" />
<fa icon="square-caret-down" size="2xl" @click="decrement()" />
</div> </div>
</div> </div>
</div> <p class="text-center mb-2 italic">Sign & Send to publish to the world</p>
<p class="text-right">Sign & Send to publish to the world</p> <button
<div class="text-right"> class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
<button class="rounded border border-slate-400" @click="confirm"> @click="confirm"
<span class="m-2">Sign & Send</span> >
Sign &amp; Send
</button> </button>
&nbsp; <button
<button class="rounded border border-slate-400" @click="cancel"> class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
<span class="m-2">Cancel</span> @click="cancel"
>
Cancel
</button> </button>
</div> </div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -106,12 +116,14 @@ export default class GiftedDialog extends Vue {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 1.5rem;
} }
.dialog { .dialog {
background-color: white; background-color: white;
padding: 1rem; padding: 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
width: 50%; width: 100%;
max-width: 500px;
} }
</style> </style>

2
src/main.ts

@ -13,6 +13,7 @@ import {
faBurst, faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
faChevronRight,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleQuestion, faCircleQuestion,
@ -53,6 +54,7 @@ library.add(
faBurst, faBurst,
faCalendar, faCalendar,
faChevronLeft, faChevronLeft,
faChevronRight,
faCircle, faCircle,
faCircleCheck, faCircleCheck,
faCircleQuestion, faCircleQuestion,

8
src/router/index.ts

@ -166,6 +166,14 @@ const routes: Array<RouteRecordRaw> = [
/* webpackChunkName: "statistics" */ "../views/StatisticsView.vue" /* webpackChunkName: "statistics" */ "../views/StatisticsView.vue"
), ),
}, },
{
path: "/contact-gives",
name: "contact-gives",
component: () =>
import(
/* webpackChunkName: "statistics" */ "../views/ContactGiftingView.vue"
),
},
]; ];
/** @type {*} */ /** @type {*} */

265
src/views/ContactGiftingView.vue

@ -0,0 +1,265 @@
<template>
<QuickNav selected="Home"></QuickNav>
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24">
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<router-link
:to="{ name: 'home' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
Give to Contacts
</h1>
</div>
<!-- Quick Search -->
<!-- Initial Loading Animation -->
<!-- Results List -->
<ul class="border-t border-slate-300">
<li class="border-b border-slate-300 py-3">
<h2 class="text-base flex gap-4 items-center">
<span class="grow italic"
><fa icon="question-circle" class="fa-fw fa-xl text-slate-400"></fa>
Anonymous
</span>
<span class="text-right">
<button
type="button"
@click="openDialog()"
class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md"
>
<fa icon="gift" class="fa-fw"></fa>
</button>
</span>
</h2>
</li>
<li
v-for="contact in allContacts"
:key="contact.did"
class="border-b border-slate-300 py-3"
>
<h2 class="text-base flex gap-4 items-center">
<span class="grow font-semibold"
><fa icon="user" class="fa-fw fa-xl text-slate-400"></fa>
{{ contact.name || "(no name)" }}
</span>
<span class="text-right">
<button
type="button"
@click="openDialog(contact)"
class="block w-full text-center text-sm uppercase bg-blue-600 text-white px-3 py-1.5 rounded-md"
>
<fa icon="gift" class="fa-fw"></fa>
</button>
</span>
</h2>
</li>
</ul>
<GiftedDialog
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<AlertMessage
:alertTitle="alertTitle"
:alertMessage="alertMessage"
></AlertMessage>
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db";
import { AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto";
import { createAndSubmitGive } from "@/libs/endorserServer";
import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import AlertMessage from "@/components/AlertMessage";
import QuickNav from "@/components/QuickNav";
@Component({
components: { GiftedDialog, AlertMessage, QuickNav },
})
export default class HomeView extends Vue {
activeDid = "";
allAccounts: Array<Account> = [];
allContacts: Array<Contact> = [];
apiServer = "";
isHiddenSpinner = true;
alertTitle = "";
alertMessage = "";
accounts: AccountsSchema;
numAccounts = 0;
async beforeCreate() {
accountsDB.open();
this.accounts = accountsDB.accounts;
this.numAccounts = await this.accounts.count();
}
public async getIdentity(activeDid) {
await accountsDB.open();
const account = await accountsDB.accounts
.where("did")
.equals(activeDid)
.first();
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to load Give records with no identity available.",
);
}
return identity;
}
public async getHeaders(identity) {
const token = await accessToken(identity);
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
};
return headers;
}
async created() {
try {
await accountsDB.open();
this.allAccounts = await accountsDB.accounts.toArray();
await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
this.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId;
this.updateAllFeed();
} catch (err) {
this.alertTitle = "Error";
this.alertMessage =
err.userMessage ||
"There was an error retrieving the latest sweet, sweet action.";
}
}
public async buildHeaders() {
const headers = { "Content-Type": "application/json" };
if (this.activeDid) {
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
const account = allAccounts.find((acc) => acc.did === this.activeDid);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"An ID is chosen but there are no keys for it so it cannot be used to talk with the service.",
);
}
headers["Authorization"] = "Bearer " + (await accessToken(identity));
} else {
// it's OK without auth... we just won't get any identifiers
}
return headers;
}
openDialog(giver) {
this.$refs.customDialog.open(giver);
}
handleDialogResult(result) {
if (result.action === "confirm") {
return new Promise((resolve) => {
this.recordGive(result.contact?.did, result.description, result.hours);
resolve();
});
} else {
// action was "cancel" so do nothing
}
}
/**
*
* @param giverDid may be null
* @param description may be an empty string
* @param hours may be 0
*/
public async recordGive(giverDid, description, hours) {
if (!this.activeDid) {
this.setAlert(
"Error",
"You must select an identity before you can record a give.",
);
return;
}
if (!description && !hours) {
this.setAlert(
"Error",
"You must enter a description or some number of hours.",
);
return;
}
try {
const identity = await this.getIdentity(this.activeDid);
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
identity,
giverDid,
this.activeDid,
description,
hours,
);
if (isGiveCreationError(result)) {
const errorMessage = getGiveCreationErrorMessage(result);
console.log("Error with give result:", result);
this.setAlert(
"Error",
errorMessage || "There was an error recording the give.",
);
} else {
this.setAlert("Success", "That gift was recorded.");
}
} catch (error) {
console.log("Error with give caught:", error);
this.setAlert(
"Error",
getGiveErrorMessage(error) || "There was an error recording the give.",
);
}
}
private setAlert(title, message) {
this.alertTitle = title;
this.alertMessage = message;
}
// Helper functions for readability
isGiveCreationError(result) {
return result.status !== 201 || result.data?.error;
}
getGiveCreationErrorMessage(result) {
return result.data?.error?.message;
}
getGiveErrorMessage(error) {
return error.userMessage || error.response?.data?.error?.message;
}
}
</script>

62
src/views/HomeView.vue

@ -7,22 +7,40 @@
</h1> </h1>
<div class="mb-8"> <div class="mb-8">
<h1 class="text-2xl">Quick Action</h1> <h2 class="text-xl font-bold">Quick Action</h2>
<p>Choose a contact to whom to show appreciation:</p> <p class="mb-4">Show appreciation to a contact:</p>
<!-- similar contact selection code is in multiple places -->
<div class="px-4"> <ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
<button <li
v-for="contact in allContacts" v-for="contact in allContacts"
:key="contact.did" :key="contact.did"
@click="openDialog(contact)" @click="openDialog(contact)"
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" >
<div class="mb-1">
<fa icon="user" class="fa-fw fa-xl text-slate-400"></fa>
</div>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
> >
{{ contact.name || "(no name)" }} {{ contact.name || "(no name)" }}
</button> </h3>
<span v-if="allContacts.length > 0">&nbsp;or&nbsp;</span> </li>
<button @click="openDialog()" class="text-blue-500"> </ul>
someone not specified
</button> <!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
<router-link
v-if="allContacts.length > 7"
:to="{ name: 'contact-gives' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
>
Show More Contacts&hellip;
</router-link>
<!-- If there are no contacts, show this instead: -->
<div
class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
>
(No contacts to show.)
</div> </div>
</div> </div>
@ -33,29 +51,27 @@
> >
</GiftedDialog> </GiftedDialog>
<div> <div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
<h1 class="text-2xl">Latest Activity</h1> <h2 class="text-xl font-bold mb-4">Latest Activity</h2>
<span :class="{ hidden: isHiddenSpinner }"> <div :class="{ hidden: isHiddenSpinner }">
<fa icon="spinner" class="fa-spin-pulse"></fa> <p class="text-slate-500 text-center italic mt-4 mb-4">
Loading&hellip; <fa icon="spinner" class="fa-spin-pulse"></fa> Loading&hellip;
</span> </p>
<ul> </div>
<ul class="border-t border-slate-300">
<li <li
class="border-b border-slate-300 py-2" class="border-b border-slate-300 py-2"
v-for="record in feedData" v-for="record in feedData"
:key="record.jwtId" :key="record.jwtId"
> >
<div <div
class="border-b border-dashed border-slate-400 text-orange-400 py-2 mb-2 font-bold uppercase text-sm" class="border-b border-dashed border-slate-400 text-orange-400 pb-2 mb-2 font-bold uppercase text-sm"
v-if="record.jwtId == feedLastViewedId" v-if="record.jwtId == feedLastViewedId"
> >
You've seen all claims below: You've seen all claims below:
</div> </div>
<div class="flex"> <div class="flex">
<fa <fa icon="gift" class="pt-1 pr-2 text-slate-500"></fa>
icon="gift"
class="fa-fw flex-none pt-1 pr-2 text-slate-500"
></fa>
<!-- icon values: "coins" = money; "clock" = time; "gift" = others --> <!-- icon values: "coins" = money; "clock" = time; "gift" = others -->
<span class="">{{ this.giveDescription(record) }}</span> <span class="">{{ this.giveDescription(record) }}</span>
</div> </div>

Loading…
Cancel
Save