forked from trent_larson/crowd-funder-for-time-pwa
add display of my own offers
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
|
- change server plan & project endpoints to use jwtId as identifier rather than rowid
|
||||||
|
- on mobile, don't capitalize first word & don't add spaces of website entry, and don't add spaces in numeric entry
|
||||||
|
- edit offers & gives, or revoke allowing recreation
|
||||||
|
- bug (that is hard to reproduce) - offer gave USD (by default?)
|
||||||
|
- .1 When available in the server, give message for 'nonAmountGiven' on offers on ProjectsView page.
|
||||||
- .5 fix timeSafari.org cert renewals
|
- .5 fix timeSafari.org cert renewals
|
||||||
- .2 anchor hash into BTC
|
- .2 anchor hash into BTC
|
||||||
- 04 allow backup of localStorage key
|
- 04 allow backup of localStorage key
|
||||||
|
|||||||
@@ -80,9 +80,13 @@ export interface GiveServerRecord {
|
|||||||
export interface OfferServerRecord {
|
export interface OfferServerRecord {
|
||||||
amount: number;
|
amount: number;
|
||||||
amountGiven: number;
|
amountGiven: number;
|
||||||
|
amountGivenConfirmed: number;
|
||||||
fullClaim: OfferVerifiableCredential;
|
fullClaim: OfferVerifiableCredential;
|
||||||
fulfillsPlanHandleId: string;
|
fulfillsPlanHandleId: string;
|
||||||
handleId: string;
|
handleId: string;
|
||||||
|
jwtId: string;
|
||||||
|
nonAmountGivenConfirmed: number;
|
||||||
|
objectDescription: string;
|
||||||
offeredByDid: string;
|
offeredByDid: string;
|
||||||
recipientDid: string;
|
recipientDid: string;
|
||||||
requirementsMet: boolean;
|
requirementsMet: boolean;
|
||||||
@@ -145,6 +149,57 @@ export interface PlanVerifiableCredential {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents data about a project
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
|
* We should use PlanServerRecord instead.
|
||||||
|
**/
|
||||||
|
export interface PlanData {
|
||||||
|
/**
|
||||||
|
* Name of the project
|
||||||
|
**/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Description of the project
|
||||||
|
**/
|
||||||
|
description: string;
|
||||||
|
/**
|
||||||
|
* URL referencing information about the project
|
||||||
|
**/
|
||||||
|
handleId: string;
|
||||||
|
/**
|
||||||
|
* The DID of the issuer
|
||||||
|
*/
|
||||||
|
issuerDid: string;
|
||||||
|
/**
|
||||||
|
* The Identier of the project -- different from jwtId, needs to be fixed
|
||||||
|
**/
|
||||||
|
rowid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RateLimits {
|
||||||
|
doneClaimsThisWeek: string;
|
||||||
|
doneRegistrationsThisMonth: string;
|
||||||
|
maxClaimsPerWeek: string;
|
||||||
|
maxRegistrationsPerMonth: string;
|
||||||
|
nextMonthBeginDateTime: string;
|
||||||
|
nextWeekBeginDateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifiableCredential {
|
||||||
|
"@context": string;
|
||||||
|
"@type": string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
identifier?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorldProperties {
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RegisterVerifiableCredential {
|
export interface RegisterVerifiableCredential {
|
||||||
"@context": string;
|
"@context": string;
|
||||||
"@type": string;
|
"@type": string;
|
||||||
@@ -153,11 +208,35 @@ export interface RegisterVerifiableCredential {
|
|||||||
participant: { identifier: string };
|
participant: { identifier: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// now for some of the error & other wrapper types
|
||||||
|
|
||||||
|
export interface ResultWithType {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SuccessResult extends ResultWithType {
|
||||||
|
type: "success";
|
||||||
|
response: AxiosResponse<ClaimResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
error?: {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResult {
|
||||||
|
type: "error";
|
||||||
|
error: InternalError;
|
||||||
|
}
|
||||||
|
|
||||||
export interface InternalError {
|
export interface InternalError {
|
||||||
error: string; // for system logging
|
error: string; // for system logging
|
||||||
userMessage?: string; // for user display
|
userMessage?: string; // for user display
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
||||||
|
|
||||||
// This is used to check for hidden info.
|
// This is used to check for hidden info.
|
||||||
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
||||||
const HIDDEN_DID = "did:none:HIDDEN";
|
const HIDDEN_DID = "did:none:HIDDEN";
|
||||||
@@ -294,22 +373,6 @@ export function didInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultWithType {
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SuccessResult extends ResultWithType {
|
|
||||||
type: "success";
|
|
||||||
response: AxiosResponse<ClaimResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ErrorResult {
|
|
||||||
type: "error";
|
|
||||||
error: InternalError;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
* For result, see https://api.endorser.ch/api-docs/#/claims/post_api_v2_claim
|
||||||
*
|
*
|
||||||
@@ -487,57 +550,3 @@ export function isNumeric(str: string): boolean {
|
|||||||
export function numberOrZero(str: string): number {
|
export function numberOrZero(str: string): number {
|
||||||
return isNumeric(str) ? +str : 0;
|
return isNumeric(str) ? +str : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorResponse {
|
|
||||||
error?: {
|
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RateLimits {
|
|
||||||
doneClaimsThisWeek: string;
|
|
||||||
doneRegistrationsThisMonth: string;
|
|
||||||
maxClaimsPerWeek: string;
|
|
||||||
maxRegistrationsPerMonth: string;
|
|
||||||
nextMonthBeginDateTime: string;
|
|
||||||
nextWeekBeginDateTime: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents data about a project
|
|
||||||
**/
|
|
||||||
export interface ProjectData {
|
|
||||||
/**
|
|
||||||
* Name of the project
|
|
||||||
**/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* Description of the project
|
|
||||||
**/
|
|
||||||
description: string;
|
|
||||||
/**
|
|
||||||
* URL referencing information about the project
|
|
||||||
**/
|
|
||||||
handleId: string;
|
|
||||||
/**
|
|
||||||
* The DID of the issuer
|
|
||||||
*/
|
|
||||||
issuerDid: string;
|
|
||||||
/**
|
|
||||||
* The Identier of the project
|
|
||||||
**/
|
|
||||||
rowid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VerifiableCredential {
|
|
||||||
"@context": string;
|
|
||||||
"@type": string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
identifier?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorldProperties {
|
|
||||||
startTime?: string;
|
|
||||||
endTime?: string;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,6 +36,25 @@ export const UNIT_LONG: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
/* eslint-enable prettier/prettier */
|
/* eslint-enable prettier/prettier */
|
||||||
|
|
||||||
|
const UNIT_CODES: Record<string, Record<string, string>> = {
|
||||||
|
BTC: {
|
||||||
|
name: "Bitcoin",
|
||||||
|
faIcon: "bitcoin-sign",
|
||||||
|
},
|
||||||
|
HUR: {
|
||||||
|
name: "hours",
|
||||||
|
faIcon: "clock",
|
||||||
|
},
|
||||||
|
USD: {
|
||||||
|
name: "US Dollars",
|
||||||
|
faIcon: "dollar",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function iconForUnitCode(unitCode: string) {
|
||||||
|
return UNIT_CODES[unitCode]?.faIcon || "question";
|
||||||
|
}
|
||||||
|
|
||||||
export const isGlobalUri = (uri: string) => {
|
export const isGlobalUri = (uri: string) => {
|
||||||
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
|
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
<br />
|
<br />
|
||||||
(Only most recent hours included. To see more, click
|
(Only most recent hours included. To see more, click
|
||||||
<span
|
<span
|
||||||
class="text-sm uppercase bg-slate-500 text-white px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-slate-500 text-white px-1 py-1 rounded-md"
|
||||||
>
|
>
|
||||||
<fa icon="file-lines" class="fa-fw" />
|
<fa icon="file-lines" class="fa-fw" />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -51,13 +51,13 @@
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
v-bind:class="computedRemoteTabClassNames()"
|
|
||||||
@click="
|
@click="
|
||||||
projects = [];
|
projects = [];
|
||||||
isRemoteActive = true;
|
isRemoteActive = true;
|
||||||
isLocalActive = false;
|
isLocalActive = false;
|
||||||
searchAll();
|
searchAll();
|
||||||
"
|
"
|
||||||
|
v-bind:class="computedRemoteTabClassNames()"
|
||||||
>
|
>
|
||||||
Anywhere
|
Anywhere
|
||||||
<span
|
<span
|
||||||
@@ -133,7 +133,7 @@ import { accountsDB, db } from "@/db/index";
|
|||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { BoundingBox, MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
import { didInfo, ProjectData } from "@/libs/endorserServer";
|
import { didInfo, PlanData } from "@/libs/endorserServer";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import EntityIcon from "@/components/EntityIcon.vue";
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
@@ -164,7 +164,7 @@ export default class DiscoverView extends Vue {
|
|||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
searchTerms = "";
|
searchTerms = "";
|
||||||
projects: ProjectData[] = [];
|
projects: PlanData[] = [];
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
isLocalActive = true;
|
isLocalActive = true;
|
||||||
isRemoteActive = false;
|
isRemoteActive = false;
|
||||||
@@ -178,8 +178,8 @@ export default class DiscoverView extends Vue {
|
|||||||
async mounted() {
|
async mounted() {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
this.activeDid = settings?.activeDid || "";
|
this.activeDid = (settings?.activeDid as string) || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = (settings?.apiServer as string) || "";
|
||||||
this.searchBox = settings?.searchBoxes?.[0] || null;
|
this.searchBox = settings?.searchBoxes?.[0] || null;
|
||||||
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
@@ -276,7 +276,7 @@ export default class DiscoverView extends Vue {
|
|||||||
|
|
||||||
const results = await response.json();
|
const results = await response.json();
|
||||||
|
|
||||||
const plans: ProjectData[] = results.data;
|
const plans: PlanData[] = results.data;
|
||||||
if (plans) {
|
if (plans) {
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||||
@@ -360,7 +360,7 @@ export default class DiscoverView extends Vue {
|
|||||||
|
|
||||||
if (results.data) {
|
if (results.data) {
|
||||||
if (beforeId) {
|
if (beforeId) {
|
||||||
const plans: ProjectData[] = results.data;
|
const plans: PlanData[] = results.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||||
this.projects.push({
|
this.projects.push({
|
||||||
@@ -428,13 +428,15 @@ export default class DiscoverView extends Vue {
|
|||||||
"py-3": true,
|
"py-3": true,
|
||||||
"rounded-t-lg": true,
|
"rounded-t-lg": true,
|
||||||
"border-b-2": true,
|
"border-b-2": true,
|
||||||
|
|
||||||
active: this.isLocalActive,
|
active: this.isLocalActive,
|
||||||
"text-blue-600": this.isLocalActive,
|
"text-black": this.isLocalActive,
|
||||||
"border-blue-600": this.isLocalActive,
|
"border-black": this.isLocalActive,
|
||||||
"font-semibold": this.isLocalActive,
|
"font-semibold": this.isLocalActive,
|
||||||
|
|
||||||
|
"text-blue-600": !this.isLocalActive,
|
||||||
"border-transparent": !this.isLocalActive,
|
"border-transparent": !this.isLocalActive,
|
||||||
"hover:text-slate-600": !this.isLocalActive,
|
"hover:border-slate-400": !this.isLocalActive,
|
||||||
"hover:border-slate-300": !this.isLocalActive,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,13 +446,15 @@ export default class DiscoverView extends Vue {
|
|||||||
"py-3": true,
|
"py-3": true,
|
||||||
"rounded-t-lg": true,
|
"rounded-t-lg": true,
|
||||||
"border-b-2": true,
|
"border-b-2": true,
|
||||||
|
|
||||||
active: this.isRemoteActive,
|
active: this.isRemoteActive,
|
||||||
"text-blue-600": this.isRemoteActive,
|
"text-black": this.isRemoteActive,
|
||||||
"border-blue-600": this.isRemoteActive,
|
"border-black": this.isRemoteActive,
|
||||||
"font-semibold": this.isRemoteActive,
|
"font-semibold": this.isRemoteActive,
|
||||||
|
|
||||||
|
"text-blue-600": !this.isRemoteActive,
|
||||||
"border-transparent": !this.isRemoteActive,
|
"border-transparent": !this.isRemoteActive,
|
||||||
"hover:text-slate-600": !this.isRemoteActive,
|
"hover:border-slate-400": !this.isRemoteActive,
|
||||||
"hover:border-slate-300": !this.isRemoteActive,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-if="offer.amount" class="whitespace-nowrap">
|
<span v-if="offer.amount" class="whitespace-nowrap">
|
||||||
<fa
|
<fa
|
||||||
:icon="iconForUnitCode(offer.unit)"
|
:icon="libsUtil.iconForUnitCode(offer.unit)"
|
||||||
class="fa-fw text-slate-400"
|
class="fa-fw text-slate-400"
|
||||||
/>{{ offer.amount }}
|
/>{{ offer.amount }}
|
||||||
</span>
|
</span>
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-if="give.amount" class="whitespace-nowrap">
|
<span v-if="give.amount" class="whitespace-nowrap">
|
||||||
<fa
|
<fa
|
||||||
:icon="iconForUnitCode(give.unit)"
|
:icon="libsUtil.iconForUnitCode(give.unit)"
|
||||||
class="fa-fw text-slate-400"
|
class="fa-fw text-slate-400"
|
||||||
/>{{ give.amount }}
|
/>{{ give.amount }}
|
||||||
</span>
|
</span>
|
||||||
@@ -755,25 +755,6 @@ export default class ProjectViewView extends Vue {
|
|||||||
(this.$refs.customGiveDialog as GiftedDialog).open(giver, offer.handleId);
|
(this.$refs.customGiveDialog as GiftedDialog).open(giver, offer.handleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
UNIT_CODES: Record<string, Record<string, string>> = {
|
|
||||||
BTC: {
|
|
||||||
name: "Bitcoin",
|
|
||||||
faIcon: "bitcoin-sign",
|
|
||||||
},
|
|
||||||
HUR: {
|
|
||||||
name: "hours",
|
|
||||||
faIcon: "clock",
|
|
||||||
},
|
|
||||||
USD: {
|
|
||||||
name: "US Dollars",
|
|
||||||
faIcon: "dollar",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
iconForUnitCode(unitCode: string) {
|
|
||||||
return this.UNIT_CODES[unitCode]?.faIcon || "question";
|
|
||||||
}
|
|
||||||
|
|
||||||
// return an HTTPS URL if it's not a global URL
|
// return an HTTPS URL if it's not a global URL
|
||||||
addScheme(url: string) {
|
addScheme(url: string) {
|
||||||
if (!libsUtil.isGlobalUri(url)) {
|
if (!libsUtil.isGlobalUri(url)) {
|
||||||
|
|||||||
@@ -8,8 +8,44 @@
|
|||||||
Your Ideas
|
Your Ideas
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Quick Search -->
|
<!-- 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">
|
<div id="QuickSearch" class="mb-4 flex">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -22,9 +58,11 @@
|
|||||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- New Project -->
|
<!-- New Project -->
|
||||||
<button
|
<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"
|
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()"
|
@click="onClickNewProject()"
|
||||||
>
|
>
|
||||||
@@ -39,8 +77,108 @@
|
|||||||
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Offer Results List -->
|
||||||
<InfiniteScroll @reached-bottom="loadMoreData">
|
<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"
|
||||||
|
></EntityIcon>
|
||||||
|
</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="circle-info"
|
||||||
|
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">
|
<ul class="border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
class="border-b border-slate-300"
|
class="border-b border-slate-300"
|
||||||
@@ -74,15 +212,18 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { accountsDB, db } from "@/db/index";
|
import { accountsDB, db } from "@/db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||||
import { accessToken } from "@/libs/crypto";
|
import { accessToken } from "@/libs/crypto";
|
||||||
|
import * as libsUtil from "@/libs/util";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
import InfiniteScroll from "@/components/InfiniteScroll.vue";
|
||||||
import QuickNav from "@/components/QuickNav.vue";
|
import QuickNav from "@/components/QuickNav.vue";
|
||||||
import ProjectIcon from "@/components/ProjectIcon.vue";
|
import ProjectIcon from "@/components/ProjectIcon.vue";
|
||||||
import TopMessage from "@/components/TopMessage.vue";
|
import TopMessage from "@/components/TopMessage.vue";
|
||||||
import { ProjectData } from "@/libs/endorserServer";
|
import { OfferServerRecord, PlanData } from "@/libs/endorserServer";
|
||||||
|
import EntityIcon from "@/components/EntityIcon.vue";
|
||||||
|
|
||||||
interface Notification {
|
interface Notification {
|
||||||
group: string;
|
group: string;
|
||||||
@@ -92,16 +233,21 @@ interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { InfiniteScroll, QuickNav, ProjectIcon, TopMessage },
|
components: { EntityIcon, InfiniteScroll, QuickNav, ProjectIcon, TopMessage },
|
||||||
})
|
})
|
||||||
export default class ProjectsView extends Vue {
|
export default class ProjectsView extends Vue {
|
||||||
$notify!: (notification: Notification, timeout?: number) => void;
|
$notify!: (notification: Notification, timeout?: number) => void;
|
||||||
|
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
projects: ProjectData[] = [];
|
projects: PlanData[] = [];
|
||||||
current: IIdentifier;
|
currentIid: IIdentifier;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
numAccounts = 0;
|
numAccounts = 0;
|
||||||
|
offers: OfferServerRecord[] = [];
|
||||||
|
showOffers = true;
|
||||||
|
showProjects = false;
|
||||||
|
|
||||||
|
libsUtil = libsUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 'created' hook runs when the Vue instance is first created
|
* 'created' hook runs when the Vue instance is first created
|
||||||
@@ -110,8 +256,8 @@ export default class ProjectsView extends Vue {
|
|||||||
try {
|
try {
|
||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||||
const activeDid = settings?.activeDid || "";
|
const activeDid: string = (settings?.activeDid as string) || "";
|
||||||
this.apiServer = settings?.apiServer || "";
|
this.apiServer = (settings?.apiServer as string) || "";
|
||||||
|
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
this.numAccounts = await accountsDB.accounts.count();
|
this.numAccounts = await accountsDB.accounts.count();
|
||||||
@@ -127,12 +273,11 @@ export default class ProjectsView extends Vue {
|
|||||||
-1,
|
-1,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const identity = await this.getIdentity(activeDid);
|
this.currentIid = await this.getIdentity(activeDid);
|
||||||
this.current = identity;
|
await this.loadOffers();
|
||||||
this.loadProjects(identity);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Error initializing:", err);
|
console.error("Error initializing:", err);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -150,7 +295,7 @@ export default class ProjectsView extends Vue {
|
|||||||
* @param url the url used to fetch the data
|
* @param url the url used to fetch the data
|
||||||
* @param token Authorization token
|
* @param token Authorization token
|
||||||
**/
|
**/
|
||||||
async dataLoader(url: string, token: string) {
|
async projectDataLoader(url: string, token: string) {
|
||||||
const headers: { [key: string]: string } = {
|
const headers: { [key: string]: string } = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
@@ -160,13 +305,17 @@ export default class ProjectsView extends Vue {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
const resp = await this.axios.get(url, { headers });
|
const resp = await this.axios.get(url, { headers });
|
||||||
if (resp.status === 200 || !resp.data.data) {
|
if (resp.status === 200 || !resp.data.data) {
|
||||||
const plans: ProjectData[] = resp.data.data;
|
const plans: PlanData[] = resp.data.data;
|
||||||
for (const plan of plans) {
|
for (const plan of plans) {
|
||||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||||
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("Bad server response & data:", resp.status, resp.data);
|
console.error(
|
||||||
|
"Bad server response & data for plans:",
|
||||||
|
resp.status,
|
||||||
|
resp.data,
|
||||||
|
);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -179,7 +328,7 @@ export default class ProjectsView extends Vue {
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Got error loading projects:", error.message || error);
|
console.error("Got error loading plans:", error.message || error);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -198,15 +347,44 @@ export default class ProjectsView extends Vue {
|
|||||||
* Data loader used by infinite scroller
|
* Data loader used by infinite scroller
|
||||||
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||||
**/
|
**/
|
||||||
async loadMoreData(payload: boolean) {
|
async loadMoreProjectData(payload: boolean) {
|
||||||
if (this.projects.length > 0 && payload) {
|
if (this.projects.length > 0 && payload) {
|
||||||
const latestProject = this.projects[this.projects.length - 1];
|
const latestProject = this.projects[this.projects.length - 1];
|
||||||
const url = `${this.apiServer}/api/v2/report/plansByIssuer?beforeId=${latestProject.rowid}`;
|
await this.loadProjects(
|
||||||
const token = await accessToken(this.current);
|
this.currentIid,
|
||||||
await this.dataLoader(url, token);
|
`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
|
* Handle clicking on a project entry found in the list
|
||||||
* @param id of the project
|
* @param id of the project
|
||||||
@@ -219,32 +397,6 @@ export default class ProjectsView extends Vue {
|
|||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load projects initially
|
|
||||||
* @param identity of the user
|
|
||||||
**/
|
|
||||||
async loadProjects(identity: IIdentifier) {
|
|
||||||
const url = `${this.apiServer}/api/v2/report/plansByIssuer`;
|
|
||||||
const token: string = await accessToken(identity);
|
|
||||||
await this.dataLoader(url, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getIdentity(activeDid: string) {
|
|
||||||
await accountsDB.open();
|
|
||||||
const account = await accountsDB.accounts
|
|
||||||
.where("did")
|
|
||||||
.equals(activeDid)
|
|
||||||
.first();
|
|
||||||
const identity = JSON.parse(account?.identity || "null");
|
|
||||||
|
|
||||||
if (!identity) {
|
|
||||||
throw new Error(
|
|
||||||
"Attempted to load project records with no identifier available.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handling clicking on the new project button
|
* Handling clicking on the new project button
|
||||||
**/
|
**/
|
||||||
@@ -255,5 +407,120 @@ export default class ProjectsView extends Vue {
|
|||||||
};
|
};
|
||||||
this.$router.push(route);
|
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>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user