forked from trent_larson/crowd-funder-for-time-pwa
Merge branch 'master' into seed-phrase-backup-prompt
This commit is contained in:
@@ -1313,6 +1313,28 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
|
||||
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats type string for display by adding spaces before capitals
|
||||
* and optionally adds an appropriate article prefix (a/an)
|
||||
*
|
||||
* @param text - Text to format
|
||||
* @returns Formatted string with article prefix
|
||||
*/
|
||||
export const capitalizeAndInsertSpacesBeforeCapsWithAPrefix = (
|
||||
text: string,
|
||||
): string => {
|
||||
const word = capitalizeAndInsertSpacesBeforeCaps(text);
|
||||
if (word) {
|
||||
// if the word starts with a vowel, use "an" instead of "a"
|
||||
const firstLetter = word[0].toLowerCase();
|
||||
const vowels = ["a", "e", "i", "o", "u"];
|
||||
const particle = vowels.includes(firstLetter) ? "an" : "a";
|
||||
return particle + " " + word;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
return readable summary of claim, or something generic
|
||||
|
||||
|
||||
@@ -160,6 +160,41 @@ export const isGiveAction = (
|
||||
return isGiveClaimType(veriClaim.claimType);
|
||||
};
|
||||
|
||||
export interface OfferFulfillment {
|
||||
offerHandleId: string;
|
||||
offerType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract offer fulfillment information from the fulfills field
|
||||
* Handles both array and single object cases
|
||||
*/
|
||||
export const extractOfferFulfillment = (fulfills: any): OfferFulfillment | null => {
|
||||
if (!fulfills) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle both array and single object cases
|
||||
let offerFulfill = null;
|
||||
|
||||
if (Array.isArray(fulfills)) {
|
||||
// Find the Offer in the fulfills array
|
||||
offerFulfill = fulfills.find((item) => item["@type"] === "Offer");
|
||||
} else if (fulfills["@type"] === "Offer") {
|
||||
// fulfills is a single Offer object
|
||||
offerFulfill = fulfills;
|
||||
}
|
||||
|
||||
if (offerFulfill) {
|
||||
return {
|
||||
offerHandleId: offerFulfill.identifier,
|
||||
offerType: offerFulfill["@type"],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const shortDid = (did: string) => {
|
||||
if (did.startsWith("did:peer:")) {
|
||||
return (
|
||||
|
||||
@@ -36,7 +36,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
// Generate a short random ID for this scanner instance
|
||||
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
this.options = options ?? {};
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
|
||||
{
|
||||
...this.options,
|
||||
@@ -49,7 +49,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
this.context = this.canvas.getContext("2d", { willReadFrequently: true });
|
||||
this.video = document.createElement("video");
|
||||
this.video.setAttribute("playsinline", "true"); // Required for iOS
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] DOM elements created successfully`,
|
||||
);
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
this.cameraStateListeners.forEach((listener) => {
|
||||
try {
|
||||
listener.onStateChange(state, message);
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
|
||||
{
|
||||
state,
|
||||
@@ -89,7 +89,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
async checkPermissions(): Promise<boolean> {
|
||||
try {
|
||||
this.updateCameraState("initializing", "Checking camera permissions...");
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
||||
);
|
||||
|
||||
@@ -99,7 +99,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
const permissions = await navigator.permissions.query({
|
||||
name: "camera" as PermissionName,
|
||||
});
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
|
||||
permissions.state,
|
||||
);
|
||||
@@ -165,7 +165,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
"initializing",
|
||||
"Requesting camera permissions...",
|
||||
);
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
||||
);
|
||||
|
||||
@@ -175,7 +175,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
(device) => device.kind === "videoinput",
|
||||
);
|
||||
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
||||
count: videoDevices.length,
|
||||
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
|
||||
userAgent: navigator.userAgent,
|
||||
@@ -188,7 +188,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
}
|
||||
|
||||
// Try to get a stream with specific constraints
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`,
|
||||
{
|
||||
facingMode: "environment",
|
||||
@@ -210,7 +210,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
// Stop the test stream immediately
|
||||
stream.getTracks().forEach((track) => {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
||||
kind: track.kind,
|
||||
label: track.label,
|
||||
readyState: track.readyState,
|
||||
@@ -275,12 +275,12 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
async isSupported(): Promise<boolean> {
|
||||
try {
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Checking browser support...`,
|
||||
);
|
||||
// Check for secure context first
|
||||
if (!window.isSecureContext) {
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`,
|
||||
);
|
||||
return false;
|
||||
@@ -300,7 +300,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
(device) => device.kind === "videoinput",
|
||||
);
|
||||
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Device support check:`, {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Device support check:`, {
|
||||
hasSecureContext: window.isSecureContext,
|
||||
hasMediaDevices: !!navigator.mediaDevices,
|
||||
hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia,
|
||||
@@ -379,7 +379,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
// Log scan attempt every 100 frames or 1 second
|
||||
if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
|
||||
attempt: this.scanAttempts,
|
||||
dimensions: {
|
||||
width: this.canvas.width,
|
||||
@@ -421,7 +421,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
!code.data ||
|
||||
code.data.length === 0;
|
||||
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
|
||||
data: code.data,
|
||||
location: code.location,
|
||||
attempts: this.scanAttempts,
|
||||
@@ -512,13 +512,13 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
this.scanAttempts = 0;
|
||||
this.lastScanTime = Date.now();
|
||||
this.updateCameraState("initializing", "Starting camera...");
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Starting scan with options:`,
|
||||
this.options,
|
||||
);
|
||||
|
||||
// Get camera stream with options
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
|
||||
);
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||
@@ -531,7 +531,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
this.updateCameraState("active", "Camera is active");
|
||||
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
||||
tracks: this.stream.getTracks().map((t) => ({
|
||||
kind: t.kind,
|
||||
label: t.label,
|
||||
@@ -550,14 +550,14 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
this.video.style.display = "none";
|
||||
}
|
||||
await this.video.play();
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Video element started playing`,
|
||||
);
|
||||
}
|
||||
|
||||
// Emit stream to component
|
||||
this.events.emit("stream", this.stream);
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
|
||||
|
||||
// Start QR code scanning
|
||||
this.scanQRCode();
|
||||
@@ -595,7 +595,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
}
|
||||
|
||||
try {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
|
||||
scanAttempts: this.scanAttempts,
|
||||
duration: Date.now() - this.lastScanTime,
|
||||
});
|
||||
@@ -604,7 +604,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Animation frame cancelled`,
|
||||
);
|
||||
}
|
||||
@@ -613,13 +613,13 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
if (this.video) {
|
||||
this.video.pause();
|
||||
this.video.srcObject = null;
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Video element stopped`);
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Video element stopped`);
|
||||
}
|
||||
|
||||
// Stop all tracks in the stream
|
||||
if (this.stream) {
|
||||
this.stream.getTracks().forEach((track) => {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
|
||||
kind: track.kind,
|
||||
label: track.label,
|
||||
readyState: track.readyState,
|
||||
@@ -631,7 +631,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
// Emit stream stopped event
|
||||
this.events.emit("stream", null);
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -643,17 +643,17 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
throw error;
|
||||
} finally {
|
||||
this.isScanning = false;
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
||||
}
|
||||
}
|
||||
|
||||
addListener(listener: ScanListener): void {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
|
||||
this.scanListener = listener;
|
||||
}
|
||||
|
||||
onStream(callback: (stream: MediaStream | null) => void): void {
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Adding stream event listener`,
|
||||
);
|
||||
this.events.on("stream", callback);
|
||||
@@ -661,24 +661,24 @@ export class WebInlineQRScanner implements QRScannerService {
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
try {
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
|
||||
await this.stopScan();
|
||||
this.events.removeAllListeners();
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
|
||||
|
||||
// Clean up DOM elements
|
||||
if (this.video) {
|
||||
this.video.remove();
|
||||
this.video = null;
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Video element removed`);
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Video element removed`);
|
||||
}
|
||||
if (this.canvas) {
|
||||
this.canvas.remove();
|
||||
this.canvas = null;
|
||||
logger.error(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
|
||||
logger.debug(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
|
||||
}
|
||||
this.context = null;
|
||||
logger.error(
|
||||
logger.debug(
|
||||
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
<div class="flex columns-3">
|
||||
<h2 class="text-md font-bold w-full">
|
||||
{{
|
||||
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
|
||||
serverUtil.capitalizeAndInsertSpacesBeforeCaps(
|
||||
veriClaim.claimType || "",
|
||||
)
|
||||
}}
|
||||
<button
|
||||
v-if="canEditClaim"
|
||||
@@ -106,77 +108,91 @@
|
||||
</div>
|
||||
|
||||
<!-- Fullfills Links -->
|
||||
|
||||
<!-- fullfills links for a give -->
|
||||
<div v-if="detailsForGive?.fulfillsPlanHandleId" class="mt-4">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
|
||||
"
|
||||
class="text-blue-500 mt-2"
|
||||
>
|
||||
Fulfills a bigger plan...
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- if there's another, it's probably fulfilling an offer, too -->
|
||||
<div
|
||||
v-if="
|
||||
detailsForGive?.fulfillsType &&
|
||||
detailsForGive?.fulfillsType !== 'PlanAction' &&
|
||||
detailsForGive?.fulfillsHandleId
|
||||
"
|
||||
>
|
||||
<!-- router-link to /claim/ only changes URL path -->
|
||||
<a
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
@click="
|
||||
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
|
||||
"
|
||||
>
|
||||
Fulfills
|
||||
{{
|
||||
capitalizeAndInsertSpacesBeforeCaps(
|
||||
detailsForGive.fulfillsType,
|
||||
)
|
||||
}}...
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- fullfills links for an offer -->
|
||||
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
|
||||
"
|
||||
class="text-blue-500 mt-4"
|
||||
>
|
||||
Offered to a bigger plan...
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Providers -->
|
||||
<div v-if="providersForGive?.length > 0" class="mt-4">
|
||||
<span>Other assistance provided by:</span>
|
||||
<ul class="ml-4">
|
||||
<li
|
||||
v-for="provider of providersForGive"
|
||||
:key="provider.identifier"
|
||||
class="list-disc ml-4"
|
||||
<div class="mt-4 empty:hidden">
|
||||
<!-- fullfills links for a give -->
|
||||
<div v-if="detailsForGive?.fulfillsPlanHandleId">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
|
||||
"
|
||||
class="text-blue-500 mt-2"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="grow overflow-hidden">
|
||||
<a
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
@click="handleProviderClick(provider)"
|
||||
>
|
||||
an activity...
|
||||
</a>
|
||||
This fulfills a bigger plan
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Show offer fulfillment if this give fulfills an offer -->
|
||||
<div v-if="detailsForGiveOfferFulfillment?.offerHandleId">
|
||||
<!-- router-link to /claim/ only changes URL path -->
|
||||
<a
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
@click="
|
||||
showDifferentClaimPage(
|
||||
detailsForGiveOfferFulfillment.offerHandleId,
|
||||
)
|
||||
"
|
||||
>
|
||||
This fulfills
|
||||
{{
|
||||
serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
||||
detailsForGiveOfferFulfillment.offerType || "Offer",
|
||||
)
|
||||
}}
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- fullfills links for an offer -->
|
||||
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
|
||||
"
|
||||
class="text-blue-500 mt-4"
|
||||
>
|
||||
Offered to a bigger plan
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Providers -->
|
||||
<div v-if="providersForGive?.length > 0">
|
||||
<span>Other assistance provided by:</span>
|
||||
<ul class="ml-4">
|
||||
<li
|
||||
v-for="provider of providersForGive"
|
||||
:key="provider.identifier"
|
||||
class="list-disc ml-4"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="grow overflow-hidden">
|
||||
<a
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
@click="handleProviderClick(provider)"
|
||||
>
|
||||
an activity
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -556,6 +572,17 @@ export default class ClaimView extends Vue {
|
||||
fulfillsPlanHandleId?: string;
|
||||
fulfillsType?: string;
|
||||
fulfillsHandleId?: string;
|
||||
fullClaim?: {
|
||||
fulfills?: Array<{
|
||||
"@type": string;
|
||||
identifier?: string;
|
||||
}>;
|
||||
};
|
||||
} | null = null;
|
||||
// Additional offer information extracted from the fulfills array
|
||||
detailsForGiveOfferFulfillment: {
|
||||
offerHandleId?: string;
|
||||
offerType?: string;
|
||||
} | null = null;
|
||||
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
|
||||
// Project information for fulfillsPlanHandleId
|
||||
@@ -689,6 +716,7 @@ export default class ClaimView extends Vue {
|
||||
this.confsVisibleToIdList = [];
|
||||
this.detailsForGive = null;
|
||||
this.detailsForOffer = null;
|
||||
this.detailsForGiveOfferFulfillment = null;
|
||||
this.projectInfo = null;
|
||||
this.fullClaim = null;
|
||||
this.fullClaimDump = "";
|
||||
@@ -701,6 +729,15 @@ export default class ClaimView extends Vue {
|
||||
this.veriClaimDidsVisible = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract offer fulfillment information from the fulfills array
|
||||
*/
|
||||
extractOfferFulfillment() {
|
||||
this.detailsForGiveOfferFulfillment = libsUtil.extractOfferFulfillment(
|
||||
this.detailsForGive?.fullClaim?.fulfills
|
||||
);
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// UTILITY METHODS
|
||||
// =================================================
|
||||
@@ -758,13 +795,6 @@ export default class ClaimView extends Vue {
|
||||
this.canShare = !!navigator.share;
|
||||
}
|
||||
|
||||
// insert a space before any capital letters except the initial letter
|
||||
// (and capitalize initial letter, just in case)
|
||||
capitalizeAndInsertSpacesBeforeCaps(text: string): string {
|
||||
if (!text) return "";
|
||||
return text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||
}
|
||||
|
||||
totalConfirmers() {
|
||||
return (
|
||||
this.numConfsNotVisible +
|
||||
@@ -821,6 +851,8 @@ export default class ClaimView extends Vue {
|
||||
});
|
||||
if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
|
||||
this.detailsForGive = giveResp.data.data[0];
|
||||
// Extract offer information from the fulfills array
|
||||
this.extractOfferFulfillment();
|
||||
} else {
|
||||
await this.$logError(
|
||||
"Error getting detailed give info: " + JSON.stringify(giveResp),
|
||||
|
||||
@@ -96,50 +96,50 @@
|
||||
</div>
|
||||
|
||||
<!-- Fullfills Links -->
|
||||
<div class="mt-4">
|
||||
<!-- fullfills links for a give -->
|
||||
<div v-if="giveDetails?.fulfillsPlanHandleId">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(
|
||||
giveDetails?.fulfillsPlanHandleId || '',
|
||||
)
|
||||
"
|
||||
class="text-blue-500 mt-2 cursor-pointer"
|
||||
>
|
||||
This fulfills a bigger plan
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- fullfills links for a give -->
|
||||
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
encodeURIComponent(giveDetails?.fulfillsPlanHandleId || '')
|
||||
"
|
||||
class="text-blue-500 mt-2 cursor-pointer"
|
||||
>
|
||||
This fulfills a bigger plan
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- if there's another, it's probably fulfilling an offer, too -->
|
||||
<div
|
||||
v-if="
|
||||
giveDetails?.fulfillsType &&
|
||||
giveDetails?.fulfillsType !== 'PlanAction' &&
|
||||
giveDetails?.fulfillsHandleId
|
||||
"
|
||||
>
|
||||
<!-- router-link to /claim/ only changes URL path -->
|
||||
<router-link
|
||||
:to="
|
||||
'/claim/' +
|
||||
encodeURIComponent(giveDetails?.fulfillsHandleId || '')
|
||||
"
|
||||
class="text-blue-500 mt-2 cursor-pointer"
|
||||
>
|
||||
This fulfills
|
||||
{{
|
||||
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
||||
giveDetails?.fulfillsType || "",
|
||||
)
|
||||
}}
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
<!-- Show offer fulfillment if this give fulfills an offer -->
|
||||
<div v-if="giveDetailsOfferFulfillment?.offerHandleId">
|
||||
<!-- router-link to /claim/ only changes URL path -->
|
||||
<router-link
|
||||
:to="
|
||||
'/claim/' +
|
||||
encodeURIComponent(
|
||||
giveDetailsOfferFulfillment.offerHandleId || '',
|
||||
)
|
||||
"
|
||||
class="text-blue-500 mt-2 cursor-pointer"
|
||||
>
|
||||
This fulfills
|
||||
{{
|
||||
serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
||||
giveDetailsOfferFulfillment.offerType || "Offer",
|
||||
)
|
||||
}}
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -493,6 +493,11 @@ export default class ConfirmGiftView extends Vue {
|
||||
confsVisibleErrorMessage = "";
|
||||
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
|
||||
giveDetails?: GiveSummaryRecord;
|
||||
// Additional offer information extracted from the fulfills array
|
||||
giveDetailsOfferFulfillment: {
|
||||
offerHandleId?: string;
|
||||
offerType?: string;
|
||||
} | null = null;
|
||||
giverName = "";
|
||||
issuerName = "";
|
||||
isLoading = false;
|
||||
@@ -648,6 +653,8 @@ export default class ConfirmGiftView extends Vue {
|
||||
|
||||
if (resp.status === 200) {
|
||||
this.giveDetails = resp.data.data[0];
|
||||
// Extract offer information from the fulfills array
|
||||
this.extractOfferFulfillment();
|
||||
} else {
|
||||
throw new Error("Error getting detailed give info: " + resp.status);
|
||||
}
|
||||
@@ -707,6 +714,15 @@ export default class ConfirmGiftView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract offer fulfillment information from the fulfills array
|
||||
*/
|
||||
private extractOfferFulfillment() {
|
||||
this.giveDetailsOfferFulfillment = libsUtil.extractOfferFulfillment(
|
||||
this.giveDetails?.fullClaim?.fulfills
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches confirmer information for the claim
|
||||
*/
|
||||
@@ -849,27 +865,6 @@ export default class ConfirmGiftView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats type string for display by adding spaces before capitals
|
||||
* Optionally adds a prefix
|
||||
*
|
||||
* @param text - Text to format
|
||||
* @param prefix - Optional prefix to add
|
||||
* @returns Formatted string
|
||||
*/
|
||||
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string): string {
|
||||
const word = this.capitalizeAndInsertSpacesBeforeCaps(text);
|
||||
if (word) {
|
||||
// if the word starts with a vowel, use "an" instead of "a"
|
||||
const firstLetter = word[0].toLowerCase();
|
||||
const vowels = ["a", "e", "i", "o", "u"];
|
||||
const particle = vowels.includes(firstLetter) ? "an" : "a";
|
||||
return particle + " " + word;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates sharing of claim information
|
||||
* Handles share functionality based on platform capabilities
|
||||
@@ -894,11 +889,5 @@ export default class ConfirmGiftView extends Vue {
|
||||
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||
this.veriClaimDump = "";
|
||||
}
|
||||
|
||||
capitalizeAndInsertSpacesBeforeCaps(text: string) {
|
||||
return !text
|
||||
? ""
|
||||
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user