Browse Source
Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/83pull/84/head
anomalist
1 year ago
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