You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							520 lines
						
					
					
						
							16 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							520 lines
						
					
					
						
							16 KiB
						
					
					
				
								<template>
							 | 
						|
								  <QuickNav selected="Projects"></QuickNav>
							 | 
						|
								  <TopMessage />
							 | 
						|
								
							 | 
						|
								  <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">
							 | 
						|
								      Your Ideas
							 | 
						|
								    </h1>
							 | 
						|
								
							 | 
						|
								    <!-- 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="
							 | 
						|
								              offers = [];
							 | 
						|
								              projects = [];
							 | 
						|
								              showOffers = true;
							 | 
						|
								              showProjects = false;
							 | 
						|
								              loadOffers();
							 | 
						|
								            "
							 | 
						|
								            v-bind:class="computedOfferTabClassNames()"
							 | 
						|
								          >
							 | 
						|
								            Offers
							 | 
						|
								          </a>
							 | 
						|
								        </li>
							 | 
						|
								        <li>
							 | 
						|
								          <a
							 | 
						|
								            href="#"
							 | 
						|
								            @click="
							 | 
						|
								              offers = [];
							 | 
						|
								              projects = [];
							 | 
						|
								              showOffers = false;
							 | 
						|
								              showProjects = true;
							 | 
						|
								              loadProjects();
							 | 
						|
								            "
							 | 
						|
								            v-bind:class="computedProjectTabClassNames()"
							 | 
						|
								          >
							 | 
						|
								            Projects
							 | 
						|
								          </a>
							 | 
						|
								        </li>
							 | 
						|
								      </ul>
							 | 
						|
								    </div>
							 | 
						|
								
							 | 
						|
								    <!-- 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
							 | 
						|
								      v-if="showProjects"
							 | 
						|
								      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>
							 | 
						|
								
							 | 
						|
								    <!-- Offer Results List -->
							 | 
						|
								    <InfiniteScroll v-if="showOffers" @reached-bottom="loadMoreOfferData">
							 | 
						|
								      <ul class="border-t border-slate-300">
							 | 
						|
								        <li
							 | 
						|
								          class="border-b border-slate-300"
							 | 
						|
								          v-for="offer in offers"
							 | 
						|
								          :key="offer.handleId"
							 | 
						|
								        >
							 | 
						|
								          <div class="block py-4 flex gap-4">
							 | 
						|
								            <div v-if="offer.fulfillsPlanHandleId" class="flex-none w-12">
							 | 
						|
								              <ProjectIcon
							 | 
						|
								                :entityId="offer.fulfillsPlanHandleId"
							 | 
						|
								                :iconSize="48"
							 | 
						|
								                class="inline-block align-middle border border-slate-300 rounded-md"
							 | 
						|
								              ></ProjectIcon>
							 | 
						|
								            </div>
							 | 
						|
								            <div v-if="offer.recipientDid" class="flex-none w-12">
							 | 
						|
								              <EntityIcon
							 | 
						|
								                :entityId="offer.recipientDid"
							 | 
						|
								                :iconSize="48"
							 | 
						|
								                class="inline-block align-middle border border-slate-300 rounded-md"
							 | 
						|
								              />
							 | 
						|
								            </div>
							 | 
						|
								
							 | 
						|
								            <div>
							 | 
						|
								              <div>
							 | 
						|
								                {{ offer.objectDescription }}
							 | 
						|
								              </div>
							 | 
						|
								
							 | 
						|
								              <span class="text-sm">
							 | 
						|
								                <span v-if="offer.amount">
							 | 
						|
								                  <fa
							 | 
						|
								                    :icon="libsUtil.iconForUnitCode(offer.unit)"
							 | 
						|
								                    class="fa-fw text-slate-400"
							 | 
						|
								                  />
							 | 
						|
								
							 | 
						|
								                  <span v-if="offer.amountGiven >= offer.amount">
							 | 
						|
								                    <fa icon="check-circle" class="fa-fw text-green-500" />
							 | 
						|
								                    All {{ offer.amount }} given
							 | 
						|
								                  </span>
							 | 
						|
								                  <span v-else>
							 | 
						|
								                    <fa
							 | 
						|
								                      icon="triangle-exclamation"
							 | 
						|
								                      class="fa-fw text-yellow-500"
							 | 
						|
								                    />
							 | 
						|
								                    {{ offer.amountGiven ? "" : "All" }}
							 | 
						|
								                    {{ offer.amount - (offer.amountGiven || 0) }} remaining
							 | 
						|
								                  </span>
							 | 
						|
								
							 | 
						|
								                  <span v-if="offer.amountGiven > 0">
							 | 
						|
								                    <span class="text-sm text-slate-400">
							 | 
						|
								                      ({{ offer.amountGiven }} given,
							 | 
						|
								                      <span
							 | 
						|
								                        v-if="offer.amountGivenConfirmed >= offer.amountGiven"
							 | 
						|
								                      >
							 | 
						|
								                        <!-- no need for green icon; unnecessary if there's already a green, confusing if there's a yellow -->
							 | 
						|
								                        all
							 | 
						|
								                      </span>
							 | 
						|
								                      <span v-else>
							 | 
						|
								                        <!-- only show icon if there's not already a warning -->
							 | 
						|
								                        <fa
							 | 
						|
								                          v-if="offer.amountGiven >= offer.amount"
							 | 
						|
								                          icon="triangle-exclamation"
							 | 
						|
								                          class="fa-fw text-yellow-300"
							 | 
						|
								                        />
							 | 
						|
								                        {{ offer.amountGivenConfirmed || 0 }}
							 | 
						|
								                      </span>
							 | 
						|
								                      of that is confirmed)
							 | 
						|
								                    </span>
							 | 
						|
								                  </span>
							 | 
						|
								                </span>
							 | 
						|
								                <span v-else>
							 | 
						|
								                  <!-- Non-amount offer -->
							 | 
						|
								                  <span v-if="offer.nonAmountGivenConfirmed">
							 | 
						|
								                    <fa icon="check-circle" class="fa-fw text-green-500" />
							 | 
						|
								                    {{ offer.nonAmountGivenConfirmed }}
							 | 
						|
								                    {{ offer.nonAmountGivenConfirmed == 1 ? "give" : "gives" }}
							 | 
						|
								                    are confirmed.
							 | 
						|
								                  </span>
							 | 
						|
								                  <span v-else>
							 | 
						|
								                    <fa
							 | 
						|
								                      icon="triangle-exclamation"
							 | 
						|
								                      class="fa-fw text-yellow-500"
							 | 
						|
								                    />
							 | 
						|
								                    <span class="text-sm">Not confirmed by anyone</span>
							 | 
						|
								                  </span>
							 | 
						|
								                </span>
							 | 
						|
								
							 | 
						|
								                <a @click="onClickLoadClaim(offer.jwtId)">
							 | 
						|
								                  <fa
							 | 
						|
								                    icon="file-lines"
							 | 
						|
								                    class="pl-2 text-blue-500 cursor-pointer"
							 | 
						|
								                  ></fa>
							 | 
						|
								                </a>
							 | 
						|
								              </span>
							 | 
						|
								            </div>
							 | 
						|
								          </div>
							 | 
						|
								        </li>
							 | 
						|
								      </ul>
							 | 
						|
								    </InfiniteScroll>
							 | 
						|
								    <!-- Project Results List -->
							 | 
						|
								    <InfiniteScroll v-if="showProjects" @reached-bottom="loadMoreProjectData">
							 | 
						|
								      <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">
							 | 
						|
								              <ProjectIcon
							 | 
						|
								                :entityId="project.handleId"
							 | 
						|
								                :iconSize="48"
							 | 
						|
								                class="inline-block align-middle border border-slate-300 rounded-md"
							 | 
						|
								              ></ProjectIcon>
							 | 
						|
								            </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>
							 | 
						|
								  </section>
							 | 
						|
								</template>
							 | 
						|
								
							 | 
						|
								<script lang="ts">
							 | 
						|
								import { Component, Vue } from "vue-facing-decorator";
							 | 
						|
								
							 | 
						|
								import { NotificationIface } from "@/constants/app";
							 | 
						|
								import { accountsDB, db } from "@/db/index";
							 | 
						|
								import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
							 | 
						|
								import { accessToken } from "@/libs/crypto";
							 | 
						|
								import * as libsUtil from "@/libs/util";
							 | 
						|
								import { IIdentifier } from "@veramo/core";
							 | 
						|
								import InfiniteScroll from "@/components/InfiniteScroll.vue";
							 | 
						|
								import QuickNav from "@/components/QuickNav.vue";
							 | 
						|
								import ProjectIcon from "@/components/ProjectIcon.vue";
							 | 
						|
								import TopMessage from "@/components/TopMessage.vue";
							 | 
						|
								import { OfferSummaryRecord, PlanData } from "@/libs/endorserServer";
							 | 
						|
								import EntityIcon from "@/components/EntityIcon.vue";
							 | 
						|
								
							 | 
						|
								@Component({
							 | 
						|
								  components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage },
							 | 
						|
								})
							 | 
						|
								export default class ProjectsView extends Vue {
							 | 
						|
								  $notify!: (notification: NotificationIface, timeout?: number) => void;
							 | 
						|
								
							 | 
						|
								  apiServer = "";
							 | 
						|
								  projects: PlanData[] = [];
							 | 
						|
								  currentIid: IIdentifier;
							 | 
						|
								  isLoading = false;
							 | 
						|
								  numAccounts = 0;
							 | 
						|
								  offers: OfferSummaryRecord[] = [];
							 | 
						|
								  showOffers = true;
							 | 
						|
								  showProjects = false;
							 | 
						|
								
							 | 
						|
								  libsUtil = libsUtil;
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * '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: string = (settings?.activeDid as string) || "";
							 | 
						|
								      this.apiServer = (settings?.apiServer as string) || "";
							 | 
						|
								
							 | 
						|
								      await accountsDB.open();
							 | 
						|
								      this.numAccounts = await accountsDB.accounts.count();
							 | 
						|
								      if (this.numAccounts === 0) {
							 | 
						|
								        console.error("No accounts found.");
							 | 
						|
								        this.$notify(
							 | 
						|
								          {
							 | 
						|
								            group: "alert",
							 | 
						|
								            type: "danger",
							 | 
						|
								            title: "Error",
							 | 
						|
								            text: "You need an identifier to load your projects.",
							 | 
						|
								          },
							 | 
						|
								          -1,
							 | 
						|
								        );
							 | 
						|
								      } else {
							 | 
						|
								        this.currentIid = await this.getIdentity(activeDid);
							 | 
						|
								        await this.loadOffers();
							 | 
						|
								      }
							 | 
						|
								    } catch (err) {
							 | 
						|
								      console.error("Error initializing:", err);
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          title: "Error",
							 | 
						|
								          text: "Something went wrong loading your projects.",
							 | 
						|
								        },
							 | 
						|
								        -1,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Core project data loader
							 | 
						|
								   * @param url the url used to fetch the data
							 | 
						|
								   * @param token Authorization token
							 | 
						|
								   **/
							 | 
						|
								  async projectDataLoader(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: PlanData[] = resp.data.data;
							 | 
						|
								        for (const plan of plans) {
							 | 
						|
								          const { name, description, handleId, issuerDid, rowid } = plan;
							 | 
						|
								          this.projects.push({ name, description, handleId, issuerDid, rowid });
							 | 
						|
								        }
							 | 
						|
								      } else {
							 | 
						|
								        console.error(
							 | 
						|
								          "Bad server response & data for plans:",
							 | 
						|
								          resp.status,
							 | 
						|
								          resp.data,
							 | 
						|
								        );
							 | 
						|
								        this.$notify(
							 | 
						|
								          {
							 | 
						|
								            group: "alert",
							 | 
						|
								            type: "danger",
							 | 
						|
								            title: "Error",
							 | 
						|
								            text: "Failed to get projects from the server. Try again later.",
							 | 
						|
								          },
							 | 
						|
								          -1,
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								      // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								    } catch (error: any) {
							 | 
						|
								      console.error("Got error loading plans:", error.message || error);
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          title: "Error",
							 | 
						|
								          text: "Got an error loading 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 loadMoreProjectData(payload: boolean) {
							 | 
						|
								    if (this.projects.length > 0 && payload) {
							 | 
						|
								      const latestProject = this.projects[this.projects.length - 1];
							 | 
						|
								      await this.loadProjects(
							 | 
						|
								        this.currentIid,
							 | 
						|
								        `beforeId=${latestProject.rowid}`,
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Load projects initially
							 | 
						|
								   * @param identifier of the user
							 | 
						|
								   * @param urlExtra additional url parameters in a string
							 | 
						|
								   **/
							 | 
						|
								  async loadProjects(identifier?: IIdentifier, urlExtra: string = "") {
							 | 
						|
								    const identity = identifier || this.currentIid;
							 | 
						|
								    const url = `${this.apiServer}/api/v2/report/plansByIssuer?${urlExtra}`;
							 | 
						|
								    const token: string = await accessToken(identity);
							 | 
						|
								    await this.projectDataLoader(url, token);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  public async getIdentity(activeDid: string): Promise<IIdentifier> {
							 | 
						|
								    await accountsDB.open();
							 | 
						|
								    const account = await accountsDB.accounts
							 | 
						|
								      .where("did")
							 | 
						|
								      .equals(activeDid)
							 | 
						|
								      .first();
							 | 
						|
								    const identity = JSON.parse((account?.identity as string) || "null");
							 | 
						|
								
							 | 
						|
								    if (!identity) {
							 | 
						|
								      throw new Error(
							 | 
						|
								        "Attempted to load project records with no identifier available.",
							 | 
						|
								      );
							 | 
						|
								    }
							 | 
						|
								    return identity;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * 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.push(route);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Handling clicking on the new project button
							 | 
						|
								   **/
							 | 
						|
								  onClickNewProject(): void {
							 | 
						|
								    localStorage.removeItem("projectId");
							 | 
						|
								    const route = {
							 | 
						|
								      name: "new-edit-project",
							 | 
						|
								    };
							 | 
						|
								    this.$router.push(route);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  onClickLoadClaim(jwtId: string) {
							 | 
						|
								    const route = {
							 | 
						|
								      path: "/claim/" + encodeURIComponent(jwtId),
							 | 
						|
								    };
							 | 
						|
								    this.$router.push(route);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Core offer data loader
							 | 
						|
								   * @param url the url used to fetch the data
							 | 
						|
								   * @param token Authorization token
							 | 
						|
								   **/
							 | 
						|
								  async offerDataLoader(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) {
							 | 
						|
								        this.offers = this.offers.concat(resp.data.data);
							 | 
						|
								      } else {
							 | 
						|
								        console.error(
							 | 
						|
								          "Bad server response & data for offers:",
							 | 
						|
								          resp.status,
							 | 
						|
								          resp.data,
							 | 
						|
								        );
							 | 
						|
								        this.$notify(
							 | 
						|
								          {
							 | 
						|
								            group: "alert",
							 | 
						|
								            type: "danger",
							 | 
						|
								            title: "Error",
							 | 
						|
								            text: "Failed to get offers from the server. Try again later.",
							 | 
						|
								          },
							 | 
						|
								          -1,
							 | 
						|
								        );
							 | 
						|
								      }
							 | 
						|
								      // eslint-disable-next-line @typescript-eslint/no-explicit-any
							 | 
						|
								    } catch (error: any) {
							 | 
						|
								      console.error("Got error loading offers:", error.message || error);
							 | 
						|
								      this.$notify(
							 | 
						|
								        {
							 | 
						|
								          group: "alert",
							 | 
						|
								          type: "danger",
							 | 
						|
								          title: "Error",
							 | 
						|
								          text: "Got an error loading offers.",
							 | 
						|
								        },
							 | 
						|
								        -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 loadMoreOfferData(payload: boolean) {
							 | 
						|
								    if (this.offers.length > 0 && payload) {
							 | 
						|
								      const latestOffer = this.offers[this.offers.length - 1];
							 | 
						|
								      await this.loadOffers(this.currentIid, `&beforeId=${latestOffer.jwtId}`);
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  /**
							 | 
						|
								   * Load offers initially
							 | 
						|
								   * @param identifier of the user
							 | 
						|
								   * @param urlExtra additional url parameters in a string
							 | 
						|
								   **/
							 | 
						|
								  async loadOffers(identifier?: IIdentifier, urlExtra: string = "") {
							 | 
						|
								    const identity = identifier || this.currentIid;
							 | 
						|
								    const url = `${this.apiServer}/api/v2/report/offers?offeredByDid=${identity.did}${urlExtra}`;
							 | 
						|
								    const token: string = await accessToken(identity);
							 | 
						|
								    await this.offerDataLoader(url, token);
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  public computedOfferTabClassNames() {
							 | 
						|
								    return {
							 | 
						|
								      "inline-block": true,
							 | 
						|
								      "py-3": true,
							 | 
						|
								      "rounded-t-lg": true,
							 | 
						|
								      "border-b-2": true,
							 | 
						|
								
							 | 
						|
								      active: this.showOffers,
							 | 
						|
								      "text-black": this.showOffers,
							 | 
						|
								      "border-black": this.showOffers,
							 | 
						|
								      "font-semibold": this.showOffers,
							 | 
						|
								
							 | 
						|
								      "text-blue-600": !this.showOffers,
							 | 
						|
								      "border-transparent": !this.showOffers,
							 | 
						|
								      "hover:border-slate-400": !this.showOffers,
							 | 
						|
								    };
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  public computedProjectTabClassNames() {
							 | 
						|
								    return {
							 | 
						|
								      "inline-block": true,
							 | 
						|
								      "py-3": true,
							 | 
						|
								      "rounded-t-lg": true,
							 | 
						|
								      "border-b-2": true,
							 | 
						|
								
							 | 
						|
								      active: this.showProjects,
							 | 
						|
								      "text-black": this.showProjects,
							 | 
						|
								      "border-black": this.showProjects,
							 | 
						|
								      "font-semibold": this.showProjects,
							 | 
						|
								
							 | 
						|
								      "text-blue-600": !this.showProjects,
							 | 
						|
								      "border-transparent": !this.showProjects,
							 | 
						|
								      "hover:border-slate-400": !this.showProjects,
							 | 
						|
								    };
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								</script>
							 | 
						|
								
							 |