|
|
|
<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">
|
|
|
|
<span>From {{ giverName || "somebody not named" }}</span>
|
|
|
|
<span> to {{ recipientName || "somebody not named" }}</span>
|
|
|
|
</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-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-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 {{ projectName }}</label>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="showGivenToUser" 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
|
|
|
|
<fa
|
|
|
|
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 & 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>
|
|
|
|
</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, getPlanFromCache } from "@/libs/endorserServer";
|
|
|
|
import * as libsUtil from "@/libs/util";
|
|
|
|
import { accessToken } from "@/libs/crypto";
|
|
|
|
import GiftedPhotoDialog from "@/components/GiftedPhotoDialog.vue";
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
components: {
|
|
|
|
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 = "";
|
|
|
|
projectName = "a project";
|
|
|
|
recipientDid = "";
|
|
|
|
recipientName = "";
|
|
|
|
showGivenToUser = false;
|
|
|
|
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;
|
|
|
|
if (this.giverDid && !this.giverName) {
|
|
|
|
this.giverName =
|
|
|
|
this.giverDid === this.activeDid ? "you" : "someone not named";
|
|
|
|
}
|
|
|
|
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.recipientDid = this.$route.query.recipientDid as string;
|
|
|
|
this.recipientName = this.$route.query.recipientName as string;
|
|
|
|
if (this.recipientDid && !this.recipientName) {
|
|
|
|
this.recipientName =
|
|
|
|
this.recipientDid === this.activeDid ? "you" : "someone not named";
|
|
|
|
}
|
|
|
|
this.unitCode = this.$route.query.unitCode as string;
|
|
|
|
|
|
|
|
this.imageUrl = localStorage.getItem("imageUrl") || "";
|
|
|
|
|
|
|
|
try {
|
|
|
|
await db.open();
|
|
|
|
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
|
|
|
this.apiServer = settings?.apiServer || "";
|
|
|
|
this.activeDid = settings?.activeDid || "";
|
|
|
|
|
|
|
|
this.givenToUser = this.recipientDid === this.activeDid;
|
|
|
|
this.showGivenToUser =
|
|
|
|
!this.projectId && this.recipientDid === this.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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.projectId) {
|
|
|
|
console.log("Getting project name from cache", this.projectId);
|
|
|
|
const identity = await libsUtil.getIdentity(this.activeDid);
|
|
|
|
const project = await getPlanFromCache(
|
|
|
|
this.projectId,
|
|
|
|
identity,
|
|
|
|
this.axios,
|
|
|
|
this.apiServer,
|
|
|
|
);
|
|
|
|
console.log("Got project name from cache", project);
|
|
|
|
this.projectName = project?.name
|
|
|
|
? "the project " + project.name
|
|
|
|
: "a project";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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() {
|
|
|
|
if (!this.activeDid) {
|
|
|
|
this.$notify(
|
|
|
|
{
|
|
|
|
group: "alert",
|
|
|
|
type: "danger",
|
|
|
|
title: "Error",
|
|
|
|
text: "You must select an identifier before you can record a give.",
|
|
|
|
},
|
|
|
|
2000,
|
|
|
|
);
|
|
|
|
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.$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() {
|
|
|
|
try {
|
|
|
|
const identity = await libsUtil.getIdentity(this.activeDid);
|
|
|
|
const recipientDid =
|
|
|
|
this.recipientDid === this.activeDid
|
|
|
|
? this.givenToUser
|
|
|
|
? this.activeDid
|
|
|
|
: undefined
|
|
|
|
: this.recipientDid;
|
|
|
|
const result = await createAndSubmitGive(
|
|
|
|
this.axios,
|
|
|
|
this.apiServer,
|
|
|
|
identity,
|
|
|
|
this.giverDid,
|
|
|
|
recipientDid,
|
|
|
|
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 errorMessage =
|
|
|
|
error.userMessage ||
|
|
|
|
error.response?.data?.error?.message ||
|
|
|
|
"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>
|