Browse Source
Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/105gifted-camera-improvements
trentlarson
8 months ago
18 changed files with 842 additions and 61 deletions
@ -0,0 +1,6 @@ |
|||||
|
# Only the variables that start with VUE_APP_ are seen in the application process.env in Vue. |
||||
|
|
||||
|
# this won't resolve as a URL on production; it's a URN only found in the test system |
||||
|
VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HNTZYJJXTGT0EZS3VEJGX7AK |
||||
|
VUE_APP_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 |
||||
|
VUE_APP_DEFAULT_IMAGE_API_SERVER=http://localhost:3002 |
@ -0,0 +1,4 @@ |
|||||
|
# Only the variables that start with VUE_APP_ are seen in the application process.env in Vue. |
||||
|
VUE_APP_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01GXYPFF7FA03NXKPYY142PY4H |
||||
|
VUE_APP_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch |
||||
|
VUE_APP_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app |
@ -0,0 +1,211 @@ |
|||||
|
<template> |
||||
|
<div v-if="visible" class="dialog-overlay"> |
||||
|
<!-- Breadcrumb --> |
||||
|
<div class="dialog"> |
||||
|
<!-- Back --> |
||||
|
<div class="text-lg text-center font-light relative px-7"> |
||||
|
<h1 |
||||
|
class="text-lg text-center px-2 py-1 absolute -right-2 -top-1" |
||||
|
@click="close()" |
||||
|
> |
||||
|
<fa icon="xmark" class="fa-fw"></fa> |
||||
|
</h1> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Heading --> |
||||
|
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4"> |
||||
|
<span v-if="uploading"> Uploading... </span> |
||||
|
<span v-else-if="blob"> Look Good? </span> |
||||
|
<span v-else> Say "Cheese"! </span> |
||||
|
</h1> |
||||
|
|
||||
|
<div v-if="uploading" class="flex justify-center"> |
||||
|
<fa icon="spinner" class="fa-spin fa-3x text-center block" /> |
||||
|
</div> |
||||
|
<div v-else-if="blob"> |
||||
|
<div class="flex justify-around"> |
||||
|
<button |
||||
|
@click="uploadImage" |
||||
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-2 rounded-full" |
||||
|
> |
||||
|
<span>Upload</span> |
||||
|
</button> |
||||
|
<button |
||||
|
@click="retryImage" |
||||
|
class="bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-2 rounded-full" |
||||
|
> |
||||
|
<span>Retry</span> |
||||
|
</button> |
||||
|
</div> |
||||
|
<img :src="URL.createObjectURL(blob)" class="mt-2 w-full" /> |
||||
|
</div> |
||||
|
<div v-else> |
||||
|
<!-- |
||||
|
Camera "resolution" doesn't change how it shows on screen but rather stretches the result, eg the following which just stretches it vertically: |
||||
|
:resolution="{ width: 375, height: 812 }" |
||||
|
--> |
||||
|
<camera facingMode="environment" autoplay ref="camera"> |
||||
|
<div class="absolute bottom-0 w-full flex justify-center pb-4"> |
||||
|
<!-- Button --> |
||||
|
<button |
||||
|
@click="takeImage" |
||||
|
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-2 rounded-full" |
||||
|
> |
||||
|
<fa icon="camera" class="fa-fw"></fa> |
||||
|
</button> |
||||
|
</div> |
||||
|
</camera> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import axios from "axios"; |
||||
|
import Camera from "simple-vue-camera"; |
||||
|
import { Component, Vue } from "vue-facing-decorator"; |
||||
|
|
||||
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; |
||||
|
import { getIdentity } from "@/libs/util"; |
||||
|
import { db } from "@/db/index"; |
||||
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; |
||||
|
import { accessToken } from "@/libs/crypto"; |
||||
|
|
||||
|
@Component({ components: { Camera } }) |
||||
|
export default class GiftedPhotoDialog extends Vue { |
||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void; |
||||
|
|
||||
|
activeDid = ""; |
||||
|
blob: Blob | null = null; |
||||
|
setImage: (arg: string) => void = () => {}; |
||||
|
uploading = false; |
||||
|
visible = false; |
||||
|
|
||||
|
URL = window.URL || window.webkitURL; |
||||
|
|
||||
|
async mounted() { |
||||
|
try { |
||||
|
await db.open(); |
||||
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings; |
||||
|
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, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
open(setImageFn: (arg: string) => void) { |
||||
|
this.visible = true; |
||||
|
this.setImage = setImageFn; |
||||
|
} |
||||
|
|
||||
|
close() { |
||||
|
this.visible = false; |
||||
|
this.blob = null; |
||||
|
} |
||||
|
|
||||
|
async takeImage(/* payload: MouseEvent */) { |
||||
|
const cameraComponent = this.$refs.camera as InstanceType<typeof Camera>; |
||||
|
this.blob = await cameraComponent?.snapshot(); // png is default; if that changes, change extension in formData.append |
||||
|
if (!this.blob) { |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "There was an error taking the picture. Please try again.", |
||||
|
}, |
||||
|
5000, |
||||
|
); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async retryImage() { |
||||
|
this.blob = null; |
||||
|
} |
||||
|
|
||||
|
async uploadImage() { |
||||
|
this.uploading = true; |
||||
|
const identifier = await getIdentity(this.activeDid); |
||||
|
const token = await accessToken(identifier); |
||||
|
const headers = { |
||||
|
Authorization: "Bearer " + token, |
||||
|
}; |
||||
|
const formData = new FormData(); |
||||
|
if (!this.blob) { |
||||
|
// yeah, this should never happen, but it helps with subsequent type checking |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "There was an error finding the picture. Please try again.", |
||||
|
}, |
||||
|
5000, |
||||
|
); |
||||
|
this.uploading = false; |
||||
|
return; |
||||
|
} |
||||
|
formData.append("image", this.blob, "snapshot.png"); // png is set in snapshot() |
||||
|
formData.append("claimType", "GiveAction"); |
||||
|
try { |
||||
|
const response = await axios.post( |
||||
|
DEFAULT_IMAGE_API_SERVER + "/image", |
||||
|
formData, |
||||
|
{ headers }, |
||||
|
); |
||||
|
this.uploading = false; |
||||
|
|
||||
|
this.visible = false; |
||||
|
this.blob = null; |
||||
|
this.setImage(response.data.url as string); |
||||
|
} catch (error) { |
||||
|
console.error("Error uploading the image", error); |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "There was an error saving the picture. Please try again.", |
||||
|
}, |
||||
|
5000, |
||||
|
); |
||||
|
this.uploading = false; |
||||
|
this.blob = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</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: 700px; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,428 @@ |
|||||
|
<template> |
||||
|
<QuickNav /> |
||||
|
<TopMessage /> |
||||
|
|
||||
|
<!-- CONTENT --> |
||||
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> |
||||
|
<!-- Back --> |
||||
|
<div class="text-lg text-center font-light relative px-7"> |
||||
|
<h1 |
||||
|
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" |
||||
|
@click="cancel()" |
||||
|
> |
||||
|
<fa icon="chevron-left" class="fa-fw"></fa> |
||||
|
</h1> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Heading --> |
||||
|
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1> |
||||
|
|
||||
|
<h1 class="text-xl font-bold text-center mb-4"> |
||||
|
{{ message }} {{ giverName || "somebody not named" }} |
||||
|
</h1> |
||||
|
<textarea |
||||
|
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" |
||||
|
placeholder="What was received" |
||||
|
v-model="description" |
||||
|
/> |
||||
|
<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] }} |
||||
|
</span> |
||||
|
<div |
||||
|
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" |
||||
|
@click="amountInput === '0' ? null : decrement()" |
||||
|
> |
||||
|
<fa icon="chevron-left" /> |
||||
|
</div> |
||||
|
<input |
||||
|
type="number" |
||||
|
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20" |
||||
|
v-model="amountInput" |
||||
|
/> |
||||
|
<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 justify-center mt-4"> |
||||
|
<span v-if="imageUrl" class="flex justify-between"> |
||||
|
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4"> |
||||
|
<img :src="imageUrl" class="h-24 rounded-xl" /> |
||||
|
</a> |
||||
|
<fa |
||||
|
icon="trash-can" |
||||
|
@click="confirmDeleteImage" |
||||
|
class="text-red-500 fa-fw ml-8 mt-10" |
||||
|
/> |
||||
|
</span> |
||||
|
<span v-else> |
||||
|
<fa |
||||
|
icon="camera" |
||||
|
class="bg-blue-500 text-white px-2 py-2 rounded-md" |
||||
|
@click="openPhotoDialog" |
||||
|
/> |
||||
|
</span> |
||||
|
</div> |
||||
|
<GiftedPhotoDialog ref="photoDialog" /> |
||||
|
|
||||
|
<div v-if="projectId" class="mt-4"> |
||||
|
<fa |
||||
|
icon="check" |
||||
|
class="bg-slate-500 text-white h-5 w-5 px-0.5 py-0.5 mr-2 rounded" |
||||
|
/> |
||||
|
<label class="text-sm">This is given to a project</label> |
||||
|
</div> |
||||
|
|
||||
|
<div v-if="!projectId" class="mt-4"> |
||||
|
<input type="checkbox" class="h-6 w-6 mr-2" v-model="givenToUser" /> |
||||
|
<label class="text-sm">Given to you</label> |
||||
|
</div> |
||||
|
|
||||
|
<div class="mt-4"> |
||||
|
<input type="checkbox" class="h-6 w-6 mr-2" v-model="isTrade" /> |
||||
|
<label class="text-sm">Trade (not a gift)</label> |
||||
|
</div> |
||||
|
|
||||
|
<p class="text-center mb-2 mt-6 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> |
||||
|
</section> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Vue } from "vue-facing-decorator"; |
||||
|
|
||||
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "@/constants/app"; |
||||
|
import QuickNav from "@/components/QuickNav.vue"; |
||||
|
import TopMessage from "@/components/TopMessage.vue"; |
||||
|
import { db } from "@/db/index"; |
||||
|
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings"; |
||||
|
import { createAndSubmitGive } from "@/libs/endorserServer"; |
||||
|
import * as libsUtil from "@/libs/util"; |
||||
|
import { accessToken } from "@/libs/crypto"; |
||||
|
import GiftedDialog from "@/components/GiftedDialog.vue"; |
||||
|
import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue"; |
||||
|
|
||||
|
@Component({ |
||||
|
components: { |
||||
|
GiftedDialog, |
||||
|
GiftedPhotoDialog, |
||||
|
QuickNav, |
||||
|
TopMessage, |
||||
|
}, |
||||
|
}) |
||||
|
export default class GiftedDetails extends Vue { |
||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void; |
||||
|
|
||||
|
activeDid = ""; |
||||
|
apiServer = ""; |
||||
|
|
||||
|
amountInput = "0"; |
||||
|
description = ""; |
||||
|
givenToUser = false; |
||||
|
giverDid: string | undefined; |
||||
|
giverName = ""; |
||||
|
imageUrl = ""; |
||||
|
isTrade = false; |
||||
|
message = ""; |
||||
|
offerId = ""; |
||||
|
projectId = ""; |
||||
|
unitCode = "HUR"; |
||||
|
|
||||
|
libsUtil = libsUtil; |
||||
|
|
||||
|
async mounted() { |
||||
|
this.amountInput = this.$route.query.amountInput as string; |
||||
|
this.description = this.$route.query.description as string; |
||||
|
this.giverDid = this.$route.query.giverDid as string; |
||||
|
this.giverName = this.$route.query.giverName as string; |
||||
|
this.message = this.$route.query.message as string; |
||||
|
this.offerId = this.$route.query.offerId as string; |
||||
|
this.projectId = this.$route.query.projectId as string; |
||||
|
this.unitCode = this.$route.query.unitCode as string; |
||||
|
|
||||
|
this.imageUrl = localStorage.getItem("imageUrl") || ""; |
||||
|
|
||||
|
this.givenToUser = !this.projectId; |
||||
|
|
||||
|
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.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, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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.deleteImage(); // not awaiting, so they'll go back immediately |
||||
|
this.$router.back(); |
||||
|
} |
||||
|
|
||||
|
openPhotoDialog() { |
||||
|
(this.$refs.photoDialog as GiftedPhotoDialog).open((imgUrl) => { |
||||
|
this.imageUrl = imgUrl; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
confirmDeleteImage() { |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "modal", |
||||
|
type: "confirm", |
||||
|
title: "Are you sure you want to delete the image?", |
||||
|
text: "", |
||||
|
onYes: this.deleteImage, |
||||
|
}, |
||||
|
-1, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
async deleteImage() { |
||||
|
if (!this.imageUrl) { |
||||
|
return; |
||||
|
} |
||||
|
try { |
||||
|
const identity = await libsUtil.getIdentity(this.activeDid); |
||||
|
const token = await accessToken(identity); |
||||
|
const response = await this.axios.delete( |
||||
|
DEFAULT_IMAGE_API_SERVER + |
||||
|
"/image/" + |
||||
|
encodeURIComponent(this.imageUrl), |
||||
|
{ |
||||
|
headers: { |
||||
|
Authorization: `Bearer ${token}`, |
||||
|
}, |
||||
|
}, |
||||
|
); |
||||
|
if (response.status === 204) { |
||||
|
// don't bother with a notification |
||||
|
// (either they'll simply continue or they're canceling and going back) |
||||
|
} else { |
||||
|
console.error("Non-success deleting image:", response); |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "There was a problem deleting the image.", |
||||
|
}, |
||||
|
5000, |
||||
|
); |
||||
|
// keep the imageUrl in localStorage so the user can try again if they want |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
localStorage.removeItem("imageUrl"); |
||||
|
this.imageUrl = ""; |
||||
|
} catch (error) { |
||||
|
console.error("Error deleting image:", error); |
||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
||||
|
if ((error as any).response.status === 404) { |
||||
|
console.log("The image was already deleted:", error); |
||||
|
|
||||
|
localStorage.removeItem("imageUrl"); |
||||
|
this.imageUrl = ""; |
||||
|
|
||||
|
// it already doesn't exist so we won't say anything to the user |
||||
|
} else { |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "There was an error deleting the image.", |
||||
|
}, |
||||
|
5000, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async confirm() { |
||||
|
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(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* |
||||
|
* @param giverDid may be null |
||||
|
* @param description may be an empty string |
||||
|
* @param amountInput may be 0 |
||||
|
* @param unitCode may be omitted, defaults to "HUR" |
||||
|
*/ |
||||
|
public async recordGive() { |
||||
|
if (!this.activeDid) { |
||||
|
this.$notify( |
||||
|
{ |
||||
|
group: "alert", |
||||
|
type: "danger", |
||||
|
title: "Error", |
||||
|
text: "You must select an identifier before you can record a give.", |
||||
|
}, |
||||
|
-1, |
||||
|
); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (!this.description && !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] |
||||
|
}.`, |
||||
|
}, |
||||
|
-1, |
||||
|
); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const identity = await libsUtil.getIdentity(this.activeDid); |
||||
|
const result = await createAndSubmitGive( |
||||
|
this.axios, |
||||
|
this.apiServer, |
||||
|
identity, |
||||
|
this.giverDid, |
||||
|
this.givenToUser ? this.activeDid : undefined, |
||||
|
this.description, |
||||
|
parseFloat(this.amountInput), |
||||
|
this.unitCode, |
||||
|
this.projectId, |
||||
|
this.offerId, |
||||
|
this.isTrade, |
||||
|
this.imageUrl, |
||||
|
); |
||||
|
|
||||
|
if ( |
||||
|
result.type === "error" || |
||||
|
this.isGiveCreationError(result.response) |
||||
|
) { |
||||
|
const errorMessage = this.getGiveCreationErrorMessage(result); |
||||
|
console.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.`, |
||||
|
}, |
||||
|
5000, |
||||
|
); |
||||
|
localStorage.removeItem("imageUrl"); |
||||
|
this.$router.back(); |
||||
|
} |
||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
||||
|
} catch (error: any) { |
||||
|
console.error("Error with give recordation caught:", error); |
||||
|
const message = |
||||
|
error.userMessage || |
||||
|
error.response?.data?.error?.message || |
||||
|
"There was an error recording the give."; |
||||
|
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 |
||||
|
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 |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
</script> |
Loading…
Reference in new issue