<template> <QuickNav selected="Discover"></QuickNav> <TopMessage /> <!-- CONTENT --> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <!-- Heading --> <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8"> Discover Projects </h1> <!-- Quick Search --> <div id="QuickSearch" class="mb-4 flex" v-on:keyup.enter="searchSelected()"> <input type="text" v-model="searchTerms" placeholder="Search…" class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2" /> <button @click="searchSelected()" 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> <!-- Result Tabs --> <div class="text-center text-slate-500 border-b border-slate-300"> <ul class="flex flex-wrap justify-center gap-4 -mb-px"> <li> <a href="#" @click=" projects = []; isLocalActive = true; isRemoteActive = false; searchLocal(); " v-bind:class="computedLocalTabStyleClassNames()" > Nearby <span class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md" v-if="isLocalActive" > {{ localCount > -1 ? localCount : "?" }} </span> </a> </li> <li> <a href="#" @click=" projects = []; isRemoteActive = true; isLocalActive = false; searchAll(); " v-bind:class="computedRemoteTabStyleClassNames()" > Anywhere <span class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md" v-if="isRemoteActive" > {{ remoteCount > -1 ? remoteCount : "?" }} </span> </a> </li> </ul> </div> <div v-if="isLocalActive"> <div> <button class="ml-2 mt-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500" @click="$router.push({ name: 'search-area' })" > Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search </button> </div> </div> <!-- 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> <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 cursor-pointer" > <div> <ProjectIcon :entityId="project.handleId" :iconSize="48" :imageUrl="project.image" class="block border border-slate-300 rounded-md max-h-12 max-w-12" /> </div> <div class="grow"> <h2 class="text-base font-semibold">{{ project.name }}</h2> <div class="text-sm"> <fa icon="user" class="fa-fw text-slate-400"></fa> {{ didInfo(project.issuerDid, activeDid, allMyDids, allContacts) }} </div> </div> </a> </li> </ul> </InfiniteScroll> </section> </template> <script lang="ts"> import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; import QuickNav from "@/components/QuickNav.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue"; import ProjectIcon from "@/components/ProjectIcon.vue"; import TopMessage from "@/components/TopMessage.vue"; import { NotificationIface } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; import { Contact } from "@/db/tables/contacts"; import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { didInfo, getHeaders, PlanData } from "@/libs/endorserServer"; @Component({ components: { InfiniteScroll, ProjectIcon, QuickNav, TopMessage, }, }) export default class DiscoverView extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; activeDid = ""; allContacts: Array<Contact> = []; allMyDids: Array<string> = []; apiServer = ""; searchTerms = ""; projects: PlanData[] = []; isLoading = false; isLocalActive = true; isRemoteActive = false; localCount = -1; remoteCount = -1; searchBox: { name: string; bbox: BoundingBox } | null = null; // make this function available to the Vue template didInfo = didInfo; async mounted() { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); this.activeDid = (settings?.activeDid as string) || ""; this.apiServer = (settings?.apiServer as string) || ""; this.searchBox = settings?.searchBoxes?.[0] || null; this.allContacts = await db.contacts.toArray(); await accountsDB.open(); const allAccounts = await accountsDB.accounts.toArray(); this.allMyDids = allAccounts.map((acc) => acc.did); if (this.searchBox) { await this.searchLocal(); } else { this.isLocalActive = false; this.isRemoteActive = true; await this.searchAll(); } } public resetCounts() { this.localCount = -1; this.remoteCount = -1; } public async searchSelected() { if (this.isLocalActive) { await this.searchLocal(); } else { await this.searchAll(); } } public async searchAll(beforeId?: string) { this.resetCounts(); if (!beforeId) { // this was an initial search so clear any previous results this.projects = []; } let queryParams = "claimContents=" + encodeURIComponent(this.searchTerms); if (beforeId) { queryParams = queryParams + `&beforeId=${beforeId}`; } try { this.isLoading = true; const response = await fetch( this.apiServer + "/api/v2/report/plans?" + queryParams, { method: "GET", headers: await getHeaders(this.activeDid), }, ); if (response.status !== 200) { const details = await response.text(); console.error("Problem with full search:", details); this.$notify( { group: "alert", type: "danger", title: "Error", text: `There was a problem accessing the server. Try again later.`, }, -1, ); throw details; } const results = await response.json(); const plans: PlanData[] = results.data; if (plans) { for (const plan of plans) { const { name, description, handleId, image, issuerDid, rowid } = plan; this.projects.push({ name, description, handleId, image, issuerDid, rowid, }); } this.remoteCount = this.projects.length; } else { throw JSON.stringify(results); } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { console.error("Error with feed load:", e); this.$notify( { group: "alert", type: "danger", title: "Error", text: e.userMessage || "There was a problem retrieving projects.", }, -1, ); } finally { this.isLoading = false; } } public async searchLocal(beforeId?: string) { this.resetCounts(); if (!this.searchBox) { this.projects = []; return; } if (!beforeId) { // this was an initial search so clear any previous results this.projects = []; } const claimContents = "claimContents=" + encodeURIComponent(this.searchTerms); let queryParams = [ claimContents, "minLocLat=" + this.searchBox.bbox.minLat, "maxLocLat=" + this.searchBox.bbox.maxLat, "westLocLon=" + this.searchBox.bbox.westLong, "eastLocLon=" + this.searchBox.bbox.eastLong, ].join("&"); if (beforeId) { queryParams = queryParams + `&beforeId=${beforeId}`; } try { this.isLoading = true; const response = await fetch( this.apiServer + "/api/v2/report/plansByLocation?" + queryParams, { method: "GET", headers: await getHeaders(this.activeDid), }, ); if (response.status !== 200) { const details = await response.text(); console.error("Problem with nearby search:", details); this.$notify( { group: "alert", type: "danger", title: "Error", text: "There was a problem accessing the server. Try again later.", }, -1, ); throw await response.text(); } const results = await response.json(); if (results.data) { if (beforeId) { const plans: PlanData[] = results.data; for (const plan of plans) { const { name, description, handleId, issuerDid, rowid } = plan; this.projects.push({ name, description, handleId, issuerDid, rowid, }); } } else { this.projects = results.data; } this.localCount = this.projects.length; } else { throw JSON.stringify(results); } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { console.error("Error with feed load:", e); this.$notify( { group: "alert", type: "danger", title: "Error", text: e.userMessage || "There was a problem retrieving projects.", }, -1, ); } 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]; if (this.isLocalActive) { this.searchLocal(latestProject["rowid"]); } else if (this.isRemoteActive) { this.searchAll(latestProject["rowid"]); } } } /** * Handle clicking on a project entry found in the list * @param id of the project **/ onClickLoadProject(id: string) { localStorage.setItem("projectId", id); const route = { path: "/project/" + encodeURIComponent(id), }; (this.$router as Router).push(route); } public computedLocalTabStyleClassNames() { return { "inline-block": true, "py-3": true, "rounded-t-lg": true, "border-b-2": true, active: this.isLocalActive, "text-black": this.isLocalActive, "border-black": this.isLocalActive, "font-semibold": this.isLocalActive, "text-blue-600": !this.isLocalActive, "border-transparent": !this.isLocalActive, "hover:border-slate-400": !this.isLocalActive, }; } public computedRemoteTabStyleClassNames() { return { "inline-block": true, "py-3": true, "rounded-t-lg": true, "border-b-2": true, active: this.isRemoteActive, "text-black": this.isRemoteActive, "border-black": this.isRemoteActive, "font-semibold": this.isRemoteActive, "text-blue-600": !this.isRemoteActive, "border-transparent": !this.isRemoteActive, "hover:border-slate-400": !this.isRemoteActive, }; } } </script>