<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>