|
|
|
<template>
|
|
|
|
<QuickNav />
|
|
|
|
<TopMessage />
|
|
|
|
|
|
|
|
<!-- CONTENT -->
|
|
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
|
|
<!-- Breadcrumb -->
|
|
|
|
<div id="ViewBreadcrumb" class="mb-8">
|
|
|
|
<h1 class="text-lg text-center 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>
|
|
|
|
Idea
|
|
|
|
</h1>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Project Details -->
|
|
|
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
|
|
|
<div>
|
|
|
|
<div class="block pb-4 flex gap-4">
|
|
|
|
<div class="flex-none w-16 pt-1">
|
|
|
|
<ProjectIcon
|
|
|
|
:entityId="projectId"
|
|
|
|
:iconSize="64"
|
|
|
|
class="block border border-slate-300 rounded-md"
|
|
|
|
></ProjectIcon>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="overflow-hidden">
|
|
|
|
<h2 class="text-xl font-semibold">{{ name }}</h2>
|
|
|
|
<div class="text-sm mb-3">
|
|
|
|
<div class="truncate">
|
|
|
|
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
|
|
|
{{
|
|
|
|
serverUtil.didInfo(issuer, activeDid, allMyDids, allContacts)
|
|
|
|
}}
|
|
|
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
|
|
|
|
<button
|
|
|
|
@click="
|
|
|
|
libsUtil.doCopyTwoSecRedo(
|
|
|
|
issuer,
|
|
|
|
() => (showDidCopy = !showDidCopy),
|
|
|
|
)
|
|
|
|
"
|
|
|
|
class="ml-2 mr-2"
|
|
|
|
>
|
|
|
|
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
|
|
|
</button>
|
|
|
|
<span v-show="showDidCopy">Copied DID</span>
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
<div v-if="timeSince">
|
|
|
|
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
|
|
|
{{ timeSince }}
|
|
|
|
</div>
|
|
|
|
<div v-if="latitude || longitude">
|
|
|
|
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
|
|
|
|
<a
|
|
|
|
:href="getOpenStreetMapUrl()"
|
|
|
|
target="_blank"
|
|
|
|
class="underline"
|
|
|
|
>Map View
|
|
|
|
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
<div v-if="url">
|
|
|
|
<fa icon="globe" class="fa-fw text-slate-400"></fa>
|
|
|
|
<a :href="addScheme(url)" target="_blank" class="underline"
|
|
|
|
>{{ domainForWebsite(this.url) }}
|
|
|
|
</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>
|
|
|
|
|
|
|
|
<button
|
|
|
|
v-if="activeDid === issuer || activeDid === agentDid"
|
|
|
|
type="button"
|
|
|
|
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
|
|
|
@click="onEditClick()"
|
|
|
|
>
|
|
|
|
Edit
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="activeDid" class="mb-4">
|
|
|
|
<div class="text-center">
|
|
|
|
<button
|
|
|
|
@click="openOfferDialog()"
|
|
|
|
class="block w-full text-lg font-bold uppercase 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-2 py-3 rounded-md"
|
|
|
|
>
|
|
|
|
Offer (maybe with conditions)...
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<OfferDialog ref="customOfferDialog" :projectId="this.projectId" />
|
|
|
|
|
|
|
|
<div v-if="activeDid">
|
|
|
|
<GiftedDialog
|
|
|
|
ref="customGiveDialog"
|
|
|
|
message="Received from"
|
|
|
|
:projectId="this.projectId"
|
|
|
|
>
|
|
|
|
</GiftedDialog>
|
|
|
|
<div class="text-center">
|
|
|
|
<p class="mt-2 mb-4 text-center">Record a contribution from:</p>
|
|
|
|
</div>
|
|
|
|
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
|
|
|
<li @click="openGiftDialog({ name: 'you', did: activeDid })">
|
|
|
|
<fa icon="hand" class="fa-fw text-slate-400 text-5xl" />
|
|
|
|
<h3
|
|
|
|
class="mt-5 text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
|
|
|
>
|
|
|
|
You
|
|
|
|
</h3>
|
|
|
|
</li>
|
|
|
|
<li @click="openGiftDialog()">
|
|
|
|
<img
|
|
|
|
src="../assets/blank-square.svg"
|
|
|
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
|
|
|
/>
|
|
|
|
<h3
|
|
|
|
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
|
|
|
>
|
|
|
|
Anonymous/Unnamed
|
|
|
|
</h3>
|
|
|
|
</li>
|
|
|
|
<li
|
|
|
|
v-for="contact in allContacts.slice(0, 6)"
|
|
|
|
:key="contact.did"
|
|
|
|
@click="openGiftDialog(contact)"
|
|
|
|
>
|
|
|
|
<EntityIcon
|
|
|
|
:entityId="contact.did"
|
|
|
|
:iconSize="64"
|
|
|
|
class="mx-auto border border-slate-300 rounded-md mb-1"
|
|
|
|
/>
|
|
|
|
<h3
|
|
|
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
|
|
|
>
|
|
|
|
{{ contact.name || "(no name)" }}
|
|
|
|
</h3>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
|
|
|
<a
|
|
|
|
v-if="allContacts.length >= 7"
|
|
|
|
@click="onClickAllContactsGifting()"
|
|
|
|
class="block text-center text-md font-bold uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
|
|
|
>
|
|
|
|
Show More Contacts…
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Gifts to & from this -->
|
|
|
|
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4">
|
|
|
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
|
|
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
|
|
|
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>
|
|
|
|
|
|
|
|
<div class="bg-slate-100 px-4 py-3 rounded-md">
|
|
|
|
<h3 class="text-sm uppercase font-semibold mb-3">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"></fa>
|
|
|
|
{{
|
|
|
|
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)" @click="confirmClaim(give)">
|
|
|
|
<fa icon="circle-check" class="text-blue-500 cursor-pointer" />
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="grid items-start grid-cols-1 gap-4">
|
|
|
|
<div
|
|
|
|
v-if="fulfillersToThis.length > 0"
|
|
|
|
class="bg-slate-100 px-4 py-3 rounded-md"
|
|
|
|
>
|
|
|
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
|
|
|
Contributions To This Idea
|
|
|
|
</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>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="fulfilledByThis" class="bg-slate-100 px-4 py-3 rounded-md">
|
|
|
|
<h3 class="text-sm uppercase font-semibold mb-3">
|
|
|
|
Contributions From This Idea
|
|
|
|
</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>
|
|
|
|
</section>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
import { AxiosError, RawAxiosRequestHeaders } from "axios";
|
|
|
|
import * as moment from "moment";
|
|
|
|
import { IIdentifier } from "@veramo/core";
|
|
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
|
|
|
|
|
|
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 { accountsDB, db } from "@/db/index";
|
|
|
|
import { Account } from "@/db/tables/accounts";
|
|
|
|
import { Contact } from "@/db/tables/contacts";
|
|
|
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
|
|
|
import { accessToken } from "@/libs/crypto";
|
|
|
|
import * as libsUtil from "@/libs/util";
|
|
|
|
import {
|
|
|
|
BLANK_GENERIC_SERVER_RECORD,
|
|
|
|
GenericServerRecord,
|
|
|
|
GiverInputInfo,
|
|
|
|
GiveServerRecord,
|
|
|
|
OfferServerRecord,
|
|
|
|
PlanServerRecord,
|
|
|
|
} from "@/libs/endorserServer";
|
|
|
|
import * as serverUtil from "@/libs/endorserServer";
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
components: {
|
|
|
|
EntityIcon,
|
|
|
|
GiftedDialog,
|
|
|
|
OfferDialog,
|
|
|
|
ProjectIcon,
|
|
|
|
QuickNav,
|
|
|
|
TopMessage,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
export default class ProjectViewView extends Vue {
|
|
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
|
|
|
|
activeDid = "";
|
|
|
|
agentDid = "";
|
|
|
|
allMyDids: Array<string> = [];
|
|
|
|
allContacts: Array<Contact> = [];
|
|
|
|
apiServer = "";
|
|
|
|
description = "";
|
|
|
|
expanded = false;
|
|
|
|
fulfilledByThis: PlanServerRecord | null = null;
|
|
|
|
fulfillersToThis: Array<PlanServerRecord> = [];
|
|
|
|
givesToThis: Array<GiveServerRecord> = [];
|
|
|
|
issuer = "";
|
|
|
|
latitude = 0;
|
|
|
|
longitude = 0;
|
|
|
|
name = "";
|
|
|
|
offersToThis: Array<OfferServerRecord> = [];
|
|
|
|
projectId = localStorage.getItem("projectId") || ""; // handle ID
|
|
|
|
showDidCopy = false;
|
|
|
|
timeSince = "";
|
|
|
|
truncatedDesc = "";
|
|
|
|
truncateLength = 40;
|
|
|
|
url = "";
|
|
|
|
|
|
|
|
libsUtil = libsUtil;
|
|
|
|
serverUtil = serverUtil;
|
|
|
|
|
|
|
|
async created() {
|
|
|
|
await db.open();
|
|
|
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
|
|
|
this.activeDid = settings?.activeDid || "";
|
|
|
|
this.apiServer = settings?.apiServer || "";
|
|
|
|
this.allContacts = await db.contacts.toArray();
|
|
|
|
|
|
|
|
await accountsDB.open();
|
|
|
|
const accounts = accountsDB.accounts;
|
|
|
|
const accountsArr: Account[] = await accounts?.toArray();
|
|
|
|
this.allMyDids = accountsArr.map((acc) => acc.did);
|
|
|
|
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
|
|
|
const identity = JSON.parse(account?.identity || "null");
|
|
|
|
|
|
|
|
const pathParam = window.location.pathname.substring("/project/".length);
|
|
|
|
if (pathParam) {
|
|
|
|
this.projectId = decodeURIComponent(pathParam);
|
|
|
|
}
|
|
|
|
this.loadProject(this.projectId, identity);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async getIdentity(activeDid: string) {
|
|
|
|
await accountsDB.open();
|
|
|
|
const account = (await accountsDB.accounts
|
|
|
|
.where("did")
|
|
|
|
.equals(activeDid)
|
|
|
|
.first()) as Account;
|
|
|
|
const identity = JSON.parse(account?.identity || "null");
|
|
|
|
return identity;
|
|
|
|
}
|
|
|
|
|
|
|
|
onEditClick() {
|
|
|
|
localStorage.setItem("projectId", this.projectId as string);
|
|
|
|
const route = {
|
|
|
|
name: "new-edit-project",
|
|
|
|
};
|
|
|
|
this.$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, identity: IIdentifier) {
|
|
|
|
this.projectId = projectId;
|
|
|
|
|
|
|
|
const url =
|
|
|
|
this.apiServer + "/api/claim/byHandle/" + encodeURIComponent(projectId);
|
|
|
|
const headers: RawAxiosRequestHeaders = {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
};
|
|
|
|
if (identity) {
|
|
|
|
const token = await accessToken(identity);
|
|
|
|
headers["Authorization"] = "Bearer " + token;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
const resp = await this.axios.get(url, { headers });
|
|
|
|
if (resp.status === 200) {
|
|
|
|
const startTime = resp.data.startTime;
|
|
|
|
if (startTime != null) {
|
|
|
|
const eventDate = new Date(startTime);
|
|
|
|
const now = moment.now();
|
|
|
|
this.timeSince = moment.utc(now).to(eventDate);
|
|
|
|
}
|
|
|
|
this.agentDid = resp.data.claim?.agent?.identifier;
|
|
|
|
this.issuer = resp.data.issuer;
|
|
|
|
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. See logs for more info.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error: unknown) {
|
|
|
|
console.error("Error retrieving project:", error);
|
|
|
|
const serverError = error as AxiosError;
|
|
|
|
if (serverError.response?.status === 404) {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "That project does not exist.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "Something went wrong retrieving that project. See logs for more info.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const givesInUrl =
|
|
|
|
this.apiServer +
|
|
|
|
"/api/v2/report/givesToPlans?planIds=" +
|
|
|
|
encodeURIComponent(JSON.stringify([projectId]));
|
|
|
|
try {
|
|
|
|
const resp = await this.axios.get(givesInUrl, { headers });
|
|
|
|
if (resp.status === 200 && resp.data.data) {
|
|
|
|
this.givesToThis = resp.data.data;
|
|
|
|
} else {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "Failed to retrieve gives to this project.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error: unknown) {
|
|
|
|
const serverError = error as AxiosError;
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "Something went wrong retrieving gives to this project.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
console.error(
|
|
|
|
"Error retrieving gives to this project:",
|
|
|
|
serverError.message,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const offersToUrl =
|
|
|
|
this.apiServer +
|
|
|
|
"/api/v2/report/offersToPlans?planIds=" +
|
|
|
|
encodeURIComponent(JSON.stringify([projectId]));
|
|
|
|
try {
|
|
|
|
const resp = await this.axios.get(offersToUrl, { headers });
|
|
|
|
if (resp.status === 200 && resp.data.data) {
|
|
|
|
this.offersToThis = resp.data.data;
|
|
|
|
} else {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "Failed to retrieve offers to this project.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error: unknown) {
|
|
|
|
const serverError = error as AxiosError;
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "Something went wrong retrieving offers to this project.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
console.error(
|
|
|
|
"Error retrieving offers to this project:",
|
|
|
|
serverError.message,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const fulfilledByUrl =
|
|
|
|
this.apiServer +
|
|
|
|
"/api/v2/report/planFulfilledByPlan?planHandleId=" +
|
|
|
|
encodeURIComponent(projectId);
|
|
|
|
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.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} 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.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
console.error(
|
|
|
|
"Error retrieving plans fulfilled by this project:",
|
|
|
|
serverError.message,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const fulfillersToUrl =
|
|
|
|
this.apiServer +
|
|
|
|
"/api/v2/report/planFulfillersToPlan?planHandleId=" +
|
|
|
|
encodeURIComponent(projectId);
|
|
|
|
try {
|
|
|
|
const resp = await this.axios.get(fulfillersToUrl, { headers });
|
|
|
|
if (resp.status === 200) {
|
|
|
|
this.fulfillersToThis = resp.data.data;
|
|
|
|
} else {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "Failed to retrieve plan fulfillers to this project.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error: unknown) {
|
|
|
|
const serverError = error as AxiosError;
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "Something went wrong retrieving plan fulfillers to this project.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
console.error(
|
|
|
|
"Error retrieving plan fulfillers to this project:",
|
|
|
|
serverError.message,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle clicking on a project entry found in the list
|
|
|
|
* @param id of the project
|
|
|
|
**/
|
|
|
|
async onClickLoadProject(projectId: string) {
|
|
|
|
localStorage.setItem("projectId", projectId);
|
|
|
|
const route = {
|
|
|
|
path: "/project/" + encodeURIComponent(projectId),
|
|
|
|
};
|
|
|
|
this.$router.push(route);
|
|
|
|
this.loadProject(projectId, await this.getIdentity(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
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
openGiftDialog(contact?: GiverInputInfo) {
|
|
|
|
(this.$refs.customGiveDialog as GiftedDialog).open(contact);
|
|
|
|
}
|
|
|
|
|
|
|
|
openOfferDialog() {
|
|
|
|
(this.$refs.customOfferDialog as OfferDialog).open();
|
|
|
|
}
|
|
|
|
|
|
|
|
onClickAllContactsGifting() {
|
|
|
|
localStorage.setItem("projectId", this.projectId);
|
|
|
|
const route = {
|
|
|
|
name: "contact-gives",
|
|
|
|
};
|
|
|
|
this.$router.push(route);
|
|
|
|
}
|
|
|
|
|
|
|
|
onClickLoadClaim(jwtId: string) {
|
|
|
|
const route = {
|
|
|
|
path: "/claim/" + encodeURIComponent(jwtId),
|
|
|
|
};
|
|
|
|
this.$router.push(route);
|
|
|
|
}
|
|
|
|
|
|
|
|
checkIsFulfillable(offer: OfferServerRecord) {
|
|
|
|
const offerRecord: GenericServerRecord = {
|
|
|
|
...BLANK_GENERIC_SERVER_RECORD,
|
|
|
|
claim: offer.fullClaim,
|
|
|
|
claimType: "Offer",
|
|
|
|
issuer: offer.offeredByDid,
|
|
|
|
};
|
|
|
|
return libsUtil.canFulfillOffer(offerRecord);
|
|
|
|
}
|
|
|
|
|
|
|
|
onClickFulfillGiveToOffer(offer: OfferServerRecord) {
|
|
|
|
const offerRecord: GenericServerRecord = {
|
|
|
|
...BLANK_GENERIC_SERVER_RECORD,
|
|
|
|
claim: offer.fullClaim,
|
|
|
|
issuer: offer.offeredByDid,
|
|
|
|
};
|
|
|
|
const giver: GiverInputInfo = {
|
|
|
|
did: libsUtil.offerGiverDid(offerRecord),
|
|
|
|
};
|
|
|
|
(this.$refs.customGiveDialog as GiftedDialog).open(giver, offer.handleId);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
checkIsConfirmable(give: GiveServerRecord) {
|
|
|
|
const giveDetails: GenericServerRecord = {
|
|
|
|
...BLANK_GENERIC_SERVER_RECORD,
|
|
|
|
claim: give.fullClaim,
|
|
|
|
claimType: "GiveAction",
|
|
|
|
issuer: give.agentDid,
|
|
|
|
};
|
|
|
|
return libsUtil.isGiveRecordTheUserCanConfirm(giveDetails, this.activeDid);
|
|
|
|
}
|
|
|
|
|
|
|
|
// similar code is found in ClaimView
|
|
|
|
async confirmClaim(give: GiveServerRecord) {
|
|
|
|
if (confirm("Do you personally confirm that this is true?")) {
|
|
|
|
// 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,
|
|
|
|
await this.getIdentity(this.activeDid),
|
|
|
|
this.apiServer,
|
|
|
|
this.axios,
|
|
|
|
);
|
|
|
|
if (result.type === "success") {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "success",
|
|
|
|
title: "Success",
|
|
|
|
text: "Confirmation submitted.",
|
|
|
|
},
|
|
|
|
5000,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
console.error("Got error submitting the confirmation:", result);
|
|
|
|
const message =
|
|
|
|
(result.error?.error as string) ||
|
|
|
|
"There was a problem submitting the confirmation. See logs for more info.";
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: message,
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|