<template> <QuickNav selected="Projects"></QuickNav> <section id="Content" class="p-6 pb-24"> <!-- Heading --> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> Your Plans </h1> <!-- Quick Search --> <div id="QuickSearch" class="mb-4 flex"> <input type="text" placeholder="Search…" class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2" /> <button class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400" > <fa icon="magnifying-glass" class="fa-fw"></fa> </button> </div> <!-- New Project --> <button class="fixed right-6 bottom-24 text-center text-4xl leading-none bg-blue-600 text-white w-14 py-2.5 rounded-full" @click="onClickNewProject()" > <fa icon="plus" class="fa-fw"></fa> </button> <!-- Loading Animation --> <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" v-if="isLoading" > <fa icon="spinner" class="fa-spin-pulse"></fa> </div> <!-- Results List --> <InfiniteScroll @reached-bottom="loadMoreData"> <ul class="border-t border-slate-300"> <li class="border-b border-slate-300" v-for="project in projects" :key="project.handleId" > <a @click="onClickLoadProject(project.handleId)" class="block py-4 flex gap-4" > <div class="flex-none w-12"> <img src="https://picsum.photos/200/200?random=1" class="w-full rounded" /> </div> <div class="grow overflow-hidden"> <h2 class="text-base font-semibold">{{ project.name }}</h2> <div class="text-sm truncate"> {{ project.description }} </div> </div> </a> </li> </ul> </InfiniteScroll> <AlertMessage :alertTitle="alertTitle" :alertMessage="alertMessage" ></AlertMessage> </section> </template> <script lang="ts"> import { Component, Vue } from "vue-facing-decorator"; import { accountsDB, db } from "@/db"; import { AccountsSchema } from "@/db/tables/accounts"; import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { accessToken } from "@/libs/crypto"; import { IIdentifier } from "@veramo/core"; import InfiniteScroll from "@/components/InfiniteScroll"; import AlertMessage from "@/components/AlertMessage"; import QuickNav from "@/components/QuickNav"; @Component({ components: { InfiniteScroll, AlertMessage, QuickNav }, }) export default class ProjectsView extends Vue { apiServer = ""; projects: ProjectData[] = []; current: IIdentifier; isLoading = false; alertTitle = ""; alertMessage = ""; accounts: AccountsSchema; numAccounts = 0; async beforeCreate() { accountsDB.open(); this.accounts = accountsDB.accounts; this.numAccounts = await this.accounts.count(); } /** * Core project data loader * @param url the url used to fetch the data * @param token Authorization token **/ async dataLoader(url: string, token: string) { const headers: { [key: string]: string } = { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }; try { this.isLoading = true; const resp = await this.axios.get(url, { headers }); if (resp.status === 200 || !resp.data.data) { const plans: ProjectData[] = resp.data.data; for (const plan of plans) { const { name, description, handleId = plan.fullIri, rowid } = plan; this.projects.push({ name, description, handleId, rowid }); } } else { console.log("Bad server response & data:", resp.status, resp.data); throw Error("Failed to get projects from the server."); } } catch (error) { console.error("Got error loading projects:", error.message); this.alertTitle = "Error"; this.alertMessage = "Got an error loading projects:" + error.message; } finally { this.isLoading = false; } } /** * Data loader used by infinite scroller * @param payload is the flag from the InfiniteScroll indicating if it should load **/ async loadMoreData(payload: boolean) { if (this.projects.length > 0 && payload) { const latestProject = this.projects[this.projects.length - 1]; const url = `${this.apiServer}/api/v2/report/plansByIssuer?beforeId=${latestProject.rowid}`; const token = await accessToken(this.current); await this.dataLoader(url, token); } } /** * Handle clicking on a project entry found in the list * @param id of the project **/ onClickLoadProject(id: string) { localStorage.setItem("projectId", id); const route = { name: "project", }; this.$router.push(route); } /** * Load projects initially * @param identity of the user **/ async LoadProjects(identity: IIdentifier) { const url = `${this.apiServer}/api/v2/report/plansByIssuer`; const token: string = await accessToken(identity); await this.dataLoader(url, token); } 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 project records with no identity available.", ); } return identity; } /** * 'created' hook runs when the Vue instance is first created **/ async created() { try { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); const activeDid = settings?.activeDid || ""; this.apiServer = settings?.apiServer || ""; if (this.numAccounts === 0) { console.error("No accounts found."); this.alertTitle = "Error"; this.alertMessage = "You need an identity to load your projects."; } else { const identity = await this.getIdentity(activeDid); this.current = identity; this.LoadProjects(identity); } } catch (err) { console.log("Error initializing:", err); this.alertTitle = "Error"; this.alertMessage = "Something went wrong loading your projects."; } } /** * Handling clicking on the new project button **/ onClickNewProject(): void { localStorage.removeItem("projectId"); const route = { name: "new-edit-project", }; this.$router.push(route); } } </script>