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