forked from trent_larson/crowd-funder-for-time-pwa
add an image to projects (which shows on all ProjectIcons except for offers)
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-html="generateIdenticon()" class="w-fit"></div>
|
||||
<div v-html="generateIdenticon()" class="h-full w-full object-contain"></div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { toSvg } from "jdenticon";
|
||||
@@ -21,11 +21,16 @@ const BLANK_CONFIG = {
|
||||
export default class ProjectIcon extends Vue {
|
||||
@Prop entityId = "";
|
||||
@Prop iconSize = 0;
|
||||
@Prop imageUrl = "";
|
||||
|
||||
generateIdenticon() {
|
||||
const config = this.entityId ? undefined : BLANK_CONFIG;
|
||||
const svgString = toSvg(this.entityId, this.iconSize, config);
|
||||
return svgString;
|
||||
if (this.imageUrl) {
|
||||
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
|
||||
} else {
|
||||
const config = this.entityId ? undefined : BLANK_CONFIG;
|
||||
const svgString = toSvg(this.entityId, this.iconSize, config);
|
||||
return svgString;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -118,6 +118,7 @@ export interface PlanSummaryRecord {
|
||||
endTime?: string;
|
||||
fulfillsPlanHandleId: string;
|
||||
handleId: string;
|
||||
image?: string;
|
||||
issuerDid: string;
|
||||
locLat?: number;
|
||||
locLon?: number;
|
||||
@@ -175,7 +176,7 @@ export interface PlanVerifiableCredential {
|
||||
* Represents data about a project
|
||||
*
|
||||
* @deprecated
|
||||
* We should use PlanServerRecord instead.
|
||||
* We should use PlanSummaryRecord instead.
|
||||
**/
|
||||
export interface PlanData {
|
||||
/**
|
||||
@@ -190,6 +191,7 @@ export interface PlanData {
|
||||
* URL referencing information about the project
|
||||
**/
|
||||
handleId: string;
|
||||
image?: string;
|
||||
/**
|
||||
* The DID of the issuer
|
||||
*/
|
||||
|
||||
@@ -102,12 +102,13 @@
|
||||
@click="onClickLoadProject(project.handleId)"
|
||||
class="block py-4 flex gap-4"
|
||||
>
|
||||
<div class="w-12">
|
||||
<div>
|
||||
<ProjectIcon
|
||||
:entityId="project.handleId"
|
||||
:iconSize="48"
|
||||
class="block border border-slate-300 rounded-md"
|
||||
></ProjectIcon>
|
||||
:imageUrl="project.image"
|
||||
class="block border border-slate-300 rounded-md max-h-12 max-w-12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grow">
|
||||
@@ -271,8 +272,15 @@ export default class DiscoverView extends Vue {
|
||||
const plans: PlanData[] = results.data;
|
||||
if (plans) {
|
||||
for (const plan of plans) {
|
||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
||||
const { name, description, handleId, image, issuerDid, rowid } = plan;
|
||||
this.projects.push({
|
||||
name,
|
||||
description,
|
||||
handleId,
|
||||
image,
|
||||
issuerDid,
|
||||
rowid,
|
||||
});
|
||||
}
|
||||
this.remoteCount = this.projects.length;
|
||||
} else {
|
||||
|
||||
@@ -29,10 +29,31 @@
|
||||
v-model="fullClaim.name"
|
||||
/>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
<span v-if="imageUrl" class="flex justify-between">
|
||||
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
|
||||
<img :src="imageUrl" class="h-24 rounded-xl" />
|
||||
</a>
|
||||
<fa
|
||||
icon="trash-can"
|
||||
@click="confirmDeleteImage"
|
||||
class="text-red-500 fa-fw ml-8 mt-10"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
<fa
|
||||
icon="camera"
|
||||
class="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-2 rounded-md"
|
||||
@click="openImageDialog"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<ImageMethodDialog ref="imageDialog" />
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Other Authorized Representative"
|
||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||
class="mt-4 block w-full rounded border border-slate-400 px-3 py-2"
|
||||
v-model="agentDid"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
@@ -155,23 +176,31 @@ import "leaflet/dist/leaflet.css";
|
||||
import { AxiosError } from "axios";
|
||||
import * as didJwt from "did-jwt";
|
||||
import { DateTime } from "luxon";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { accessToken, SimpleSigner } from "@/libs/crypto";
|
||||
import * as libsUtil from "@/libs/util";
|
||||
import { useAppStore } from "@/store/app";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { PlanVerifiableCredential } from "@/libs/endorserServer";
|
||||
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
|
||||
|
||||
@Component({
|
||||
components: { LMap, LMarker, LTileLayer, QuickNav },
|
||||
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
|
||||
})
|
||||
export default class NewEditProjectView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
errNote(message) {
|
||||
this.$notify(
|
||||
{ group: "alert", type: "danger", title: "Error", text: message },
|
||||
5000,
|
||||
);
|
||||
}
|
||||
|
||||
activeDid = "";
|
||||
agentDid = "";
|
||||
@@ -183,6 +212,7 @@ export default class NewEditProjectView extends Vue {
|
||||
name: "",
|
||||
description: "",
|
||||
}; // this default is only to avoid errors before plan is loaded
|
||||
imageUrl = "";
|
||||
includeLocation = false;
|
||||
isHiddenSave = false;
|
||||
isHiddenSpinner = true;
|
||||
@@ -197,10 +227,7 @@ export default class NewEditProjectView extends Vue {
|
||||
zoneName = DateTime.local().zoneName;
|
||||
zoom = 2;
|
||||
|
||||
async beforeCreate() {
|
||||
await accountsDB.open();
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
}
|
||||
libsUtil = libsUtil;
|
||||
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
@@ -228,6 +255,9 @@ export default class NewEditProjectView extends Vue {
|
||||
}
|
||||
|
||||
async mounted() {
|
||||
await accountsDB.open();
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
this.activeDid = (settings?.activeDid as string) || "";
|
||||
@@ -235,7 +265,7 @@ export default class NewEditProjectView extends Vue {
|
||||
|
||||
if (this.projectId) {
|
||||
if (this.numAccounts === 0) {
|
||||
console.error("Error: no account was found.");
|
||||
this.errNote("There was a problem loading your account info.");
|
||||
} else {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
if (!identity) {
|
||||
@@ -264,6 +294,7 @@ export default class NewEditProjectView extends Vue {
|
||||
if (resp.status === 200) {
|
||||
this.projectIssuerDid = resp.data.issuer;
|
||||
this.fullClaim = resp.data.claim;
|
||||
this.imageUrl = resp.data.claim.image || "";
|
||||
this.lastClaimJwtId = resp.data.id;
|
||||
if (this.fullClaim?.location) {
|
||||
this.includeLocation = true;
|
||||
@@ -283,6 +314,84 @@ export default class NewEditProjectView extends Vue {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error retrieving that project", error);
|
||||
this.errNote("There was an error retrieving that project.");
|
||||
}
|
||||
}
|
||||
|
||||
openImageDialog() {
|
||||
(this.$refs.imageDialog as ImageMethodDialog).open((imgUrl) => {
|
||||
this.imageUrl = imgUrl;
|
||||
}, "PlanAction");
|
||||
}
|
||||
|
||||
confirmDeleteImage() {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Are you sure you want to delete the image?",
|
||||
text: "",
|
||||
onYes: this.deleteImage,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteImage() {
|
||||
if (!this.imageUrl) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const identity = await libsUtil.getIdentity(this.activeDid);
|
||||
const token = await accessToken(identity);
|
||||
const response = await this.axios.delete(
|
||||
DEFAULT_IMAGE_API_SERVER +
|
||||
"/image/" +
|
||||
encodeURIComponent(this.imageUrl),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (response.status === 204) {
|
||||
// don't bother with a notification
|
||||
// (either they'll simply continue or they're canceling and going back)
|
||||
} else {
|
||||
console.error("Problem deleting image:", response);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was a problem deleting the image.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.imageUrl = "";
|
||||
} catch (error) {
|
||||
console.error("Error deleting image:", error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any).response.status === 404) {
|
||||
console.log("The image was already deleted:", error);
|
||||
|
||||
this.imageUrl = "";
|
||||
|
||||
// it already doesn't exist so we won't say anything to the user
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error deleting the image.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +408,11 @@ export default class NewEditProjectView extends Vue {
|
||||
} else {
|
||||
delete vcClaim.agent;
|
||||
}
|
||||
if (this.imageUrl) {
|
||||
vcClaim.image = this.imageUrl;
|
||||
} else {
|
||||
delete vcClaim.image;
|
||||
}
|
||||
if (this.includeLocation) {
|
||||
vcClaim.location = {
|
||||
geo: {
|
||||
|
||||
@@ -22,13 +22,14 @@
|
||||
<!-- Project Details -->
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mt-4">
|
||||
<div>
|
||||
<div class="block pb-4 flex gap-4">
|
||||
<div class="flex-none w-16 pt-1">
|
||||
<div class="pb-4 flex gap-4">
|
||||
<div class="pt-1">
|
||||
<ProjectIcon
|
||||
:entityId="projectId"
|
||||
:iconSize="64"
|
||||
class="block border border-slate-300 rounded-md"
|
||||
></ProjectIcon>
|
||||
:imageUrl="imageUrl"
|
||||
class="block border border-slate-300 rounded-md max-h-16 max-w-16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden">
|
||||
@@ -399,6 +400,7 @@ export default class ProjectViewView extends Vue {
|
||||
fulfillersToHitLimit = false;
|
||||
givesToThis: Array<GiveSummaryRecord> = [];
|
||||
givesHitLimit = false;
|
||||
imageUrl = "";
|
||||
issuer = "";
|
||||
latitude = 0;
|
||||
longitude = 0;
|
||||
@@ -427,7 +429,7 @@ export default class ProjectViewView extends Vue {
|
||||
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 identity = JSON.parse((account?.identity as string) || "null");
|
||||
|
||||
const pathParam = window.location.pathname.substring("/project/".length);
|
||||
if (pathParam) {
|
||||
@@ -488,6 +490,7 @@ export default class ProjectViewView extends Vue {
|
||||
startDateTime.toLocaleTimeString();
|
||||
}
|
||||
this.agentDid = resp.data.claim?.agent?.identifier;
|
||||
this.imageUrl = resp.data.claim?.image;
|
||||
this.issuer = resp.data.issuer;
|
||||
this.name = resp.data.claim?.name || "(no name)";
|
||||
this.description = resp.data.claim?.description || "(no description)";
|
||||
|
||||
@@ -86,12 +86,12 @@
|
||||
:key="offer.handleId"
|
||||
>
|
||||
<div class="block py-4 flex gap-4">
|
||||
<div v-if="offer.fulfillsPlanHandleId" class="flex-none w-12">
|
||||
<div v-if="offer.fulfillsPlanHandleId" class="flex-none">
|
||||
<ProjectIcon
|
||||
:entityId="offer.fulfillsPlanHandleId"
|
||||
:iconSize="48"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md"
|
||||
></ProjectIcon>
|
||||
class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="offer.recipientDid" class="flex-none w-12">
|
||||
<EntityIcon
|
||||
@@ -189,12 +189,13 @@
|
||||
@click="onClickLoadProject(project.handleId)"
|
||||
class="block py-4 flex gap-4"
|
||||
>
|
||||
<div class="flex-none w-12">
|
||||
<div class="flex-none">
|
||||
<ProjectIcon
|
||||
:entityId="project.handleId"
|
||||
:iconSize="48"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md"
|
||||
></ProjectIcon>
|
||||
:imageUrl="project.image"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grow overflow-hidden">
|
||||
@@ -211,6 +212,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AxiosRequestConfig } from "axios";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface } from "@/constants/app";
|
||||
@@ -231,6 +233,12 @@ import EntityIcon from "@/components/EntityIcon.vue";
|
||||
})
|
||||
export default class ProjectsView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
errNote(message) {
|
||||
this.$notify(
|
||||
{ group: "alert", type: "danger", title: "Error", text: message },
|
||||
5000,
|
||||
);
|
||||
}
|
||||
|
||||
apiServer = "";
|
||||
projects: PlanData[] = [];
|
||||
@@ -256,30 +264,14 @@ export default class ProjectsView extends Vue {
|
||||
this.numAccounts = await accountsDB.accounts.count();
|
||||
if (this.numAccounts === 0) {
|
||||
console.error("No accounts found.");
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You need an identifier to load your projects.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
this.errNote("You need an identifier to load your projects.");
|
||||
} else {
|
||||
this.currentIid = await this.getIdentity(activeDid);
|
||||
await this.loadOffers();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error initializing:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Something went wrong loading your projects.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
this.errNote("Something went wrong loading your projects.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,12 +288,19 @@ export default class ProjectsView extends Vue {
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
|
||||
if (resp.status === 200 && resp.data.data) {
|
||||
const plans: PlanData[] = resp.data.data;
|
||||
for (const plan of plans) {
|
||||
const { name, description, handleId, issuerDid, rowid } = plan;
|
||||
this.projects.push({ name, description, handleId, issuerDid, rowid });
|
||||
const { name, description, handleId, image, issuerDid, rowid } = plan;
|
||||
this.projects.push({
|
||||
name,
|
||||
description,
|
||||
image,
|
||||
handleId,
|
||||
issuerDid,
|
||||
rowid,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
@@ -309,28 +308,12 @@ export default class ProjectsView extends Vue {
|
||||
resp.status,
|
||||
resp.data,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to get projects from the server. Try again later.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
this.errNote("Failed to get projects from the server.");
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Got error loading plans:", error.message || error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Got an error loading projects.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
this.errNote("Got an error loading projects.");
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
@@ -421,7 +404,7 @@ export default class ProjectsView extends Vue {
|
||||
|
||||
try {
|
||||
this.isLoading = true;
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
const resp = await this.axios.get(url, { headers } as AxiosRequestConfig);
|
||||
if (resp.status === 200 && resp.data.data) {
|
||||
this.offers = this.offers.concat(resp.data.data);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user