Browse Source

add an image to projects (which shows on all ProjectIcons except for offers)

kb/add-usage-guide
Trent Larson 4 months ago
parent
commit
64f5656f41
  1. 7
      src/components/ProjectIcon.vue
  2. 4
      src/libs/endorserServer.ts
  3. 18
      src/views/DiscoverView.vue
  4. 132
      src/views/NewEditProjectView.vue
  5. 13
      src/views/ProjectViewView.vue
  6. 75
      src/views/ProjectsView.vue

7
src/components/ProjectIcon.vue

@ -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,12 +21,17 @@ const BLANK_CONFIG = {
export default class ProjectIcon extends Vue {
@Prop entityId = "";
@Prop iconSize = 0;
@Prop imageUrl = "";
generateIdenticon() {
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>
<style scoped></style>

4
src/libs/endorserServer.ts

@ -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
*/

18
src/views/DiscoverView.vue

@ -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 {

132
src/views/NewEditProjectView.vue

@ -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: {

13
src/views/ProjectViewView.vue

@ -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)";

75
src/views/ProjectsView.vue

@ -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 {

Loading…
Cancel
Save