Browse Source
			
			
			
			
				
		Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/kick-starter-for-time-pwa/pulls/43projects-view-improvements
				 5 changed files with 350 additions and 47 deletions
			
			
		@ -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> | 
				
			||||
					Loading…
					
					
				
		Reference in new issue