forked from jsnbuchanan/crowd-funder-for-time-pwa
fix: image server references and test configurations - Update image server references to use test server by default for local dev - Fix registration status checks in tests - Remove verbose console logging - Update environment configurations for consistent image server usage - Fix alert handling in contact registration tests - Clean up component lifecycle logging - Add clarifying comments about shared image server usage - Update playwright test configurations for better reliability This commit ensures consistent image server behavior across environments and improves test reliability by properly handling registration status checks and alerts.
1212 lines
37 KiB
Vue
1212 lines
37 KiB
Vue
<template>
|
|
<QuickNav />
|
|
<TopMessage />
|
|
|
|
<!-- CONTENT -->
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Breadcrumb -->
|
|
<div id="ViewBreadcrumb">
|
|
<div>
|
|
<h1 class="text-center text-lg font-light relative px-7">
|
|
<!-- Back -->
|
|
<button
|
|
@click="$router.go(-1)"
|
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
>
|
|
<fa icon="chevron-left" class="fa-fw"></fa>
|
|
</button>
|
|
Project Idea
|
|
</h1>
|
|
<h2 class="text-center text-xl font-semibold">
|
|
{{ name }}
|
|
<button
|
|
v-if="activeDid === issuer || activeDid === agentDid"
|
|
@click="onEditClick()"
|
|
title="Edit"
|
|
data-testId="editClaimButton"
|
|
>
|
|
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
|
</button>
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project Details -->
|
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4">
|
|
<div>
|
|
<div class="pb-4 flex gap-4">
|
|
<div class="pt-1">
|
|
<ProjectIcon
|
|
:entityId="projectId"
|
|
:iconSize="64"
|
|
:imageUrl="imageUrl"
|
|
:linkToFull="true"
|
|
class="block border border-slate-300 rounded-md max-h-16 max-w-16"
|
|
/>
|
|
</div>
|
|
|
|
<div class="overflow-hidden">
|
|
<div class="text-sm mb-3">
|
|
<div class="truncate">
|
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
|
{{ issuerInfoObject?.displayName }}
|
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
|
|
<a
|
|
:href="`/did/${issuer}`"
|
|
target="_blank"
|
|
class="text-blue-500"
|
|
>
|
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
|
</a>
|
|
</span>
|
|
<span v-else-if="serverUtil.isHiddenDid(issuer)">
|
|
<fa
|
|
icon="info-circle"
|
|
class="fa-fw text-blue-500 cursor-pointer"
|
|
@click="openHiddenDidDialog()"
|
|
/>
|
|
</span>
|
|
</div>
|
|
<div v-if="startTime">
|
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
|
Starts {{ startTime }}
|
|
</div>
|
|
<div v-if="endTime">
|
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
|
Ends {{ endTime }}
|
|
</div>
|
|
<div v-if="latitude || longitude">
|
|
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
|
|
<a
|
|
:href="getOpenStreetMapUrl()"
|
|
target="_blank"
|
|
class="underline text-blue-500"
|
|
>Map View
|
|
<fa
|
|
icon="arrow-up-right-from-square"
|
|
class="fa-fw text-blue-500"
|
|
/>
|
|
</a>
|
|
</div>
|
|
<div v-if="url">
|
|
<fa icon="globe" class="fa-fw text-slate-400"></fa>
|
|
<a
|
|
:href="addScheme(url)"
|
|
target="_blank"
|
|
class="underline text-blue-500"
|
|
>
|
|
{{ domainForWebsite(this.url) }}
|
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-sm text-slate-500">
|
|
<div v-if="!expanded">
|
|
{{ truncatedDesc }}
|
|
<a
|
|
v-if="description.length >= truncateLength"
|
|
@click="expandText"
|
|
class="uppercase text-xs font-semibold text-slate-700"
|
|
>... Read More</a
|
|
>
|
|
</div>
|
|
<div v-else>
|
|
{{ description }}
|
|
<a
|
|
@click="collapseText"
|
|
class="uppercase text-xs font-semibold text-slate-700"
|
|
>- Read Less</a
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<a @click="onClickLoadClaim(projectId)" class="cursor-pointer">
|
|
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid items-start grid-cols-1 sm:grid-cols-2 gap-4 mt-4">
|
|
<div>
|
|
<div
|
|
v-if="fulfillersToThis.length > 0"
|
|
class="bg-slate-100 px-4 py-3 rounded-md"
|
|
>
|
|
<h3 class="text-sm uppercase font-semibold mt-3">
|
|
Projects That Contribute To This
|
|
</h3>
|
|
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
|
<div class="text-center">
|
|
<div v-for="plan in fulfillersToThis" :key="plan.handleId">
|
|
<button
|
|
@click="onClickLoadProject(plan.handleId)"
|
|
class="text-blue-500"
|
|
>
|
|
{{ plan.name }}
|
|
</button>
|
|
</div>
|
|
<div v-if="fulfillersToHitLimit" class="text-center">
|
|
<button @click="loadPlanFulfillersTo()">Load More</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
|
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
|
Projects Getting Contributions From This
|
|
</h3>
|
|
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
|
<div class="text-center">
|
|
<button
|
|
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
|
class="text-blue-500"
|
|
>
|
|
{{ fulfilledByThis.name }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="activeDid && isRegistered">
|
|
<div class="text-center">
|
|
<p class="mt-2 mt-4 text-center">Record a contribution from:</p>
|
|
</div>
|
|
<ul
|
|
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5 mt-2"
|
|
>
|
|
<li @click="openGiftDialogToProject({ name: 'you', did: activeDid })">
|
|
<fa icon="hand" class="fa-fw text-blue-500 text-5xl cursor-pointer" />
|
|
<h3
|
|
class="mt-5 text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
|
|
>
|
|
You
|
|
</h3>
|
|
</li>
|
|
<li @click="openGiftDialogToProject()">
|
|
<img
|
|
src="../assets/blank-square.svg"
|
|
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
|
|
/>
|
|
<h3
|
|
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
|
|
>
|
|
Unnamed/Unknown
|
|
</h3>
|
|
</li>
|
|
<li
|
|
v-for="contact in allContacts.slice(0, 5)"
|
|
:key="contact.did"
|
|
@click="openGiftDialogToProject(contact)"
|
|
>
|
|
<EntityIcon
|
|
:contact="contact"
|
|
:iconSize="64"
|
|
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
|
|
/>
|
|
<h3
|
|
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
|
|
>
|
|
{{ contact.name || "(no name)" }}
|
|
</h3>
|
|
</li>
|
|
<li>
|
|
<span
|
|
v-if="allContacts.length >= 5"
|
|
@click="onClickAllContactsGifting()"
|
|
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
|
|
>
|
|
... or someone else...
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
<GiftedDialog ref="giveDialogToThis" :toProjectId="this.projectId" />
|
|
</div>
|
|
|
|
<!-- Offers & Gifts to & from this -->
|
|
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
|
<!-- First, offers on the left-->
|
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
|
<div v-if="activeDid && isRegistered">
|
|
<div class="text-center">
|
|
<button
|
|
data-testId="offerButton"
|
|
@click="openOfferDialog()"
|
|
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
|
|
>
|
|
Offer to this (maybe with conditions)...
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<OfferDialog
|
|
ref="customOfferDialog"
|
|
:projectId="this.projectId"
|
|
:projectName="this.name"
|
|
/>
|
|
|
|
<h3 class="text-lg font-bold mb-3 mt-4">Offered To This Idea</h3>
|
|
|
|
<div v-if="offersToThis.length === 0">
|
|
(None yet. Wanna
|
|
<span @click="openOfferDialog()" class="cursor-pointer text-blue-500"
|
|
>offer something... especially if others join you</span
|
|
>?)
|
|
</div>
|
|
|
|
<ul v-else class="text-sm border-t border-slate-300">
|
|
<li
|
|
v-for="offer in offersToThis"
|
|
:key="offer.id"
|
|
class="py-1.5 border-b border-slate-300"
|
|
>
|
|
<div class="flex justify-between gap-4">
|
|
<span>
|
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
|
{{
|
|
serverUtil.didInfo(
|
|
offer.offeredByDid,
|
|
activeDid,
|
|
allMyDids,
|
|
allContacts,
|
|
)
|
|
}}
|
|
</span>
|
|
<span v-if="offer.amount" class="whitespace-nowrap">
|
|
<fa
|
|
:icon="libsUtil.iconForUnitCode(offer.unit)"
|
|
class="fa-fw text-slate-400"
|
|
/>{{ offer.amount }}
|
|
</span>
|
|
</div>
|
|
<div v-if="offer.objectDescription" class="text-slate-500">
|
|
<fa icon="comment" class="fa-fw text-slate-400" />
|
|
{{ offer.objectDescription }}
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<a
|
|
@click="onClickLoadClaim(offer.jwtId as string)"
|
|
class="cursor-pointer"
|
|
>
|
|
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
|
|
</a>
|
|
<a
|
|
v-if="checkIsFulfillable(offer)"
|
|
@click="onClickFulfillGiveToOffer(offer)"
|
|
>
|
|
<fa
|
|
icon="hand-holding-heart"
|
|
class="text-blue-500 cursor-pointer"
|
|
/>
|
|
</a>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<div v-if="offersHitLimit" class="text-center text-blue-500">
|
|
<button @click="loadOffers()">Load More</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Now, gives TO this project in the middle -->
|
|
<!-- (similar to "FROM" gift display below) -->
|
|
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-to">
|
|
<div v-if="activeDid && isRegistered">
|
|
<div class="text-center">
|
|
<button
|
|
@click="openGiftDialogToProject()"
|
|
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1rounded-md"
|
|
>
|
|
Given To This...
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 class="text-lg font-bold mb-3 mt-4">Given To This Idea</h3>
|
|
|
|
<div v-if="givesToThis.length === 0">
|
|
(None yet. If you've seen something, say something by clicking a
|
|
contact above.)
|
|
</div>
|
|
|
|
<ul v-else class="text-sm border-t border-slate-300">
|
|
<li
|
|
v-for="give in givesToThis"
|
|
:key="give.id"
|
|
class="py-1.5 border-b border-slate-300"
|
|
>
|
|
<div class="flex justify-between gap-4">
|
|
<span>
|
|
<fa icon="user" class="fa-fw text-slate-400" />
|
|
{{
|
|
serverUtil.didInfo(
|
|
give.agentDid,
|
|
activeDid,
|
|
allMyDids,
|
|
allContacts,
|
|
)
|
|
}}
|
|
</span>
|
|
<span v-if="give.amount" class="whitespace-nowrap">
|
|
<fa
|
|
:icon="libsUtil.iconForUnitCode(give.unit)"
|
|
class="fa-fw text-slate-400"
|
|
/>{{ give.amount }}
|
|
</span>
|
|
</div>
|
|
<div class="text-slate-500">
|
|
<fa icon="calendar" class="fa-fw text-slate-400" />
|
|
{{ give.issuedAt?.substring(0, 10) }}
|
|
</div>
|
|
<div v-if="give.description" class="text-slate-500">
|
|
<fa icon="comment" class="fa-fw text-slate-400" />
|
|
{{ give.description }}
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<a @click="onClickLoadClaim(give.jwtId)">
|
|
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
|
|
</a>
|
|
|
|
<a
|
|
v-if="
|
|
checkIsConfirmable(give) &&
|
|
!recentlyCheckedAndUnconfirmableJwts.includes(give.jwtId)
|
|
"
|
|
@click="deepCheckConfirmable(give)"
|
|
>
|
|
<fa icon="circle-check" class="text-blue-500 cursor-pointer" />
|
|
</a>
|
|
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
|
|
<fa icon="spinner" class="fa-spin-pulse" />
|
|
</a>
|
|
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
|
|
<fa icon="circle-check" class="text-slate-500 cursor-pointer" />
|
|
</a>
|
|
</div>
|
|
<div v-if="give.fullClaim.image" class="flex justify-center">
|
|
<a :href="give.fullClaim.image" target="_blank">
|
|
<img :src="give.fullClaim.image" class="h-24 mt-2 rounded-xl" />
|
|
</a>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<div v-if="givesHitLimit" class="text-center text-blue-500">
|
|
<button @click="loadGives()">Load More</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Finally, gives FROM this project on the right -->
|
|
<!-- (similar to "TO" gift display above) -->
|
|
<div class="bg-slate-100 px-4 py-3 rounded-md" data-testId="gives-from">
|
|
<div v-if="activeDid && isRegistered">
|
|
<div class="text-center">
|
|
<button
|
|
@click="openGiftDialogFromProject()"
|
|
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
|
|
>
|
|
Given By This...
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<GiftedDialog
|
|
ref="giveDialogFromThis"
|
|
:fromProjectId="this.projectId"
|
|
/>
|
|
|
|
<h3 class="text-lg font-bold mb-3 mt-4">
|
|
Benefitted From This Project
|
|
</h3>
|
|
|
|
<div v-if="givesProvidedByThis.length === 0">(None yet.)</div>
|
|
|
|
<ul v-else class="text-sm border-t border-slate-300">
|
|
<li
|
|
v-for="give in givesProvidedByThis"
|
|
:key="give.id"
|
|
class="py-1.5 border-b border-slate-300"
|
|
>
|
|
<div class="flex justify-between gap-4">
|
|
<span>
|
|
{{
|
|
serverUtil.didInfo(
|
|
give.recipientDid,
|
|
activeDid,
|
|
allMyDids,
|
|
allContacts,
|
|
)
|
|
}}
|
|
</span>
|
|
<span v-if="give.amount" class="whitespace-nowrap">
|
|
<fa
|
|
:icon="libsUtil.iconForUnitCode(give.unit)"
|
|
class="fa-fw text-slate-400"
|
|
/>{{ give.amount }}
|
|
</span>
|
|
</div>
|
|
<div class="text-slate-500">
|
|
<fa icon="calendar" class="fa-fw text-slate-400" />
|
|
{{ give.issuedAt?.substring(0, 10) }}
|
|
</div>
|
|
<div v-if="give.description" class="text-slate-500">
|
|
<fa icon="comment" class="fa-fw text-slate-400" />
|
|
{{ give.description }}
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<a @click="onClickLoadClaim(give.jwtId)">
|
|
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
|
|
</a>
|
|
|
|
<a
|
|
v-if="
|
|
checkIsConfirmable(give) &&
|
|
!recentlyCheckedAndUnconfirmableJwts.includes(give.jwtId)
|
|
"
|
|
@click="deepCheckConfirmable(give)"
|
|
>
|
|
<fa icon="circle-check" class="text-blue-500 cursor-pointer" />
|
|
</a>
|
|
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
|
|
<fa icon="spinner" class="fa-spin-pulse" />
|
|
</a>
|
|
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
|
|
<fa icon="circle-check" class="text-slate-500 cursor-pointer" />
|
|
</a>
|
|
</div>
|
|
<div v-if="give.fullClaim.image" class="flex justify-center">
|
|
<a :href="give.fullClaim.image" target="_blank">
|
|
<img :src="give.fullClaim.image" class="h-24 mt-2 rounded-xl" />
|
|
</a>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<div v-if="givesProvidedByHitLimit" class="text-center">
|
|
<button @click="loadGivesProvidedBy()">Load More</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<HiddenDidDialog ref="hiddenDidDialog" />
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { AxiosError } from "axios";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { Router } from "vue-router";
|
|
|
|
import GiftedDialog from "../components/GiftedDialog.vue";
|
|
import OfferDialog from "../components/OfferDialog.vue";
|
|
import TopMessage from "../components/TopMessage.vue";
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import EntityIcon from "../components/EntityIcon.vue";
|
|
import ProjectIcon from "../components/ProjectIcon.vue";
|
|
import { NotificationIface } from "../constants/app";
|
|
import {
|
|
db,
|
|
logConsoleAndDb,
|
|
retrieveSettingsForActiveAccount,
|
|
} from "../db/index";
|
|
import { Contact } from "../db/tables/contacts";
|
|
import * as libsUtil from "../libs/util";
|
|
import {
|
|
GenericCredWrapper,
|
|
GiveSummaryRecord,
|
|
GiveVerifiableCredential,
|
|
OfferSummaryRecord,
|
|
OfferVerifiableCredential,
|
|
PlanSummaryRecord,
|
|
} from "../libs/endorserServer";
|
|
import * as serverUtil from "../libs/endorserServer";
|
|
import { retrieveAccountDids } from "../libs/util";
|
|
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
|
|
|
@Component({
|
|
components: {
|
|
EntityIcon,
|
|
GiftedDialog,
|
|
HiddenDidDialog,
|
|
OfferDialog,
|
|
ProjectIcon,
|
|
QuickNav,
|
|
TopMessage,
|
|
},
|
|
})
|
|
export default class ProjectViewView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
activeDid = "";
|
|
agentDid = "";
|
|
agentDidVisibleToDids: Array<string> = [];
|
|
allMyDids: Array<string> = [];
|
|
allContacts: Array<Contact> = [];
|
|
apiServer = "";
|
|
checkingConfirmationForJwtId = "";
|
|
description = "";
|
|
endTime = "";
|
|
expanded = false;
|
|
fulfilledByThis: PlanSummaryRecord | null = null;
|
|
fulfillersToThis: Array<PlanSummaryRecord> = [];
|
|
fulfillersToHitLimit = false;
|
|
givesToThis: Array<GiveSummaryRecord> = [];
|
|
givesHitLimit = false;
|
|
givesProvidedByThis: Array<GiveSummaryRecord> = [];
|
|
givesProvidedByHitLimit = false;
|
|
imageUrl = "";
|
|
isRegistered = false;
|
|
issuer = "";
|
|
issuerInfoObject: {
|
|
known: boolean;
|
|
displayName: string;
|
|
profileImageUrl?: string;
|
|
} | null = null;
|
|
issuerVisibleToDids: Array<string> = [];
|
|
latitude = 0;
|
|
longitude = 0;
|
|
name = "";
|
|
offersToThis: Array<OfferSummaryRecord> = [];
|
|
offersHitLimit = false;
|
|
projectId = ""; // handle ID
|
|
recentlyCheckedAndUnconfirmableJwts: string[] = [];
|
|
startTime = "";
|
|
truncatedDesc = "";
|
|
truncateLength = 40;
|
|
url = "";
|
|
|
|
libsUtil = libsUtil;
|
|
serverUtil = serverUtil;
|
|
|
|
async created() {
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
this.allContacts = await db.contacts.toArray();
|
|
this.isRegistered = !!settings.isRegistered;
|
|
|
|
try {
|
|
this.allMyDids = await retrieveAccountDids();
|
|
} catch (error) {
|
|
// continue because we want to see claims, even anonymously
|
|
logConsoleAndDb(
|
|
"Error retrieving all account DIDs on home page:" + error,
|
|
true,
|
|
);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error Loading Profile",
|
|
text: "See the Help page to fix problems with your personal data.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
const pathParam = window.location.pathname.substring("/project/".length);
|
|
if (pathParam) {
|
|
this.projectId = decodeURIComponent(pathParam);
|
|
}
|
|
this.loadProject(this.projectId, this.activeDid);
|
|
}
|
|
|
|
onEditClick() {
|
|
const route = {
|
|
name: "new-edit-project",
|
|
query: { projectId: this.projectId },
|
|
};
|
|
(this.$router as Router).push(route);
|
|
}
|
|
|
|
// Isn't there a better way to make this available to the template?
|
|
expandText() {
|
|
this.expanded = true;
|
|
}
|
|
|
|
collapseText() {
|
|
this.expanded = false;
|
|
}
|
|
|
|
async loadProject(projectId: string, userDid: string) {
|
|
this.projectId = projectId;
|
|
|
|
const url =
|
|
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId);
|
|
const headers = await serverUtil.getHeaders(userDid);
|
|
|
|
try {
|
|
const resp = await this.axios.get(url, { headers });
|
|
if (resp.status === 200) {
|
|
const startTime = resp.data.claim?.startTime;
|
|
if (startTime != null) {
|
|
const startDateTime = new Date(startTime);
|
|
this.startTime =
|
|
startDateTime.toLocaleDateString() +
|
|
" " +
|
|
startDateTime.toLocaleTimeString();
|
|
}
|
|
const endTime = resp.data.claim?.endTime;
|
|
if (endTime != null) {
|
|
const endDateTime = new Date(endTime);
|
|
this.endTime =
|
|
endDateTime.toLocaleDateString() +
|
|
" " +
|
|
endDateTime.toLocaleTimeString();
|
|
}
|
|
this.agentDid = resp.data.claim?.agent?.identifier;
|
|
this.agentDidVisibleToDids =
|
|
resp.data.claim?.agent?.identifierVisibleToDids || [];
|
|
this.imageUrl = resp.data.claim?.image;
|
|
this.issuer = resp.data.issuer;
|
|
this.issuerInfoObject = serverUtil.didInfoObject(
|
|
this.issuer,
|
|
this.activeDid,
|
|
this.allMyDids,
|
|
this.allContacts,
|
|
);
|
|
this.issuerVisibleToDids = resp.data.issuerVisibleToDids || [];
|
|
this.name = resp.data.claim?.name || "(no name)";
|
|
this.description = resp.data.claim?.description || "(no description)";
|
|
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
|
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
|
|
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
|
|
this.url = resp.data.claim?.url || "";
|
|
} else {
|
|
// actually, axios throws an error on 404 so we probably never get here
|
|
console.error("Error getting project:", resp);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "There was a problem getting that project.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} catch (error: unknown) {
|
|
console.error("Error retrieving project:", error);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Something went wrong retrieving that project.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
|
|
this.givesToThis = [];
|
|
this.loadGives();
|
|
|
|
this.givesProvidedByThis = [];
|
|
this.loadGivesProvidedBy();
|
|
|
|
this.offersToThis = [];
|
|
this.loadOffers();
|
|
|
|
this.fulfillersToThis = [];
|
|
this.loadPlanFulfillersTo();
|
|
|
|
this.fulfilledByThis = null;
|
|
this.loadPlanFulfilledBy();
|
|
}
|
|
|
|
async loadGives() {
|
|
const givesUrl =
|
|
this.apiServer +
|
|
"/api/v2/report/givesToPlans?planIds=" +
|
|
encodeURIComponent(JSON.stringify([this.projectId]));
|
|
let postfix = "";
|
|
if (this.givesToThis.length > 0) {
|
|
postfix =
|
|
"&beforeId=" + this.givesToThis[this.givesToThis.length - 1].jwtId;
|
|
}
|
|
const givesInUrl = givesUrl + postfix;
|
|
|
|
const headers = await serverUtil.getHeaders(this.activeDid);
|
|
try {
|
|
const resp = await this.axios.get(givesInUrl, { headers });
|
|
if (resp.status === 200 && resp.data.data) {
|
|
this.givesToThis = this.givesToThis.concat(resp.data.data);
|
|
this.givesHitLimit = resp.data.hitLimit;
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Failed to retrieve more gives to this project.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} catch (error: unknown) {
|
|
const serverError = error as AxiosError;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Something went wrong retrieving more gives to this project.",
|
|
},
|
|
5000,
|
|
);
|
|
console.error(
|
|
"Something went wrong retrieving more gives to this project:",
|
|
serverError.message,
|
|
);
|
|
}
|
|
}
|
|
|
|
async loadGivesProvidedBy() {
|
|
const providedByUrl =
|
|
this.apiServer +
|
|
"/api/v2/report/givesProvidedBy?providerId=" +
|
|
encodeURIComponent(this.projectId);
|
|
let postfix = "";
|
|
if (this.givesProvidedByThis.length > 0) {
|
|
postfix =
|
|
"&beforeId=" +
|
|
this.givesProvidedByThis[this.givesProvidedByThis.length - 1].jwtId;
|
|
}
|
|
const providedByFullUrl = providedByUrl + postfix;
|
|
|
|
const headers = await serverUtil.getHeaders(this.activeDid);
|
|
try {
|
|
const resp = await this.axios.get(providedByFullUrl, { headers });
|
|
if (resp.status === 200) {
|
|
this.givesProvidedByThis = this.givesProvidedByThis.concat(
|
|
resp.data.data,
|
|
);
|
|
this.givesProvidedByHitLimit = resp.data.hitLimit;
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Failed to retrieve gives that were provided by this project.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} catch (error: unknown) {
|
|
const serverError = error as AxiosError;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Something went wrong retrieving gives that were provided by this project.",
|
|
},
|
|
5000,
|
|
);
|
|
console.error(
|
|
"Something went wrong retrieving gives that were provided by this project:",
|
|
serverError.message,
|
|
);
|
|
}
|
|
}
|
|
|
|
async loadOffers() {
|
|
const offersUrl =
|
|
this.apiServer +
|
|
"/api/v2/report/offersToPlans?planIds=" +
|
|
encodeURIComponent(JSON.stringify([this.projectId]));
|
|
let postfix = "";
|
|
if (this.offersToThis.length > 0) {
|
|
postfix =
|
|
"&beforeId=" + this.offersToThis[this.offersToThis.length - 1].jwtId;
|
|
}
|
|
const offersInUrl = offersUrl + postfix;
|
|
|
|
const headers = await serverUtil.getHeaders(this.activeDid);
|
|
try {
|
|
const resp = await this.axios.get(offersInUrl, { headers });
|
|
if (resp.status === 200 && resp.data.data) {
|
|
this.offersToThis = this.offersToThis.concat(resp.data.data);
|
|
this.offersHitLimit = resp.data.hitLimit;
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Failed to retrieve more offers to this project.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} catch (error: unknown) {
|
|
const serverError = error as AxiosError;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Something went wrong retrieving more offers to this project.",
|
|
},
|
|
5000,
|
|
);
|
|
console.error(
|
|
"Something went wrong retrieving more offers to this project:",
|
|
serverError.message,
|
|
);
|
|
}
|
|
}
|
|
|
|
async loadPlanFulfillersTo() {
|
|
const fulfillsUrl =
|
|
this.apiServer +
|
|
"/api/v2/report/planFulfillersToPlan?planHandleId=" +
|
|
encodeURIComponent(this.projectId);
|
|
let postfix = "";
|
|
if (this.fulfillersToThis.length > 0) {
|
|
postfix =
|
|
"&beforeId=" +
|
|
this.fulfillersToThis[this.fulfillersToThis.length - 1].jwtId;
|
|
}
|
|
const fulfillsInUrl = fulfillsUrl + postfix;
|
|
|
|
const headers = await serverUtil.getHeaders(this.activeDid);
|
|
try {
|
|
const resp = await this.axios.get(fulfillsInUrl, { headers });
|
|
if (resp.status === 200) {
|
|
this.fulfillersToThis = this.fulfillersToThis.concat(resp.data.data);
|
|
this.fulfillersToHitLimit = resp.data.hitLimit;
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Failed to retrieve more plans that fullfill this project.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} catch (error: unknown) {
|
|
const serverError = error as AxiosError;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Something went wrong retrieving more plans that fulfull this project.",
|
|
},
|
|
5000,
|
|
);
|
|
console.error(
|
|
"Something went wrong retrieving more plans that fulfill this project:",
|
|
serverError.message,
|
|
);
|
|
}
|
|
}
|
|
|
|
async loadPlanFulfilledBy() {
|
|
const fulfilledByUrl =
|
|
this.apiServer +
|
|
"/api/v2/report/planFulfilledByPlan?planHandleId=" +
|
|
encodeURIComponent(this.projectId);
|
|
const headers = await serverUtil.getHeaders(this.activeDid);
|
|
try {
|
|
const resp = await this.axios.get(fulfilledByUrl, { headers });
|
|
if (resp.status === 200) {
|
|
this.fulfilledByThis = resp.data.data;
|
|
} else {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Failed to retrieve plans fulfilled by this project.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
} catch (error: unknown) {
|
|
const serverError = error as AxiosError;
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: "Something went wrong retrieving plans fulfilled by this project.",
|
|
},
|
|
5000,
|
|
);
|
|
console.error(
|
|
"Error retrieving plans fulfilled by this project:",
|
|
serverError.message,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle clicking on a project entry found in the list
|
|
* @param id of the project
|
|
**/
|
|
async onClickLoadProject(projectId: string) {
|
|
const route = {
|
|
path: "/project/" + encodeURIComponent(projectId),
|
|
};
|
|
(this.$router as Router).push(route);
|
|
this.loadProject(projectId, this.activeDid);
|
|
}
|
|
|
|
getOpenStreetMapUrl() {
|
|
// Google URL is https://maps.google.com/?q=LAT,LONG
|
|
return (
|
|
"https://www.openstreetmap.org/?mlat=" +
|
|
this.latitude +
|
|
"&mlon=" +
|
|
this.longitude +
|
|
"#map=15/" +
|
|
this.latitude +
|
|
"/" +
|
|
this.longitude
|
|
);
|
|
}
|
|
|
|
openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) {
|
|
(this.$refs.giveDialogToThis as GiftedDialog).open(
|
|
contact,
|
|
undefined,
|
|
undefined,
|
|
(contact?.name || "Someone not named") + ` gave to this project`,
|
|
);
|
|
}
|
|
|
|
openGiftDialogFromProject() {
|
|
(this.$refs.giveDialogFromThis as GiftedDialog).open(
|
|
undefined,
|
|
{ did: this.activeDid, name: "You" },
|
|
undefined,
|
|
`This project gave to you`,
|
|
);
|
|
}
|
|
|
|
openOfferDialog() {
|
|
(this.$refs.customOfferDialog as OfferDialog).open();
|
|
}
|
|
|
|
onClickAllContactsGifting() {
|
|
const route = {
|
|
name: "contact-gift",
|
|
query: {
|
|
projectId: this.projectId,
|
|
},
|
|
};
|
|
(this.$router as Router).push(route);
|
|
}
|
|
|
|
onClickLoadClaim(jwtId: string) {
|
|
const route = {
|
|
path: "/claim/" + encodeURIComponent(jwtId),
|
|
};
|
|
(this.$router as Router).push(route);
|
|
}
|
|
|
|
checkIsFulfillable(offer: OfferSummaryRecord) {
|
|
const offerRecord: GenericCredWrapper<OfferVerifiableCredential> = {
|
|
...serverUtil.BLANK_GENERIC_SERVER_RECORD,
|
|
claim: offer.fullClaim,
|
|
claimType: "Offer",
|
|
issuer: offer.offeredByDid,
|
|
};
|
|
return libsUtil.canFulfillOffer(offerRecord);
|
|
}
|
|
|
|
onClickFulfillGiveToOffer(offer: OfferSummaryRecord) {
|
|
const offerRecord: GenericCredWrapper<OfferVerifiableCredential> = {
|
|
...serverUtil.BLANK_GENERIC_SERVER_RECORD,
|
|
claim: offer.fullClaim,
|
|
issuer: offer.offeredByDid,
|
|
};
|
|
const giver: libsUtil.GiverReceiverInputInfo = {
|
|
did: libsUtil.offerGiverDid(offerRecord),
|
|
};
|
|
(this.$refs.giveDialogToThis as GiftedDialog).open(
|
|
giver,
|
|
undefined,
|
|
offer.handleId,
|
|
"Given by " + (giver?.name || "someone not named"),
|
|
);
|
|
}
|
|
|
|
// return an HTTPS URL if it's not a global URL
|
|
addScheme(url: string) {
|
|
if (!libsUtil.isGlobalUri(url)) {
|
|
return "https://" + url;
|
|
}
|
|
return url;
|
|
}
|
|
|
|
// return just the domain for display, if possible
|
|
domainForWebsite(url: string) {
|
|
try {
|
|
const hostname = new URL(url).hostname;
|
|
if (!hostname) {
|
|
// happens for non-http URLs
|
|
return url;
|
|
} else if (url.endsWith(hostname)) {
|
|
// it's just the domain
|
|
return hostname;
|
|
} else {
|
|
// there's more, but don't bother displaying the whole thing
|
|
return hostname + "...";
|
|
}
|
|
} catch (error: unknown) {
|
|
// must not be a valid URL
|
|
return url;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param confirmerIdList optional list of DIDs who confirmed; if missing, doesn't do a full server check
|
|
*/
|
|
checkIsConfirmable(give: GiveSummaryRecord, confirmerIdList?: string[]) {
|
|
const giveDetails: GenericCredWrapper<GiveVerifiableCredential> = {
|
|
...serverUtil.BLANK_GENERIC_SERVER_RECORD,
|
|
claim: give.fullClaim,
|
|
claimType: "GiveAction",
|
|
issuer: give.issuerDid,
|
|
};
|
|
return libsUtil.isGiveRecordTheUserCanConfirm(
|
|
this.isRegistered,
|
|
giveDetails,
|
|
this.activeDid,
|
|
confirmerIdList,
|
|
);
|
|
}
|
|
|
|
shallowNotifyWhyCannotConfirm(give: GiveSummaryRecord) {
|
|
const confirmerIds = this.recentlyCheckedAndUnconfirmableJwts.includes(
|
|
give.jwtId,
|
|
)
|
|
? [this.activeDid]
|
|
: [];
|
|
libsUtil.notifyWhyCannotConfirm(
|
|
this.$notify,
|
|
this.isRegistered,
|
|
"GiveAction",
|
|
give,
|
|
this.activeDid,
|
|
confirmerIds,
|
|
);
|
|
}
|
|
|
|
async deepCheckConfirmable(give: GiveSummaryRecord) {
|
|
this.checkingConfirmationForJwtId = give.jwtId;
|
|
const confirmerInfo: libsUtil.ConfirmerData | undefined =
|
|
await libsUtil.retrieveConfirmerIdList(
|
|
this.apiServer,
|
|
give.jwtId,
|
|
give.issuerDid,
|
|
this.activeDid,
|
|
);
|
|
if (
|
|
this.checkIsConfirmable(give, confirmerInfo?.confirmerIdList as string[])
|
|
) {
|
|
this.confirmConfirmClaim(give);
|
|
} else {
|
|
this.recentlyCheckedAndUnconfirmableJwts = [
|
|
...this.recentlyCheckedAndUnconfirmableJwts,
|
|
give.jwtId,
|
|
];
|
|
libsUtil.notifyWhyCannotConfirm(
|
|
this.$notify,
|
|
this.isRegistered,
|
|
"GiveAction",
|
|
give,
|
|
this.activeDid,
|
|
confirmerInfo?.confirmerIdList as string[],
|
|
);
|
|
}
|
|
this.checkingConfirmationForJwtId = "";
|
|
}
|
|
|
|
confirmConfirmClaim(give: GiveSummaryRecord) {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Confirm",
|
|
text: "Do you personally confirm that this is true?",
|
|
onYes: async () => {
|
|
await this.confirmClaim(give);
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
// similar code is found in ClaimView
|
|
async confirmClaim(give: GiveSummaryRecord) {
|
|
// similar logic is found in endorser-mobile
|
|
const goodClaim = serverUtil.removeSchemaContext(
|
|
serverUtil.removeVisibleToDids(
|
|
serverUtil.addLastClaimOrHandleAsIdIfMissing(
|
|
give.fullClaim,
|
|
give.jwtId,
|
|
give.handleId,
|
|
),
|
|
),
|
|
);
|
|
const confirmationClaim: serverUtil.GenericVerifiableCredential = {
|
|
"@context": "https://schema.org",
|
|
"@type": "AgreeAction",
|
|
object: goodClaim,
|
|
};
|
|
const result = await serverUtil.createAndSubmitClaim(
|
|
confirmationClaim,
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
);
|
|
if (result.type === "success") {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "success",
|
|
title: "Success",
|
|
text: "Confirmation submitted.",
|
|
},
|
|
5000,
|
|
);
|
|
this.recentlyCheckedAndUnconfirmableJwts = [
|
|
...this.recentlyCheckedAndUnconfirmableJwts,
|
|
give.jwtId,
|
|
];
|
|
} else {
|
|
console.error("Got error submitting the confirmation:", result);
|
|
const message =
|
|
(result.error?.error as string) ||
|
|
"There was a problem submitting the confirmation.";
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: message,
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
}
|
|
|
|
openHiddenDidDialog() {
|
|
(this.$refs.hiddenDidDialog as HiddenDidDialog).open(
|
|
"creator",
|
|
this.issuerVisibleToDids,
|
|
this.allContacts,
|
|
this.activeDid,
|
|
this.allMyDids,
|
|
);
|
|
}
|
|
}
|
|
</script>
|