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