Browse Source
			
			
			
			
				
		Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/105gifted-camera-improvements
				 18 changed files with 842 additions and 61 deletions
			
			
		| @ -0,0 +1,6 @@ | |||||
|  | # Only the variables that start with VUE_APP_ are seen in the application process.env in Vue. | ||||
|  | 
 | ||||
|  | # this won't resolve as a URL on production; it's a URN only found in the test system | ||||
|  | VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK | ||||
|  | VUE_APP_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 | ||||
|  | VUE_APP_DEFAULT_IMAGE_API_SERVER=http://localhost:3002 | ||||
| @ -0,0 +1,4 @@ | |||||
|  | # Only the variables that start with VUE_APP_ are seen in the application process.env in Vue. | ||||
|  | VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H | ||||
|  | VUE_APP_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch | ||||
|  | VUE_APP_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app | ||||
| @ -0,0 +1,211 @@ | |||||
|  | <template> | ||||
|  |   <div v-if="visible" class="dialog-overlay"> | ||||
|  |     <!-- Breadcrumb --> | ||||
|  |     <div class="dialog"> | ||||
|  |       <!-- Back --> | ||||
|  |       <div class="text-lg text-center font-light relative px-7"> | ||||
|  |         <h1 | ||||
|  |           class="text-lg text-center px-2 py-1 absolute -right-2 -top-1" | ||||
|  |           @click="close()" | ||||
|  |         > | ||||
|  |           <fa icon="xmark" class="fa-fw"></fa> | ||||
|  |         </h1> | ||||
|  |       </div> | ||||
|  | 
 | ||||
|  |       <!-- Heading --> | ||||
|  |       <h1 id="ViewHeading" class="text-4xl text-center font-light pt-4"> | ||||
|  |         <span v-if="uploading"> Uploading... </span> | ||||
|  |         <span v-else-if="blob"> Look Good? </span> | ||||
|  |         <span v-else> Say "Cheese"! </span> | ||||
|  |       </h1> | ||||
|  | 
 | ||||
|  |       <div v-if="uploading" class="flex justify-center"> | ||||
|  |         <fa icon="spinner" class="fa-spin fa-3x text-center block" /> | ||||
|  |       </div> | ||||
|  |       <div v-else-if="blob"> | ||||
|  |         <div class="flex justify-around"> | ||||
|  |           <button | ||||
|  |             @click="uploadImage" | ||||
|  |             class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-2 rounded-full" | ||||
|  |           > | ||||
|  |             <span>Upload</span> | ||||
|  |           </button> | ||||
|  |           <button | ||||
|  |             @click="retryImage" | ||||
|  |             class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-2 rounded-full" | ||||
|  |           > | ||||
|  |             <span>Retry</span> | ||||
|  |           </button> | ||||
|  |         </div> | ||||
|  |         <img :src="URL.createObjectURL(blob)" class="mt-2 w-full" /> | ||||
|  |       </div> | ||||
|  |       <div v-else> | ||||
|  |         <!-- | ||||
|  |           Camera "resolution" doesn't change how it shows on screen but rather stretches the result, eg the following which just stretches it vertically: | ||||
|  |           :resolution="{ width: 375, height: 812 }" | ||||
|  |         --> | ||||
|  |         <camera facingMode="environment" autoplay ref="camera"> | ||||
|  |           <div class="absolute bottom-0 w-full flex justify-center pb-4"> | ||||
|  |             <!-- Button --> | ||||
|  |             <button | ||||
|  |               @click="takeImage" | ||||
|  |               class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-2 rounded-full" | ||||
|  |             > | ||||
|  |               <fa icon="camera" class="fa-fw"></fa> | ||||
|  |             </button> | ||||
|  |           </div> | ||||
|  |         </camera> | ||||
|  |       </div> | ||||
|  |     </div> | ||||
|  |   </div> | ||||
|  | </template> | ||||
|  | 
 | ||||
|  | <script lang="ts"> | ||||
|  | import axios from "axios"; | ||||
|  | import Camera from "simple-vue-camera"; | ||||
|  | import { Component, Vue } from "vue-facing-decorator"; | ||||
|  | 
 | ||||
|  | import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; | ||||
|  | import { getIdentity } from "@/libs/util"; | ||||
|  | import { db } from "@/db/index"; | ||||
|  | import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; | ||||
|  | import { accessToken } from "@/libs/crypto"; | ||||
|  | 
 | ||||
|  | @Component({ components: { Camera } }) | ||||
|  | export default class GiftedPhotoDialog extends Vue { | ||||
|  |   $notify!: (notification: NotificationIface, timeout?: number) => void; | ||||
|  | 
 | ||||
|  |   activeDid = ""; | ||||
|  |   blob: Blob | null = null; | ||||
|  |   setImage: (arg: string) => void = () => {}; | ||||
|  |   uploading = false; | ||||
|  |   visible = false; | ||||
|  | 
 | ||||
|  |   URL = window.URL || window.webkitURL; | ||||
|  | 
 | ||||
|  |   async mounted() { | ||||
|  |     try { | ||||
|  |       await db.open(); | ||||
|  |       const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; | ||||
|  |       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, | ||||
|  |       ); | ||||
|  |     } | ||||
|  |   } | ||||
|  | 
 | ||||
|  |   open(setImageFn: (arg: string) => void) { | ||||
|  |     this.visible = true; | ||||
|  |     this.setImage = setImageFn; | ||||
|  |   } | ||||
|  | 
 | ||||
|  |   close() { | ||||
|  |     this.visible = false; | ||||
|  |     this.blob = null; | ||||
|  |   } | ||||
|  | 
 | ||||
|  |   async takeImage(/* payload: MouseEvent */) { | ||||
|  |     const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>; | ||||
|  |     this.blob = await cameraComponent?.snapshot(); // png is default; if that changes, change extension in formData.append | ||||
|  |     if (!this.blob) { | ||||
|  |       this.$notify( | ||||
|  |         { | ||||
|  |           group: "alert", | ||||
|  |           type: "danger", | ||||
|  |           title: "Error", | ||||
|  |           text: "There was an error taking the picture. Please try again.", | ||||
|  |         }, | ||||
|  |         5000, | ||||
|  |       ); | ||||
|  |       return; | ||||
|  |     } | ||||
|  |   } | ||||
|  | 
 | ||||
|  |   async retryImage() { | ||||
|  |     this.blob = null; | ||||
|  |   } | ||||
|  | 
 | ||||
|  |   async uploadImage() { | ||||
|  |     this.uploading = true; | ||||
|  |     const identifier = await getIdentity(this.activeDid); | ||||
|  |     const token = await accessToken(identifier); | ||||
|  |     const headers = { | ||||
|  |       Authorization: "Bearer " + token, | ||||
|  |     }; | ||||
|  |     const formData = new FormData(); | ||||
|  |     if (!this.blob) { | ||||
|  |       // yeah, this should never happen, but it helps with subsequent type checking | ||||
|  |       this.$notify( | ||||
|  |         { | ||||
|  |           group: "alert", | ||||
|  |           type: "danger", | ||||
|  |           title: "Error", | ||||
|  |           text: "There was an error finding the picture. Please try again.", | ||||
|  |         }, | ||||
|  |         5000, | ||||
|  |       ); | ||||
|  |       this.uploading = false; | ||||
|  |       return; | ||||
|  |     } | ||||
|  |     formData.append("image", this.blob, "snapshot.png"); // png is set in snapshot() | ||||
|  |     formData.append("claimType", "GiveAction"); | ||||
|  |     try { | ||||
|  |       const response = await axios.post( | ||||
|  |         DEFAULT_IMAGE_API_SERVER + "/image", | ||||
|  |         formData, | ||||
|  |         { headers }, | ||||
|  |       ); | ||||
|  |       this.uploading = false; | ||||
|  | 
 | ||||
|  |       this.visible = false; | ||||
|  |       this.blob = null; | ||||
|  |       this.setImage(response.data.url as string); | ||||
|  |     } catch (error) { | ||||
|  |       console.error("Error uploading the image", error); | ||||
|  |       this.$notify( | ||||
|  |         { | ||||
|  |           group: "alert", | ||||
|  |           type: "danger", | ||||
|  |           title: "Error", | ||||
|  |           text: "There was an error saving the picture. Please try again.", | ||||
|  |         }, | ||||
|  |         5000, | ||||
|  |       ); | ||||
|  |       this.uploading = false; | ||||
|  |       this.blob = null; | ||||
|  |     } | ||||
|  |   } | ||||
|  | } | ||||
|  | </script> | ||||
|  | 
 | ||||
|  | <style> | ||||
|  | .dialog-overlay { | ||||
|  |   position: fixed; | ||||
|  |   top: 0; | ||||
|  |   left: 0; | ||||
|  |   right: 0; | ||||
|  |   bottom: 0; | ||||
|  |   background-color: rgba(0, 0, 0, 0.5); | ||||
|  |   display: flex; | ||||
|  |   justify-content: center; | ||||
|  |   align-items: center; | ||||
|  |   padding: 1.5rem; | ||||
|  | } | ||||
|  | 
 | ||||
|  | .dialog { | ||||
|  |   background-color: white; | ||||
|  |   padding: 1rem; | ||||
|  |   border-radius: 0.5rem; | ||||
|  |   width: 100%; | ||||
|  |   max-width: 700px; | ||||
|  | } | ||||
|  | </style> | ||||
| @ -0,0 +1,428 @@ | |||||
|  | <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-blue-500 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 | ||||
|  |     </p> | ||||
|  |     <button | ||||
|  |       class="block w-full text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2" | ||||
|  |       @click="confirm" | ||||
|  |     > | ||||
|  |       Sign & Send | ||||
|  |     </button> | ||||
|  |     <button | ||||
|  |       class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md" | ||||
|  |       @click="cancel" | ||||
|  |     > | ||||
|  |       Cancel | ||||
|  |     </button> | ||||
|  |   </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() { | ||||
|  |     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() { | ||||
|  |     if (!this.activeDid) { | ||||
|  |       this.$notify( | ||||
|  |         { | ||||
|  |           group: "alert", | ||||
|  |           type: "danger", | ||||
|  |           title: "Error", | ||||
|  |           text: "You must select an identifier before you can record a give.", | ||||
|  |         }, | ||||
|  |         -1, | ||||
|  |       ); | ||||
|  |       return; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     if (!this.description && !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] | ||||
|  |           }.`, | ||||
|  |         }, | ||||
|  |         -1, | ||||
|  |       ); | ||||
|  |       return; | ||||
|  |     } | ||||
|  | 
 | ||||
|  |     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 | ||||
|  |     ); | ||||
|  |   } | ||||
|  | } | ||||
|  | </script> | ||||
					Loading…
					
					
				
		Reference in new issue