<template> <QuickNav /> <TopMessage /> <!-- CONTENT --> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <!-- Back --> <div class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" @click="cancel()" > <fa icon="chevron-left" class="fa-fw"></fa> </h1> </div> <!-- Heading --> <h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1> <h1 class="text-xl font-bold text-center mb-4"> {{ message }} {{ giverName || "somebody not named" }} </h1> <textarea class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" placeholder="What was received" v-model="description" /> <div class="flex flex-row justify-center"> <span class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20" @click="changeUnitCode()" > {{ libsUtil.UNIT_SHORT[unitCode] }} </span> <div class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" @click="amountInput === '0' ? null : decrement()" > <fa icon="chevron-left" /> </div> <input type="number" class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20" v-model="amountInput" /> <div class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2" @click="increment()" > <fa icon="chevron-right" /> </div> </div> <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="openPhotoDialog" /> </span> </div> <GiftedPhotoDialog ref="photoDialog" /> <div v-if="projectId" class="mt-4"> <fa icon="check" class="bg-slate-500 text-white h-5 w-5 px-0.5 py-0.5 mr-2 rounded" /> <label class="text-sm">This is given to a project</label> </div> <div v-if="!projectId" class="mt-4"> <input type="checkbox" class="h-6 w-6 mr-2" v-model="givenToUser" /> <label class="text-sm">Given to you</label> </div> <div class="mt-4"> <input type="checkbox" class="h-6 w-6 mr-2" v-model="isTrade" /> <label class="text-sm">Trade (not a gift)</label> </div> <p class="text-center mb-2 mt-6 italic"> Sign & Send to publish to the world <fa icon="circle-info" class="pl-2 text-blue-500 cursor-pointer" @click="explainData()" /> </p> <div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <button 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" @click="confirm" > Sign & Send </button> <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="cancel" > Cancel </button> </div> </section> </template> <script lang="ts"> import { Component, Vue } from "vue-facing-decorator"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; import QuickNav from "@/components/QuickNav.vue"; import TopMessage from "@/components/TopMessage.vue"; import { db } from "@/db/index"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; import { createAndSubmitGive } from "@/libs/endorserServer"; import * as libsUtil from "@/libs/util"; import { accessToken } from "@/libs/crypto"; import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue"; @Component({ components: { GiftedDialog, GiftedPhotoDialog, QuickNav, TopMessage, }, }) export default class GiftedDetails extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; activeDid = ""; apiServer = ""; amountInput = "0"; description = ""; givenToUser = false; giverDid: string | undefined; giverName = ""; imageUrl = ""; isTrade = false; message = ""; offerId = ""; projectId = ""; unitCode = "HUR"; libsUtil = libsUtil; async mounted() { this.amountInput = this.$route.query.amountInput as string; this.description = this.$route.query.description as string; this.giverDid = this.$route.query.giverDid as string; this.giverName = this.$route.query.giverName as string; this.message = this.$route.query.message as string; this.offerId = this.$route.query.offerId as string; this.projectId = this.$route.query.projectId as string; this.unitCode = this.$route.query.unitCode as string; this.imageUrl = localStorage.getItem("imageUrl") || ""; this.givenToUser = !this.projectId; try { await db.open(); const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; this.apiServer = settings?.apiServer || ""; this.activeDid = settings?.activeDid || ""; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { console.error("Error retrieving settings from database:", err); this.$notify( { group: "alert", type: "danger", title: "Error", text: err.message || "There was an error retrieving your settings.", }, -1, ); } } changeUnitCode() { const units = Object.keys(this.libsUtil.UNIT_SHORT); const index = units.indexOf(this.unitCode); this.unitCode = units[(index + 1) % units.length]; } increment() { this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`; } decrement() { this.amountInput = `${Math.max( 0, (parseFloat(this.amountInput) || 1) - 1, )}`; } cancel() { this.deleteImage(); // not awaiting, so they'll go back immediately this.$router.back(); } openPhotoDialog() { (this.$refs.photoDialog as GiftedPhotoDialog).open((imgUrl) => { this.imageUrl = imgUrl; }); } 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("Non-success deleting image:", response); this.$notify( { group: "alert", type: "danger", title: "Error", text: "There was a problem deleting the image.", }, 5000, ); // keep the imageUrl in localStorage so the user can try again if they want return; } localStorage.removeItem("imageUrl"); 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); localStorage.removeItem("imageUrl"); 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, ); } } } async confirm() { if (!this.activeDid) { this.$notify( { group: "alert", type: "danger", title: "Error", text: "You must select an identifier before you can record a give.", }, 2000, ); return; } if (parseFloat(this.amountInput) < 0) { this.$notify( { group: "alert", type: "danger", text: "You may not send a negative number.", title: "", }, 2000, ); return; } if (!this.description && !parseFloat(this.amountInput)) { this.$notify( { group: "alert", type: "danger", title: "Error", text: `You must enter a description or some number of ${ this.libsUtil.UNIT_LONG[this.unitCode] }.`, }, 2000, ); return; } this.$notify( { group: "alert", type: "toast", text: "Recording the give...", title: "", }, 1000, ); // this is asynchronous, but we don't need to wait for it to complete await this.recordGive(); } /** * * @param giverDid may be null * @param description may be an empty string * @param amountInput may be 0 * @param unitCode may be omitted, defaults to "HUR" */ public async recordGive() { try { const identity = await libsUtil.getIdentity(this.activeDid); const result = await createAndSubmitGive( this.axios, this.apiServer, identity, this.giverDid, this.givenToUser ? this.activeDid : undefined, this.description, parseFloat(this.amountInput), this.unitCode, this.projectId, this.offerId, this.isTrade, this.imageUrl, ); if ( result.type === "error" || this.isGiveCreationError(result.response) ) { const errorMessage = this.getGiveCreationErrorMessage(result); console.error("Error with give creation result:", result); this.$notify( { group: "alert", type: "danger", title: "Error", text: errorMessage || "There was an error creating the give.", }, -1, ); } else { this.$notify( { group: "alert", type: "success", title: "Success", text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`, }, 5000, ); localStorage.removeItem("imageUrl"); this.$router.back(); } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { console.error("Error with give recordation caught:", error); const message = error.userMessage || error.response?.data?.error?.message || "There was an error recording the give."; this.$notify( { group: "alert", type: "danger", title: "Error", text: message, }, -1, ); } } // Helper functions for readability /** * @param result response "data" from the server * @returns true if the result indicates an error */ // eslint-disable-next-line @typescript-eslint/no-explicit-any isGiveCreationError(result: any) { return result.status !== 201 || result.data?.error; } /** * @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data") * @returns best guess at an error message */ // eslint-disable-next-line @typescript-eslint/no-explicit-any getGiveCreationErrorMessage(result: any) { return ( result.error?.userMessage || result.error?.error || result.response?.data?.error?.message ); } explainData() { this.$notify( { group: "alert", type: "success", title: "Data Sharing", text: libsUtil.PRIVACY_MESSAGE, }, -1, ); } } </script>