|
|
|
<template>
|
|
|
|
<QuickNav selected="Projects"></QuickNav>
|
|
|
|
<!-- 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">
|
|
|
|
<!-- Cancel -->
|
|
|
|
<router-link
|
|
|
|
:to="{ name: 'project' }"
|
|
|
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
|
|
|
><fa icon="chevron-left" class="fa-fw"></fa
|
|
|
|
></router-link>
|
|
|
|
Edit Idea
|
|
|
|
</h1>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Project Details -->
|
|
|
|
<!-- Image - (see design model) Empty -->
|
|
|
|
|
|
|
|
<div>
|
|
|
|
{{ errorMessage }}
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
placeholder="Idea Name"
|
|
|
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
|
|
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="mt-4 block w-full rounded border border-slate-400 px-3 py-2"
|
|
|
|
v-model="agentDid"
|
|
|
|
/>
|
|
|
|
<div class="mb-4">
|
|
|
|
<p v-if="activeDid != projectIssuerDid && agentDid != projectIssuerDid">
|
|
|
|
<span class="text-red-500">Beware!</span>
|
|
|
|
If you save this, the original project owner will no longer be able to
|
|
|
|
edit it.
|
|
|
|
<button @click="agentDid = projectIssuerDid" class="text-blue-500">
|
|
|
|
Click here to make the original owner an authorized representative.
|
|
|
|
</button>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<textarea
|
|
|
|
placeholder="Description"
|
|
|
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
|
|
rows="5"
|
|
|
|
v-model="fullClaim.description"
|
|
|
|
maxlength="5000"
|
|
|
|
></textarea>
|
|
|
|
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
|
|
|
If you want to be contacted, be sure to include your contact information.
|
|
|
|
</div>
|
|
|
|
<div class="text-xs text-slate-500 italic -mt-3 mb-4">
|
|
|
|
{{ fullClaim.description?.length }}/5000 max. characters
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<input
|
|
|
|
v-model="fullClaim.url"
|
|
|
|
placeholder="Website"
|
|
|
|
autocapitalize="none"
|
|
|
|
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
|
|
|
/>
|
|
|
|
|
|
|
|
<div class="flex mb-4 columns-3 w-full">
|
|
|
|
<input
|
|
|
|
v-model="startDateInput"
|
|
|
|
placeholder="Start Date"
|
|
|
|
type="date"
|
|
|
|
class="col-span-1 w-full rounded border border-slate-400 px-3 py-2"
|
|
|
|
/>
|
|
|
|
<input
|
|
|
|
:disabled="!startDateInput"
|
|
|
|
v-model="startTimeInput"
|
|
|
|
placeholder="Start Time"
|
|
|
|
type="time"
|
|
|
|
class="col-span-1 w-full rounded border border-slate-400 ml-2 px-3 py-2"
|
|
|
|
/>
|
|
|
|
<span class="col-span-1 w-full flex justify-center">{{ zoneName }}</span>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="flex items-center mb-4">
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
class="mr-2"
|
|
|
|
v-model="includeLocation"
|
|
|
|
@click="includeLocation = !includeLocation"
|
|
|
|
/>
|
|
|
|
<label for="includeLocation">Include Location</label>
|
|
|
|
</div>
|
|
|
|
<div v-if="includeLocation" class="mb-4 aspect-video">
|
|
|
|
<p class="text-sm mb-2 text-slate-500">
|
|
|
|
For your security, choose a location nearby but not exactly at the
|
|
|
|
place.
|
|
|
|
</p>
|
|
|
|
|
|
|
|
<l-map
|
|
|
|
ref="map"
|
|
|
|
v-model:zoom="zoom"
|
|
|
|
:center="[0, 0]"
|
|
|
|
class="!z-40 rounded-md"
|
|
|
|
@click="
|
|
|
|
(event) => {
|
|
|
|
latitude = event.latlng.lat;
|
|
|
|
longitude = event.latlng.lng;
|
|
|
|
}
|
|
|
|
"
|
|
|
|
>
|
|
|
|
<l-tile-layer
|
|
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
|
|
layer-type="base"
|
|
|
|
name="OpenStreetMap"
|
|
|
|
/>
|
|
|
|
<l-marker
|
|
|
|
v-if="latitude && longitude"
|
|
|
|
:lat-lng="[latitude, longitude]"
|
|
|
|
@click="confirmEraseLatLong()"
|
|
|
|
/>
|
|
|
|
</l-map>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="mt-8">
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
|
|
<button
|
|
|
|
:disabled="isHiddenSave"
|
|
|
|
class="block w-full text-center 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 mb-2"
|
|
|
|
@click="onSaveProjectClick()"
|
|
|
|
>
|
|
|
|
<!-- SHOW if in idle state -->
|
|
|
|
<span :class="{ hidden: isHiddenSave }">Save Project</span>
|
|
|
|
|
|
|
|
<!-- SHOW if in saving state; DISABLE button while in saving state -->
|
|
|
|
<span :class="{ hidden: isHiddenSpinner }">
|
|
|
|
<!-- icon no worky? -->
|
|
|
|
<i class="fa-solid fa-spinner fa-spin-pulse"></i>
|
|
|
|
Saving...</span
|
|
|
|
>
|
|
|
|
</button>
|
|
|
|
<button
|
|
|
|
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="onCancelClick()"
|
|
|
|
>
|
|
|
|
Cancel
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
import "leaflet/dist/leaflet.css";
|
|
|
|
import { AxiosError, AxiosRequestHeaders } from "axios";
|
|
|
|
import { DateTime } from "luxon";
|
|
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
|
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
|
|
|
|
|
|
|
import ImageMethodDialog from "@/components/ImageMethodDialog.vue";
|
|
|
|
import QuickNav from "@/components/QuickNav.vue";
|
|
|
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app";
|
|
|
|
import { accountsDB, db } from "@/db/index";
|
|
|
|
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|
|
|
import {
|
|
|
|
createEndorserJwtVcFromClaim,
|
|
|
|
getHeaders,
|
|
|
|
PlanVerifiableCredential,
|
|
|
|
} from "@/libs/endorserServer";
|
|
|
|
import { useAppStore } from "@/store/app";
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
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 = "";
|
|
|
|
apiServer = "";
|
|
|
|
errorMessage = "";
|
|
|
|
fullClaim: PlanVerifiableCredential = {
|
|
|
|
"@context": "https://schema.org",
|
|
|
|
"@type": "PlanAction",
|
|
|
|
name: "",
|
|
|
|
description: "",
|
|
|
|
}; // this default is only to avoid errors before plan is loaded
|
|
|
|
imageUrl = "";
|
|
|
|
includeLocation = false;
|
|
|
|
isHiddenSave = false;
|
|
|
|
isHiddenSpinner = true;
|
|
|
|
lastClaimJwtId = "";
|
|
|
|
latitude = 0;
|
|
|
|
longitude = 0;
|
|
|
|
numAccounts = 0;
|
|
|
|
projectId = localStorage.getItem("projectId") || "";
|
|
|
|
projectIssuerDid = "";
|
|
|
|
startDateInput?: string;
|
|
|
|
startTimeInput?: string;
|
|
|
|
zoneName = DateTime.local().zoneName;
|
|
|
|
zoom = 2;
|
|
|
|
|
|
|
|
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) || "";
|
|
|
|
this.apiServer = (settings?.apiServer as string) || "";
|
|
|
|
|
|
|
|
if (this.projectId) {
|
|
|
|
if (this.numAccounts === 0) {
|
|
|
|
this.errNote("There was a problem loading your account info.");
|
|
|
|
} else {
|
|
|
|
this.loadProject(this.activeDid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async loadProject(userDid: string) {
|
|
|
|
const url =
|
|
|
|
this.apiServer +
|
|
|
|
"/api/claim/byHandle/" +
|
|
|
|
encodeURIComponent(this.projectId);
|
|
|
|
const headers = await getHeaders(userDid);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const resp = await this.axios.get(url, { headers });
|
|
|
|
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;
|
|
|
|
this.latitude = this.fullClaim.location.geo.latitude;
|
|
|
|
this.longitude = this.fullClaim.location.geo.longitude;
|
|
|
|
}
|
|
|
|
if (this.fullClaim?.agent?.identifier) {
|
|
|
|
this.agentDid = this.fullClaim.agent.identifier;
|
|
|
|
}
|
|
|
|
if (this.fullClaim.startTime) {
|
|
|
|
const localDateTime = DateTime.fromISO(
|
|
|
|
this.fullClaim.startTime as string,
|
|
|
|
).toLocal();
|
|
|
|
this.startDateInput = localDateTime.toFormat("yyyy-MM-dd");
|
|
|
|
this.startTimeInput = localDateTime.toFormat("HH:mm");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} 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 headers = getHeaders(this.activeDid) as AxiosRequestHeaders;
|
|
|
|
const response = await this.axios.delete(
|
|
|
|
DEFAULT_IMAGE_API_SERVER +
|
|
|
|
"/image/" +
|
|
|
|
encodeURIComponent(this.imageUrl),
|
|
|
|
{ headers },
|
|
|
|
);
|
|
|
|
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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async saveProject(issuerDid: string) {
|
|
|
|
// Make a claim
|
|
|
|
const vcClaim: PlanVerifiableCredential = this.fullClaim;
|
|
|
|
if (this.projectId) {
|
|
|
|
vcClaim.lastClaimId = this.lastClaimJwtId;
|
|
|
|
}
|
|
|
|
if (this.agentDid) {
|
|
|
|
vcClaim.agent = {
|
|
|
|
identifier: this.agentDid,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
delete vcClaim.agent;
|
|
|
|
}
|
|
|
|
if (this.imageUrl) {
|
|
|
|
vcClaim.image = this.imageUrl;
|
|
|
|
} else {
|
|
|
|
delete vcClaim.image;
|
|
|
|
}
|
|
|
|
if (this.includeLocation) {
|
|
|
|
vcClaim.location = {
|
|
|
|
geo: {
|
|
|
|
"@type": "GeoCoordinates",
|
|
|
|
latitude: this.latitude,
|
|
|
|
longitude: this.longitude,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
delete vcClaim.location;
|
|
|
|
}
|
|
|
|
if (this.startDateInput) {
|
|
|
|
try {
|
|
|
|
const startTimeFull = this.startTimeInput || "00:00:00";
|
|
|
|
const fullTimeString = this.startDateInput + " " + startTimeFull;
|
|
|
|
// throw an error on an invalid date or time string
|
|
|
|
vcClaim.startTime = new Date(fullTimeString).toISOString(); // ensure timezone is part of it
|
|
|
|
} catch {
|
|
|
|
// it's not a valid date so erase it and tell the user
|
|
|
|
delete vcClaim.startTime;
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "The date was invalid so it was not set.",
|
|
|
|
},
|
|
|
|
5000,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
delete vcClaim.startTime;
|
|
|
|
}
|
|
|
|
const vcJwt = await createEndorserJwtVcFromClaim(issuerDid, vcClaim);
|
|
|
|
|
|
|
|
// Make the xhr request payload
|
|
|
|
|
|
|
|
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
|
|
|
const url = this.apiServer + "/api/v2/claim";
|
|
|
|
const headers = await getHeaders(issuerDid);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const resp = await this.axios.post(url, payload, { headers });
|
|
|
|
if (resp.data?.success?.handleId) {
|
|
|
|
this.errorMessage = "";
|
|
|
|
|
|
|
|
useAppStore()
|
|
|
|
.setProjectId(resp.data.success.handleId)
|
|
|
|
.then(() => {
|
|
|
|
this.$router.push({ name: "project" });
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
console.error(
|
|
|
|
"Got unexpected 'data' inside response from server",
|
|
|
|
resp,
|
|
|
|
);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error Saving Idea",
|
|
|
|
text: "Server did not save the idea. Try again.",
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
let userMessage = "There was an error saving the project.";
|
|
|
|
const serverError = error as AxiosError<{
|
|
|
|
error?: { message?: string };
|
|
|
|
}>;
|
|
|
|
if (serverError) {
|
|
|
|
console.error("Got error from server", serverError);
|
|
|
|
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
|
|
|
userMessage =
|
|
|
|
(serverError.response?.data?.error?.message as string) ||
|
|
|
|
userMessage;
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "User Message",
|
|
|
|
text: userMessage,
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Server Message",
|
|
|
|
text: JSON.stringify(serverError.toJSON()),
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
console.error("Here's the full error trying to save the claim:", error);
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Claim Error",
|
|
|
|
text: error as string,
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// Now set that error for the user to see.
|
|
|
|
this.errorMessage = userMessage;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public async onSaveProjectClick() {
|
|
|
|
this.isHiddenSave = true;
|
|
|
|
this.isHiddenSpinner = false;
|
|
|
|
|
|
|
|
if (this.numAccounts === 0) {
|
|
|
|
console.error("Error: there is no account.");
|
|
|
|
} else {
|
|
|
|
this.saveProject(this.activeDid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
confirmEraseLatLong() {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "modal",
|
|
|
|
type: "confirm",
|
|
|
|
title: "Erase Marker",
|
|
|
|
text: "Are you sure you don't want to mark a location? This will erase the current location.",
|
|
|
|
onYes: async () => {
|
|
|
|
this.eraseLatLong();
|
|
|
|
},
|
|
|
|
},
|
|
|
|
-1,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public eraseLatLong() {
|
|
|
|
this.latitude = 0;
|
|
|
|
this.longitude = 0;
|
|
|
|
this.includeLocation = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public onCancelClick() {
|
|
|
|
this.$router.back();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|