forked from jsnbuchanan/crowd-funder-for-time-pwa
Changes: - Move v-model directives before other attributes - Move v-bind directives before event handlers - Reorder attributes for better readability - Fix template attribute ordering across components - Improve eslint rules - add default vite config for testing (handles nostr error too) This follows Vue.js style guide recommendations for attribute ordering and improves template consistency.
341 lines
8.6 KiB
Vue
341 lines
8.6 KiB
Vue
<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
|
|
v-model="description"
|
|
type="text"
|
|
data-testId="inputDescription"
|
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
|
placeholder="Description of what is offered"
|
|
/>
|
|
<div class="flex flex-row mt-2">
|
|
<span
|
|
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2"
|
|
@click="changeUnitCode()"
|
|
>
|
|
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
|
|
</span>
|
|
<div
|
|
v-if="amountInput !== '0'"
|
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
|
@click="decrement()"
|
|
>
|
|
<font-awesome icon="chevron-left" />
|
|
</div>
|
|
<input
|
|
v-model="amountInput"
|
|
data-testId="inputOfferAmount"
|
|
type="number"
|
|
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
|
/>
|
|
<div
|
|
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
|
@click="increment()"
|
|
>
|
|
<font-awesome icon="chevron-right" />
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 flex justify-center">
|
|
<span>
|
|
<router-link
|
|
:to="{
|
|
name: 'offer-details',
|
|
query: {
|
|
amountInput,
|
|
description,
|
|
offererDid: activeDid,
|
|
projectId,
|
|
projectName,
|
|
recipientDid,
|
|
recipientName,
|
|
unitCode: amountUnitCode,
|
|
},
|
|
}"
|
|
class="text-blue-500"
|
|
>
|
|
Conditions & more options...
|
|
</router-link>
|
|
</span>
|
|
</div>
|
|
<p class="text-center mt-6 mb-2 italic">
|
|
Sign & Send to publish to the world
|
|
</p>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
<button
|
|
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
|
@click="confirm"
|
|
>
|
|
Sign & Send
|
|
</button>
|
|
<button
|
|
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
|
@click="cancel"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
|
|
|
import { NotificationIface } from "../constants/app";
|
|
import {
|
|
createAndSubmitOffer,
|
|
serverMessageForUser,
|
|
} from "../libs/endorserServer";
|
|
import * as libsUtil from "../libs/util";
|
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
|
|
|
@Component
|
|
export default class OfferDialog extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
|
|
@Prop projectId?: string;
|
|
@Prop projectName?: string;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
|
|
amountInput = "0";
|
|
amountUnitCode = "HUR";
|
|
description = "";
|
|
expirationDateInput = "";
|
|
recipientDid? = "";
|
|
recipientName? = "";
|
|
visible = false;
|
|
|
|
libsUtil = libsUtil;
|
|
|
|
async open(recipientDid?: string, recipientName?: string) {
|
|
try {
|
|
this.recipientDid = recipientDid;
|
|
this.recipientName = recipientName;
|
|
|
|
const settings = await retrieveSettingsForActiveAccount();
|
|
this.apiServer = settings.apiServer || "";
|
|
this.activeDid = settings.activeDid || "";
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (err: any) {
|
|
console.error("Error retrieving settings from database:", err);
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: err.message || "There was an error retrieving your settings.",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
this.visible = true;
|
|
}
|
|
|
|
close() {
|
|
// close the dialog but don't change values (since it might be submitting info)
|
|
this.visible = false;
|
|
}
|
|
|
|
changeUnitCode() {
|
|
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
|
const index = units.indexOf(this.amountUnitCode);
|
|
this.amountUnitCode = units[(index + 1) % units.length];
|
|
}
|
|
|
|
increment() {
|
|
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
|
}
|
|
|
|
decrement() {
|
|
this.amountInput = `${Math.max(
|
|
0,
|
|
(parseFloat(this.amountInput) || 1) - 1,
|
|
)}`;
|
|
}
|
|
|
|
cancel() {
|
|
this.close();
|
|
this.eraseValues();
|
|
}
|
|
|
|
eraseValues() {
|
|
this.description = "";
|
|
this.amountInput = "0";
|
|
this.amountUnitCode = "HUR";
|
|
}
|
|
|
|
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.amountInput),
|
|
this.amountUnitCode,
|
|
this.expirationDateInput,
|
|
).then(() => {
|
|
this.description = "";
|
|
this.amountInput = "0";
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param description may be an empty string
|
|
* @param hours may be 0
|
|
* @param unitCode may be omitted, defaults to "HUR"
|
|
*/
|
|
public async recordOffer(
|
|
description: string,
|
|
amount: number,
|
|
unitCode: string = "HUR",
|
|
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.",
|
|
},
|
|
7000,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!description && !amount) {
|
|
this.$notify(
|
|
{
|
|
group: "alert",
|
|
type: "danger",
|
|
title: "Error",
|
|
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`,
|
|
},
|
|
-1,
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await createAndSubmitOffer(
|
|
this.axios,
|
|
this.apiServer,
|
|
this.activeDid,
|
|
description,
|
|
amount,
|
|
unitCode,
|
|
"",
|
|
expirationDateInput,
|
|
this.recipientDid,
|
|
this.projectId,
|
|
);
|
|
|
|
if (
|
|
result.type === "error" ||
|
|
this.isOfferCreationError(result.response)
|
|
) {
|
|
const errorMessage = this.getOfferCreationErrorMessage(result);
|
|
console.error("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.",
|
|
},
|
|
5000,
|
|
);
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
console.error("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 (
|
|
serverMessageForUser(result) ||
|
|
result.error?.userMessage ||
|
|
result.error?.error
|
|
);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.dialog-overlay {
|
|
z-index: 50;
|
|
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>
|