Browse Source
			
			
			
			
				
		Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/83pull/84/head
				 3 changed files with 518 additions and 32 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