forked from jsnbuchanan/crowd-funder-for-time-pwa
Merge branch 'build-improvement' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into build-improvement
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
/** * OfferDialog.vue - Dialog component for creating and submitting offers * *
|
||||
Features: * - Offer creation with description and amount * - Unit code selection
|
||||
(HUR, etc.) * - Expiration date handling * - Recipient and project targeting * -
|
||||
Real-time validation and submission * - Comprehensive error handling and user
|
||||
feedback * - Navigation to detailed offer configuration * * @author Matthew
|
||||
Raymer */
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
@@ -10,15 +16,12 @@
|
||||
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()"
|
||||
>
|
||||
<span :class="unitCodeDisplayClasses" @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"
|
||||
v-if="showDecrementButton"
|
||||
:class="controlButtonClasses"
|
||||
@click="decrement()"
|
||||
>
|
||||
<font-awesome icon="chevron-left" />
|
||||
@@ -27,33 +30,15 @@
|
||||
v-model="amountInput"
|
||||
data-testId="inputOfferAmount"
|
||||
type="number"
|
||||
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
||||
:class="amountInputClasses"
|
||||
/>
|
||||
<div
|
||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="increment()"
|
||||
>
|
||||
<div :class="incrementButtonClasses" @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"
|
||||
>
|
||||
<router-link :to="offerDetailsRoute" class="text-blue-500">
|
||||
Conditions & more options...
|
||||
</router-link>
|
||||
</span>
|
||||
@@ -62,18 +47,10 @@
|
||||
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"
|
||||
>
|
||||
<button :class="primaryButtonClasses" @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>
|
||||
<button :class="secondaryButtonClasses" @click="cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,19 +62,35 @@ import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { createAndSubmitOffer } from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import {
|
||||
NOTIFY_OFFER_SETTINGS_ERROR,
|
||||
NOTIFY_OFFER_RECORDING,
|
||||
NOTIFY_OFFER_IDENTITY_REQUIRED,
|
||||
NOTIFY_OFFER_DESCRIPTION_REQUIRED,
|
||||
NOTIFY_OFFER_CREATION_ERROR,
|
||||
NOTIFY_OFFER_SUCCESS,
|
||||
NOTIFY_OFFER_SUBMISSION_ERROR,
|
||||
} from "@/constants/notifications";
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class OfferDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
@Prop projectId?: string;
|
||||
@Prop projectName?: string;
|
||||
|
||||
// Vue notification system
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
// Notification system
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
// Component state
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
|
||||
amountInput = "0";
|
||||
amountUnitCode = "HUR";
|
||||
description = "";
|
||||
@@ -108,47 +101,150 @@ export default class OfferDialog extends Vue {
|
||||
|
||||
libsUtil = libsUtil;
|
||||
|
||||
// =================================================
|
||||
// COMPUTED PROPERTIES - Template Logic Streamlining
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* CSS classes for the primary action button (Sign & Send)
|
||||
* Reduces template complexity for gradient button styling
|
||||
*/
|
||||
get primaryButtonClasses(): string {
|
||||
return "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";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the secondary action button (Cancel)
|
||||
* Reduces template complexity for gradient button styling
|
||||
*/
|
||||
get secondaryButtonClasses(): string {
|
||||
return "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";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for unit code selector and increment/decrement buttons
|
||||
* Reduces template complexity for repeated border and styling patterns
|
||||
*/
|
||||
get controlButtonClasses(): string {
|
||||
return "border border-r-0 border-slate-400 bg-slate-200 px-4 py-2";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for unit code display span
|
||||
* Reduces template complexity for unit code button styling
|
||||
*/
|
||||
get unitCodeDisplayClasses(): string {
|
||||
return "rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for amount input field
|
||||
* Reduces template complexity for input styling
|
||||
*/
|
||||
get amountInputClasses(): string {
|
||||
return "w-full border border-r-0 border-slate-400 px-2 py-2 text-center";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the right-most increment button
|
||||
* Reduces template complexity for border styling
|
||||
*/
|
||||
get incrementButtonClasses(): string {
|
||||
return "rounded-r border border-slate-400 bg-slate-200 px-4 py-2";
|
||||
}
|
||||
|
||||
/**
|
||||
* Router configuration object for offer details navigation
|
||||
* Consolidates complex query parameter object from template
|
||||
*/
|
||||
get offerDetailsRoute(): object {
|
||||
return {
|
||||
name: "offer-details",
|
||||
query: {
|
||||
amountInput: this.amountInput,
|
||||
description: this.description,
|
||||
offererDid: this.activeDid,
|
||||
projectId: this.projectId,
|
||||
projectName: this.projectName,
|
||||
recipientDid: this.recipientDid,
|
||||
recipientName: this.recipientName,
|
||||
unitCode: this.amountUnitCode,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the decrement button should be visible
|
||||
* Encapsulates conditional logic from template
|
||||
*/
|
||||
get showDecrementButton(): boolean {
|
||||
return this.amountInput !== "0";
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// COMPONENT METHODS
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* Vue lifecycle hook - Initialize notification helpers
|
||||
*/
|
||||
mounted() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the dialog and load account settings
|
||||
* @param recipientDid - Optional recipient DID
|
||||
* @param recipientName - Optional recipient name
|
||||
*/
|
||||
async open(recipientDid?: string, recipientName?: string) {
|
||||
try {
|
||||
this.recipientDid = recipientDid;
|
||||
this.recipientName = recipientName;
|
||||
|
||||
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
const settings = await this.$accountSettings();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
// 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.notify.error(
|
||||
err.message || NOTIFY_OFFER_SETTINGS_ERROR.message,
|
||||
TIMEOUTS.MODAL,
|
||||
);
|
||||
}
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the dialog without changing values
|
||||
*/
|
||||
close() {
|
||||
// close the dialog but don't change values (since it might be submitting info)
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycle through available unit codes
|
||||
*/
|
||||
changeUnitCode() {
|
||||
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||
const index = units.indexOf(this.amountUnitCode);
|
||||
this.amountUnitCode = units[(index + 1) % units.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the amount input
|
||||
*/
|
||||
increment() {
|
||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the amount input
|
||||
*/
|
||||
decrement() {
|
||||
this.amountInput = `${Math.max(
|
||||
0,
|
||||
@@ -156,28 +252,30 @@ export default class OfferDialog extends Vue {
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the dialog and clear values
|
||||
*/
|
||||
cancel() {
|
||||
this.close();
|
||||
this.eraseValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear form values
|
||||
*/
|
||||
eraseValues() {
|
||||
this.description = "";
|
||||
this.amountInput = "0";
|
||||
this.amountUnitCode = "HUR";
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm and submit the offer
|
||||
*/
|
||||
async confirm() {
|
||||
this.close();
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
text: "Recording the offer...",
|
||||
title: "",
|
||||
},
|
||||
1000,
|
||||
);
|
||||
this.notify.toast(NOTIFY_OFFER_RECORDING.text, undefined, TIMEOUTS.BRIEF);
|
||||
|
||||
// this is asynchronous, but we don't need to wait for it to complete
|
||||
this.recordOffer(
|
||||
this.description,
|
||||
@@ -191,10 +289,11 @@ export default class OfferDialog extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
* @param unitCode may be omitted, defaults to "HUR"
|
||||
* Record an offer with the given parameters
|
||||
* @param description - Offer description (may be empty)
|
||||
* @param amount - Offer amount (may be 0)
|
||||
* @param unitCode - Unit code (defaults to "HUR")
|
||||
* @param expirationDateInput - Optional expiration date
|
||||
*/
|
||||
public async recordOffer(
|
||||
description: string,
|
||||
@@ -203,28 +302,16 @@ export default class OfferDialog extends Vue {
|
||||
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,
|
||||
);
|
||||
this.notify.error(NOTIFY_OFFER_IDENTITY_REQUIRED.message, TIMEOUTS.LONG);
|
||||
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,
|
||||
const message = NOTIFY_OFFER_DESCRIPTION_REQUIRED.message.replace(
|
||||
"{unit}",
|
||||
this.libsUtil.UNIT_LONG[unitCode],
|
||||
);
|
||||
this.notify.error(message, TIMEOUTS.MODAL);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -245,25 +332,12 @@ export default class OfferDialog extends Vue {
|
||||
if (!result.success) {
|
||||
const errorMessage = result.error;
|
||||
logger.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,
|
||||
this.notify.error(
|
||||
errorMessage || NOTIFY_OFFER_CREATION_ERROR.message,
|
||||
TIMEOUTS.MODAL,
|
||||
);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
text: "That offer was recorded.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.notify.success(NOTIFY_OFFER_SUCCESS.message, TIMEOUTS.VERY_LONG);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
@@ -271,41 +345,34 @@ export default class OfferDialog extends Vue {
|
||||
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,
|
||||
);
|
||||
NOTIFY_OFFER_SUBMISSION_ERROR.message;
|
||||
this.notify.error(message, TIMEOUTS.MODAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.dialog-overlay {
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,20 +13,14 @@ PhotoDialog.vue */
|
||||
<div v-if="visible" class="dialog-overlay z-[60]">
|
||||
<div class="dialog relative">
|
||||
<div class="text-lg text-center font-light relative z-50">
|
||||
<div
|
||||
id="ViewHeading"
|
||||
class="text-center font-bold absolute top-0 inset-x-0 px-4 py-2 bg-black/50 text-white leading-none pointer-events-none"
|
||||
>
|
||||
<div id="ViewHeading" :class="headingClasses">
|
||||
<span v-if="uploading"> Uploading... </span>
|
||||
<span v-else-if="blob"> Look Good? </span>
|
||||
<span v-else-if="showCameraPreview"> Take Photo </span>
|
||||
<span v-else> Say "Cheese"! </span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-lg text-center px-2 py-2 leading-none absolute right-0 top-0 text-white cursor-pointer"
|
||||
@click="close()"
|
||||
>
|
||||
<div :class="closeButtonClasses" @click="close()">
|
||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,37 +34,24 @@ PhotoDialog.vue */
|
||||
<div v-else-if="blob">
|
||||
<div v-if="crop">
|
||||
<VuePictureCropper
|
||||
:box-style="{
|
||||
backgroundColor: '#f8f8f8',
|
||||
margin: 'auto',
|
||||
}"
|
||||
:img="createBlobURL(blob)"
|
||||
:options="{
|
||||
viewMode: 1,
|
||||
dragMode: 'crop',
|
||||
aspectRatio: 1 / 1,
|
||||
}"
|
||||
:box-style="cropperBoxStyle"
|
||||
:img="blobUrl"
|
||||
:options="cropperOptions"
|
||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="flex justify-center">
|
||||
<img
|
||||
:src="createBlobURL(blob)"
|
||||
class="mt-2 rounded max-h-[90vh] max-w-[90vw] object-contain"
|
||||
/>
|
||||
<img :src="blobUrl" :class="imageDisplayClasses" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2 mt-2">
|
||||
<button
|
||||
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 py-2 px-3 rounded-md"
|
||||
@click="uploadImage"
|
||||
>
|
||||
<button :class="primaryButtonClasses" @click="uploadImage">
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showRetry"
|
||||
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md"
|
||||
:class="secondaryButtonClasses"
|
||||
@click="retryImage"
|
||||
>
|
||||
<span>Retry</span>
|
||||
@@ -86,10 +67,7 @@ PhotoDialog.vue */
|
||||
playsinline
|
||||
muted
|
||||
></video>
|
||||
<button
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||
@click="capturePhoto"
|
||||
>
|
||||
<button :class="cameraButtonClasses" @click="capturePhoto">
|
||||
<font-awesome icon="camera" class="w-[1em]" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -98,15 +76,12 @@ PhotoDialog.vue */
|
||||
<div class="flex flex-col items-center justify-center gap-4 p-4">
|
||||
<button
|
||||
v-if="isRegistered"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
:class="actionButtonClasses"
|
||||
@click="startCameraPreview"
|
||||
>
|
||||
<font-awesome icon="camera" class="w-[1em]" />
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="pickPhoto"
|
||||
>
|
||||
<button :class="actionButtonClasses" @click="pickPhoto">
|
||||
<font-awesome icon="image" class="w-[1em]" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -120,15 +95,29 @@ import axios from "axios";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import {
|
||||
NOTIFY_PHOTO_SETTINGS_ERROR,
|
||||
NOTIFY_PHOTO_CAPTURE_ERROR,
|
||||
NOTIFY_PHOTO_CAMERA_ERROR,
|
||||
NOTIFY_PHOTO_UPLOAD_ERROR,
|
||||
NOTIFY_PHOTO_UNSUPPORTED_FORMAT,
|
||||
NOTIFY_PHOTO_SIZE_ERROR,
|
||||
NOTIFY_PHOTO_PROCESSING_ERROR,
|
||||
} from "@/constants/notifications";
|
||||
|
||||
@Component({ components: { VuePictureCropper } })
|
||||
@Component({
|
||||
components: { VuePictureCropper },
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class PhotoDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||
|
||||
/** Active DID for user authentication */
|
||||
activeDid = "";
|
||||
|
||||
@@ -162,36 +151,133 @@ export default class PhotoDialog extends Vue {
|
||||
/** Camera stream reference */
|
||||
private cameraStream: MediaStream | null = null;
|
||||
|
||||
private platformService = PlatformServiceFactory.getInstance();
|
||||
URL = window.URL || window.webkitURL;
|
||||
|
||||
isRegistered = false;
|
||||
private platformCapabilities = this.platformService.getCapabilities();
|
||||
|
||||
// =================================================
|
||||
// COMPUTED PROPERTIES - Template Logic Streamlining
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* CSS classes for the dialog heading section
|
||||
* Reduces template complexity for absolute positioning and styling
|
||||
*/
|
||||
get headingClasses(): string {
|
||||
return "text-center font-bold absolute top-0 inset-x-0 px-4 py-2 bg-black/50 text-white leading-none pointer-events-none";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the close button
|
||||
* Reduces template complexity for absolute positioning and styling
|
||||
*/
|
||||
get closeButtonClasses(): string {
|
||||
return "text-lg text-center px-2 py-2 leading-none absolute right-0 top-0 text-white cursor-pointer";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the primary action button (Upload)
|
||||
* Reduces template complexity for gradient button styling
|
||||
*/
|
||||
get primaryButtonClasses(): string {
|
||||
return "bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the secondary action button (Retry)
|
||||
* Reduces template complexity for gradient button styling
|
||||
*/
|
||||
get secondaryButtonClasses(): string {
|
||||
return "bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-2 px-3 rounded-md";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the camera capture button
|
||||
* Reduces template complexity for absolute positioning and circular styling
|
||||
*/
|
||||
get cameraButtonClasses(): string {
|
||||
return "absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for action buttons (camera/image selection)
|
||||
* Reduces template complexity for button styling
|
||||
*/
|
||||
get actionButtonClasses(): string {
|
||||
return "bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for image display
|
||||
* Reduces template complexity for image styling
|
||||
*/
|
||||
get imageDisplayClasses(): string {
|
||||
return "mt-2 rounded max-h-[90vh] max-w-[90vw] object-contain";
|
||||
}
|
||||
|
||||
/**
|
||||
* Picture cropper box style configuration
|
||||
* Consolidates complex configuration object from template
|
||||
*/
|
||||
get cropperBoxStyle(): object {
|
||||
return {
|
||||
backgroundColor: "#f8f8f8",
|
||||
margin: "auto",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Picture cropper options configuration
|
||||
* Consolidates complex configuration object from template
|
||||
*/
|
||||
get cropperOptions(): object {
|
||||
return {
|
||||
viewMode: 1,
|
||||
dragMode: "crop",
|
||||
aspectRatio: 1 / 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Blob URL for displaying images
|
||||
* Encapsulates blob URL creation logic
|
||||
*/
|
||||
get blobUrl(): string {
|
||||
return this.blob ? this.createBlobURL(this.blob) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform capabilities accessor
|
||||
* Provides cached access to platform capabilities
|
||||
*/
|
||||
get platformCapabilities() {
|
||||
return this.$platformService.getCapabilities();
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// COMPONENT METHODS
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* Lifecycle hook: Initializes component and retrieves user settings
|
||||
* @throws {Error} When settings retrieval fails
|
||||
*/
|
||||
async mounted() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
|
||||
// logger.log("PhotoDialog mounted");
|
||||
try {
|
||||
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
const settings = await this.$accountSettings();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
logger.log("isRegistered:", this.isRegistered);
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error retrieving settings from database:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "There was an error retrieving your settings.",
|
||||
},
|
||||
-1,
|
||||
this.notify.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: NOTIFY_PHOTO_SETTINGS_ERROR.message,
|
||||
TIMEOUTS.MODAL,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -275,14 +361,9 @@ export default class PhotoDialog extends Vue {
|
||||
this.fileName = result.fileName;
|
||||
} catch (error) {
|
||||
logger.error("Error taking picture:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to take picture. Please try again.",
|
||||
},
|
||||
5000,
|
||||
this.notify.error(
|
||||
NOTIFY_PHOTO_CAPTURE_ERROR.message,
|
||||
TIMEOUTS.STANDARD,
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -335,15 +416,7 @@ export default class PhotoDialog extends Vue {
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error starting camera preview:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to access camera. Please try again.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.notify.error(NOTIFY_PHOTO_CAMERA_ERROR.message, TIMEOUTS.STANDARD);
|
||||
this.showCameraPreview = false;
|
||||
}
|
||||
}
|
||||
@@ -394,15 +467,7 @@ export default class PhotoDialog extends Vue {
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error capturing photo:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to capture photo. Please try again.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.notify.error(NOTIFY_PHOTO_CAPTURE_ERROR.message, TIMEOUTS.STANDARD);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,15 +482,7 @@ export default class PhotoDialog extends Vue {
|
||||
this.fileName = result.fileName;
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error taking picture:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to take picture. Please try again.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.notify.error(NOTIFY_PHOTO_CAPTURE_ERROR.message, TIMEOUTS.STANDARD);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,14 +497,9 @@ export default class PhotoDialog extends Vue {
|
||||
this.fileName = result.fileName;
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error picking image:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to pick image. Please try again.",
|
||||
},
|
||||
5000,
|
||||
this.notify.error(
|
||||
NOTIFY_PHOTO_PROCESSING_ERROR.message,
|
||||
TIMEOUTS.STANDARD,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -489,14 +541,9 @@ export default class PhotoDialog extends Vue {
|
||||
};
|
||||
const formData = new FormData();
|
||||
if (!this.blob) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error finding the picture. Please try again.",
|
||||
},
|
||||
5000,
|
||||
this.notify.error(
|
||||
NOTIFY_PHOTO_PROCESSING_ERROR.message,
|
||||
TIMEOUTS.STANDARD,
|
||||
);
|
||||
this.uploading = false;
|
||||
return;
|
||||
@@ -525,7 +572,7 @@ export default class PhotoDialog extends Vue {
|
||||
// Log the raw error first
|
||||
logger.error("Raw error object:", JSON.stringify(error, null, 2));
|
||||
|
||||
let errorMessage = "There was an error saving the picture.";
|
||||
let errorMessage = NOTIFY_PHOTO_UPLOAD_ERROR.message;
|
||||
|
||||
if (axios.isAxiosError(error)) {
|
||||
const status = error.response?.status;
|
||||
@@ -548,10 +595,9 @@ export default class PhotoDialog extends Vue {
|
||||
if (status === 401) {
|
||||
errorMessage = "Authentication failed. Please try logging in again.";
|
||||
} else if (status === 413) {
|
||||
errorMessage = "Image file is too large. Please try a smaller image.";
|
||||
errorMessage = NOTIFY_PHOTO_SIZE_ERROR.message;
|
||||
} else if (status === 415) {
|
||||
errorMessage =
|
||||
"Unsupported image format. Please try a different image.";
|
||||
errorMessage = NOTIFY_PHOTO_UNSUPPORTED_FORMAT.message;
|
||||
} else if (status && status >= 500) {
|
||||
errorMessage = "Server error. Please try again later.";
|
||||
} else if (data?.message) {
|
||||
@@ -573,15 +619,7 @@ export default class PhotoDialog extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: errorMessage,
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.notify.error(errorMessage, TIMEOUTS.STANDARD);
|
||||
this.uploading = false;
|
||||
this.blob = undefined;
|
||||
}
|
||||
|
||||
@@ -196,3 +196,80 @@ export const NOTIFY_CAMERA_SHARE_METHOD = {
|
||||
yesText: "we are nearby with cameras",
|
||||
noText: "we will share another way",
|
||||
};
|
||||
|
||||
// OfferDialog.vue constants
|
||||
export const NOTIFY_OFFER_SETTINGS_ERROR = {
|
||||
title: "Error",
|
||||
message: "There was an error retrieving your settings.",
|
||||
};
|
||||
|
||||
export const NOTIFY_OFFER_RECORDING = {
|
||||
text: "Recording the offer...",
|
||||
title: "",
|
||||
};
|
||||
|
||||
export const NOTIFY_OFFER_IDENTITY_REQUIRED = {
|
||||
title: "Error",
|
||||
message: "You must select an identity before you can record an offer.",
|
||||
};
|
||||
|
||||
export const NOTIFY_OFFER_DESCRIPTION_REQUIRED = {
|
||||
title: "Error",
|
||||
message: "You must enter a description or some number of {unit}.",
|
||||
};
|
||||
|
||||
export const NOTIFY_OFFER_CREATION_ERROR = {
|
||||
title: "Error",
|
||||
message: "There was an error creating the offer.",
|
||||
};
|
||||
|
||||
export const NOTIFY_OFFER_SUCCESS = {
|
||||
title: "Success",
|
||||
message: "That offer was recorded.",
|
||||
};
|
||||
|
||||
export const NOTIFY_OFFER_SUBMISSION_ERROR = {
|
||||
title: "Error",
|
||||
message: "There was an error recording the offer.",
|
||||
};
|
||||
|
||||
// PhotoDialog.vue constants
|
||||
export const NOTIFY_PHOTO_SETTINGS_ERROR = {
|
||||
title: "Error",
|
||||
message: "There was an error retrieving your settings.",
|
||||
};
|
||||
|
||||
export const NOTIFY_PHOTO_CAPTURE_ERROR = {
|
||||
title: "Error",
|
||||
message: "Failed to take picture. Please try again.",
|
||||
};
|
||||
|
||||
export const NOTIFY_PHOTO_CAMERA_ERROR = {
|
||||
title: "Camera Error",
|
||||
message: "Could not access camera. Please check permissions and try again.",
|
||||
};
|
||||
|
||||
export const NOTIFY_PHOTO_UPLOAD_ERROR = {
|
||||
title: "Upload Error",
|
||||
message: "Failed to upload image. Please try again.",
|
||||
};
|
||||
|
||||
export const NOTIFY_PHOTO_UPLOAD_SUCCESS = {
|
||||
title: "Success",
|
||||
message: "Image uploaded successfully.",
|
||||
};
|
||||
|
||||
export const NOTIFY_PHOTO_UNSUPPORTED_FORMAT = {
|
||||
title: "Unsupported Format",
|
||||
message: "This file format is not supported. Please try a different image.",
|
||||
};
|
||||
|
||||
export const NOTIFY_PHOTO_SIZE_ERROR = {
|
||||
title: "File Too Large",
|
||||
message: "Image file is too large. Please choose a smaller image.",
|
||||
};
|
||||
|
||||
export const NOTIFY_PHOTO_PROCESSING_ERROR = {
|
||||
title: "Processing Error",
|
||||
message: "Failed to process image. Please try again.",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user