Files
crowd-funder-for-time-pwa/src/components/GiftedDialog.vue
Matthew Raymer e5518cd47c fix: update Vue template syntax and improve Vite config
- Fix Vue template syntax in App.vue by using proper event handler format
- Update Vite config to properly handle ESM imports and crypto modules
- Add manual chunks for better code splitting
- Improve environment variable handling in vite-env.d.ts
- Fix TypeScript linting errors in App.vue
2025-04-18 09:59:33 +00:00

415 lines
11 KiB
Vue

<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">
{{ customTitle }}
</h1>
<input
v-model="description"
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
:placeholder="prompt || 'What was given?'"
/>
<div class="flex flex-row justify-center">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()"
>
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
</div>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
/>
<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: 'gifted-details',
query: {
amountInput,
description,
giverDid: giver?.did,
giverName: giver?.name,
offerId,
fulfillsProjectId: toProjectId,
providerProjectId: fromProjectId,
recipientDid: receiver?.did,
recipientName: receiver?.name,
unitCode
}
}"
class="text-blue-500"
>
Photo & more options ...
</router-link>
</span>
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<font-awesome
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</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 &amp; 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 {
createAndSubmitGive,
didInfo,
serverMessageForUser
} from '../libs/endorserServer'
import * as libsUtil from '../libs/util'
import { db, retrieveSettingsForActiveAccount } from '../db/index'
import { Contact } from '../db/tables/contacts'
import { retrieveAccountDids } from '../libs/util'
@Component
export default class GiftedDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void
@Prop() fromProjectId = ''
@Prop() toProjectId = ''
activeDid = ''
allContacts: Array<Contact> = []
allMyDids: Array<string> = []
apiServer = ''
amountInput = '0'
callbackOnSuccess?: (amount: number) => void = () => {}
customTitle?: string
description = ''
giver?: libsUtil.GiverReceiverInputInfo // undefined means no identified giver agent
isTrade = false
offerId = ''
prompt = ''
receiver?: libsUtil.GiverReceiverInputInfo
unitCode = 'HUR'
visible = false
libsUtil = libsUtil
async open(
giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo,
offerId?: string,
customTitle?: string,
prompt?: string,
callbackOnSuccess: (amount: number) => void = () => {}
) {
this.customTitle = customTitle
this.giver = giver
this.prompt = prompt || ''
this.receiver = receiver
// if we show "given to user" selection, default checkbox to true
this.amountInput = '0'
this.callbackOnSuccess = callbackOnSuccess
this.offerId = offerId || ''
try {
const settings = await retrieveSettingsForActiveAccount()
this.apiServer = settings.apiServer || ''
this.activeDid = settings.activeDid || ''
this.allContacts = await db.contacts.toArray()
this.allMyDids = await retrieveAccountDids()
if (this.giver && !this.giver.name) {
this.giver.name = didInfo(
this.giver.did,
this.activeDid,
this.allMyDids,
this.allContacts
)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
logger.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.unitCode)
this.unitCode = 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.giver = undefined
this.amountInput = '0'
this.prompt = ''
this.unitCode = 'HUR'
}
async confirm() {
if (!this.activeDid) {
this.$notify(
{
group: 'alert',
type: 'danger',
title: 'Error',
text: 'You must select an identifier before you can record a give.'
},
3000
)
return
}
if (parseFloat(this.amountInput) < 0) {
this.$notify(
{
group: 'alert',
type: 'danger',
text: 'You may not send a negative number.',
title: ''
},
2000
)
return
}
if (!this.description && !parseFloat(this.amountInput)) {
this.$notify(
{
group: 'alert',
type: 'danger',
title: 'Error',
text: `You must enter a description or some number of ${
this.libsUtil.UNIT_LONG[this.unitCode]
}.`
},
2000
)
return
}
this.close()
this.$notify(
{
group: 'alert',
type: 'toast',
text: 'Recording the give...',
title: ''
},
1000
)
// this is asynchronous, but we don't need to wait for it to complete
await this.recordGive(
(this.giver?.did as string) || null,
(this.receiver?.did as string) || null,
this.description,
parseFloat(this.amountInput),
this.unitCode
).then(() => {
this.eraseValues()
})
}
/**
*
* @param giverDid may be null
* @param recipientDid may be null
* @param description may be an empty string
* @param amount may be 0
* @param unitCode may be omitted, defaults to "HUR"
*/
async recordGive(
giverDid: string | null,
recipientDid: string | null,
description: string,
amount: number,
unitCode: string = 'HUR'
) {
try {
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
this.activeDid,
giverDid as string,
recipientDid as string,
description,
amount,
unitCode,
this.toProjectId,
this.offerId,
this.isTrade,
undefined,
this.fromProjectId
)
if (
result.type === 'error' ||
this.isGiveCreationError(result.response)
) {
const errorMessage = this.getGiveCreationErrorMessage(result)
logger.error('Error with give creation result:', result)
this.$notify(
{
group: 'alert',
type: 'danger',
title: 'Error',
text: errorMessage || 'There was an error creating the give.'
},
-1
)
} else {
this.$notify(
{
group: 'alert',
type: 'success',
title: 'Success',
text: `That ${this.isTrade ? 'trade' : 'gift'} was recorded.`
},
7000
)
if (this.callbackOnSuccess) {
this.callbackOnSuccess(amount)
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
logger.error('Error with give recordation caught:', error)
const errorMessage =
error.userMessage ||
serverMessageForUser(error) ||
'There was an error recording the give.'
this.$notify(
{
group: 'alert',
type: 'danger',
title: 'Error',
text: errorMessage
},
-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
isGiveCreationError(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
getGiveCreationErrorMessage(result: any) {
return (
result.error?.userMessage ||
result.error?.error ||
result.response?.data?.error?.message
)
}
explainData() {
this.$notify(
{
group: 'alert',
type: 'success',
title: 'Data Sharing',
text: libsUtil.PRIVACY_MESSAGE
},
-1
)
}
}
</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>