3 changed files with 443 additions and 30 deletions
			
			
		| @ -0,0 +1,317 @@ | |||
| <template> | |||
|   <div v-if="visible" class="dialog-overlay"> | |||
|     <div class="dialog"> | |||
|       <h1 class="text-xl font-bold text-center mb-4">Offer Help</h1> | |||
|       <input | |||
|         type="text" | |||
|         class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" | |||
|         placeholder="Description, prerequisites, terms, etc." | |||
|         v-model="description" | |||
|       /> | |||
|       <div class="flex flex-row mb-6"> | |||
|         <span | |||
|           class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2" | |||
|         > | |||
|           Hours | |||
|         </span> | |||
|         <div | |||
|           class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" | |||
|           @click="decrement()" | |||
|         > | |||
|           <fa icon="chevron-left" /> | |||
|         </div> | |||
|         <input | |||
|           type="text" | |||
|           class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center" | |||
|           v-model="hours" | |||
|         /> | |||
|         <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 flex-row mb-6"> | |||
|         <span | |||
|           class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center px-2 py-2" | |||
|         > | |||
|           Expiration | |||
|         </span> | |||
|         <input | |||
|           type="text" | |||
|           class="w-full border border-slate-400 px-2 py-2 rounded-r" | |||
|           :placeholder="'Date, eg. ' + new Date().toISOString().slice(0, 10)" | |||
|           v-model="expirationDateInput" | |||
|         /> | |||
|       </div> | |||
|       <p class="text-center mb-2 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> | |||
|     </div> | |||
|   </div> | |||
| </template> | |||
| 
 | |||
| <script lang="ts"> | |||
| import { Vue, Component, Prop } from "vue-facing-decorator"; | |||
| import { createAndSubmitOffer } from "@/libs/endorserServer"; | |||
| import { accountsDB, db } from "@/db/index"; | |||
| import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; | |||
| import { Account } from "@/db/tables/accounts"; | |||
| 
 | |||
| interface Notification { | |||
|   group: string; | |||
|   type: string; | |||
|   title: string; | |||
|   text: string; | |||
| } | |||
| 
 | |||
| @Component | |||
| export default class OfferDialog extends Vue { | |||
|   $notify!: (notification: Notification, timeout?: number) => void; | |||
| 
 | |||
|   @Prop message = ""; | |||
|   @Prop projectId = ""; | |||
| 
 | |||
|   activeDid = ""; | |||
|   apiServer = ""; | |||
| 
 | |||
|   description = ""; | |||
|   expirationDateInput = ""; | |||
|   hours = "0"; | |||
|   visible = false; | |||
| 
 | |||
|   async created() { | |||
|     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.log("Error retrieving settings from database:", err); | |||
|       this.$notify( | |||
|         { | |||
|           group: "alert", | |||
|           type: "danger", | |||
|           title: "Error", | |||
|           text: | |||
|             err.message || | |||
|             "There was an error retrieving the latest sweet, sweet action.", | |||
|         }, | |||
|         -1, | |||
|       ); | |||
|     } | |||
|   } | |||
| 
 | |||
|   open() { | |||
|     this.visible = true; | |||
|   } | |||
| 
 | |||
|   close() { | |||
|     this.visible = false; | |||
|   } | |||
| 
 | |||
|   increment() { | |||
|     this.hours = `${(parseFloat(this.hours) || 0) + 1}`; | |||
|   } | |||
| 
 | |||
|   decrement() { | |||
|     this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`; | |||
|   } | |||
| 
 | |||
|   cancel() { | |||
|     this.close(); | |||
|     this.description = ""; | |||
|     this.hours = "0"; | |||
|   } | |||
| 
 | |||
|   async confirm() { | |||
|     this.close(); | |||
|     this.$notify( | |||
|       { | |||
|         group: "alert", | |||
|         type: "toast", | |||
|         text: "Recording the offer...", | |||
|         title: "", | |||
|       }, | |||
|       1000, | |||
|     ); | |||
|     // this is asynchronous, but we don't need to wait for it to complete | |||
|     this.recordOffer( | |||
|       this.description, | |||
|       parseFloat(this.hours), | |||
|       this.expirationDateInput, | |||
|     ).then(() => { | |||
|       this.description = ""; | |||
|       this.hours = "0"; | |||
|     }); | |||
|   } | |||
| 
 | |||
|   public async getIdentity(activeDid: string) { | |||
|     await accountsDB.open(); | |||
|     const account = (await accountsDB.accounts | |||
|       .where("did") | |||
|       .equals(activeDid) | |||
|       .first()) as Account; | |||
|     const identity = JSON.parse(account?.identity || "null"); | |||
| 
 | |||
|     if (!identity) { | |||
|       throw new Error( | |||
|         "Attempted to load Offer records for DID ${activeDid} but no identity was found", | |||
|       ); | |||
|     } | |||
|     return identity; | |||
|   } | |||
| 
 | |||
|   /** | |||
|    * | |||
|    * @param description may be an empty string | |||
|    * @param hours may be 0 | |||
|    */ | |||
|   public async recordOffer( | |||
|     description?: string, | |||
|     hours?: number, | |||
|     expirationDateInput?: string, | |||
|   ) { | |||
|     if (!this.activeDid) { | |||
|       this.$notify( | |||
|         { | |||
|           group: "alert", | |||
|           type: "danger", | |||
|           title: "Error", | |||
|           text: "You must select an identity before you can record an offer.", | |||
|         }, | |||
|         -1, | |||
|       ); | |||
|       return; | |||
|     } | |||
| 
 | |||
|     if (!description && !hours) { | |||
|       this.$notify( | |||
|         { | |||
|           group: "alert", | |||
|           type: "danger", | |||
|           title: "Error", | |||
|           text: "You must enter a description or some number of hours.", | |||
|         }, | |||
|         -1, | |||
|       ); | |||
|       return; | |||
|     } | |||
| 
 | |||
|     try { | |||
|       const identity = await this.getIdentity(this.activeDid); | |||
|       const result = await createAndSubmitOffer( | |||
|         this.axios, | |||
|         this.apiServer, | |||
|         identity, | |||
|         description, | |||
|         hours, | |||
|         expirationDateInput, | |||
|         this.projectId, | |||
|       ); | |||
| 
 | |||
|       if ( | |||
|         result.type === "error" || | |||
|         this.isOfferCreationError(result.response) | |||
|       ) { | |||
|         const errorMessage = this.getOfferCreationErrorMessage(result); | |||
|         console.log("Error with offer creation result:", result); | |||
|         this.$notify( | |||
|           { | |||
|             group: "alert", | |||
|             type: "danger", | |||
|             title: "Error", | |||
|             text: errorMessage || "There was an error creating the offer.", | |||
|           }, | |||
|           -1, | |||
|         ); | |||
|       } else { | |||
|         this.$notify( | |||
|           { | |||
|             group: "alert", | |||
|             type: "success", | |||
|             title: "Success", | |||
|             text: "That offer was recorded.", | |||
|           }, | |||
|           10000, | |||
|         ); | |||
|       } | |||
|       // eslint-disable-next-line @typescript-eslint/no-explicit-any | |||
|     } catch (error: any) { | |||
|       console.log("Error with offer recordation caught:", error); | |||
|       const message = | |||
|         error.userMessage || | |||
|         error.response?.data?.error?.message || | |||
|         "There was an error recording the offer."; | |||
|       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 | |||
|   isOfferCreationError(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 | |||
|   getOfferCreationErrorMessage(result: any) { | |||
|     return ( | |||
|       result.error?.userMessage || | |||
|       result.error?.error || | |||
|       result.response?.data?.error?.message | |||
|     ); | |||
|   } | |||
| } | |||
| </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: 500px; | |||
| } | |||
| </style> | |||
					Loading…
					
					
				
		Reference in new issue