Compare commits

...

29 Commits

Author SHA1 Message Date
25512d3db1 some fixes to the gifted-dialog logic 2025-06-29 18:18:18 -06:00
Jose Olarte III
bf9fee7ee9 Various aesthetic improvements and optimizations 2025-06-27 21:59:19 +08:00
Jose Olarte III
08c46a27d3 Add project-to-project case 2025-06-26 18:52:52 +08:00
Jose Olarte III
c9405839c3 Merge branch 'gifting-ui-2025-05' into gifting-periphery-improvements 2025-06-26 15:27:55 +08:00
0e6a9c4f89 adjust grammar for recording receipt 2025-06-25 20:51:57 -06:00
Jose Olarte III
b6278ca148 Unit codes pulled from util.ts 2025-06-25 21:28:25 +08:00
Jose Olarte III
d8e237f8cb Describe firstStep variable 2025-06-25 21:17:11 +08:00
Jose Olarte III
4b539ccc55 Better handling of No-name and Unnamed entities 2025-06-25 21:15:05 +08:00
Jose Olarte III
ea49173885 Changed currentStep to boolean 2025-06-25 17:38:33 +08:00
Jose Olarte III
447a7cb089 Style "unnamed" entity 2025-06-25 17:35:01 +08:00
Jose Olarte III
c0ddba8898 Various design tweaks 2025-06-24 19:22:09 +08:00
Jose Olarte III
fe4ae90849 Giver-recipient display fixes
- Truncate very long texts (such as dids)
- Stacked layout in mobile, row layout in wider screens
- Minor design adjustments
2025-06-24 19:19:51 +08:00
Jose Olarte III
ce04312baa Updated amount input controls
Now consistent with gifting dialog version
2025-06-24 19:17:30 +08:00
Jose Olarte III
a8cc480960 Merge branch 'master' into gifting-periphery-improvements 2025-06-24 16:20:07 +08:00
Jose Olarte III
357822d713 Fix: truncate text blocks
- Avoid did display stretching screen width
2025-06-24 16:18:22 +08:00
Jose Olarte III
ca22161f12 Fix: entity-type identifier validation
- Ensure claims contain only correct and necessary giver and recipient identifiers, as per Endorser.ch documentation
2025-06-20 20:37:14 +08:00
Jose Olarte III
d3b80fbe47 Feature: giver-recipient validation
- Ensures person-to-person gifting won't allow the same entity as giver and recipient
- Disable user item selection if it would create conflict
- Error messaging fallback
2025-06-20 18:38:35 +08:00
Jose Olarte III
0342c872f4 Fix: added context for ContactGiftingView 2025-06-20 15:50:57 +08:00
Jose Olarte III
a7e65b3b49 Giver-recipient controls
- Dialog now shows separate cards for giver and recipient
- Ability to change giver and/or recipient
- Project giver/recipient is locked in ProjectView (context reinforcement)
2025-06-19 21:16:56 +08:00
Jose Olarte III
eb7605991c Fixed more gifting use cases 2025-06-18 19:58:10 +08:00
fa21660fd1 fix spelling 2025-06-15 12:43:22 -06:00
Jose Olarte III
df1c1f0186 Fix: pass project info
In GiftingDialog, project information is passed along if:
- Selecting "Show All" to go to ContactGiftingView
- Selecting "Photos and Other Options" to go to GiftedDetailsView
2025-06-13 20:52:26 +08:00
Jose Olarte III
3daf1c8a5c Feature: Project Gifting
- Gifting dialog: added ability to pick a project to benefit from
- Project view: modified dialog calls in Project view to toggle between giving to and benefiting from a project
- Project view: removed redundant person selection
- Project view: benefiting from a project locks the project selection in dialog to enforce context.
2025-06-12 20:50:27 +08:00
Jose Olarte III
7eefee1ea5 Fix: Conditional show-all link
- Only show "Show All" when user has contacts
2025-06-12 14:34:00 +08:00
Jose Olarte III
140c36a416 Merge branch 'master' into gifting-ui-2025-05 2025-06-11 19:10:59 +08:00
Jose Olarte III
988244b7ae Added check for "Unnamed" giver
Pass string "Unnamed" to select unnamed giver and skip contact selection step of dialog.
2025-05-16 20:22:09 +08:00
Jose Olarte III
4b355a5448 WIP: two-step dialog + functionality
- Dialog is now presented as two distinct steps
- Gifting functionality reinstated
- Minor UI tweaks
- IN PROGRESS: ensuring calls to dialog from other parts of the app remain functional
2025-05-14 21:47:12 +08:00
Jose Olarte III
b511f9cd24 WIP: adjustments to bring closer to original mockups 2025-05-13 21:16:39 +08:00
Jose Olarte III
579cecbe6e WIP: gifting UI revamp
Started to transform the gifting dialog into the two-step setup as per previous mockups
2025-05-12 21:22:05 +08:00
7 changed files with 1206 additions and 346 deletions

View File

@@ -1,99 +1,534 @@
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4"> <!-- Step 1: Giver -->
{{ customTitle }} <div v-show="firstStep" id="sectionGiftedGiver">
</h1> <label class="block font-bold mb-4">
<input {{
v-model="description" stepType === "recipient"
type="text" ? "Choose who received the gift:"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" : showProjects
:placeholder="prompt || 'What was given?'" ? "Choose a project benefitted from:"
/> : "Choose a person received from:"
<div class="flex flex-row justify-center"> }}
<span </label>
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()" <!-- Unified Quick-pick grid for People and Projects -->
<ul
:class="
shouldShowProjects
? 'grid grid-cols-3 md:grid-cols-4 gap-x-2 gap-y-4 text-center mb-4'
: 'grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-2 gap-y-4 text-center mb-4'
"
> >
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }} <template v-if="shouldShowProjects">
</span> <!-- show projects -->
<div <li
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" v-for="project in projects.slice(0, 7)"
@click="amountInput === '0' ? null : decrement()" :key="project.handleId"
> class="cursor-pointer"
<font-awesome icon="chevron-left" /> @click="
</div> stepType === 'recipient'
<input ? selectRecipientProject(project)
id="inputGivenAmount" : selectProject(project)
v-model="amountInput" "
type="number" >
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20" <div class="relative w-fit mx-auto mb-1">
/> <ProjectIcon
<div :entity-id="project.handleId"
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2" :icon-size="48"
@click="increment()" :image-url="project.image"
> class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full"
<font-awesome icon="chevron-right" /> />
</div> </div>
</div> <h3
<div class="mt-4 flex justify-center"> class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
<span> >
<router-link {{ project.name }}
:to="{ </h3>
name: 'gifted-details', <div class="text-xs text-slate-500 truncate">
query: { <font-awesome icon="user" class="fa-fw text-slate-400" />
amountInput, {{
description, didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
giverDid: giver?.did, }}
giverName: giver?.name, </div>
offerId, </li>
fulfillsProjectId: toProjectId, <li
providerProjectId: fromProjectId, v-if="projects.length === 0"
recipientDid: receiver?.did, class="text-xs text-slate-500 italic col-span-full"
recipientName: receiver?.name, >
unitCode, (No projects found.)
}, </li>
}" <li v-if="projects.length > 0">
class="text-blue-500" <router-link :to="{ name: 'discover' }" class="cursor-pointer">
> <font-awesome
Photo & more options ... icon="circle-right"
</router-link> class="text-blue-500 text-5xl mb-1"
</span> />
</div> <h3
<p class="text-center mb-2 mt-6 italic"> class="text-xs text-slate-400 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
Sign & Send to publish to the world >
<font-awesome Show All
icon="circle-info" </h3>
class="pl-2 text-blue-500 cursor-pointer" </router-link>
@click="explainData()" </li>
/> </template>
</p> <template v-else>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <!-- show people (contacts) -->
<li
v-if="
stepType === 'recipient' ||
(stepType === 'giver' && isFromProjectView)
"
:class="{
'cursor-pointer': !wouldCreateConflict(activeDid),
'cursor-not-allowed opacity-50': wouldCreateConflict(activeDid)
}"
@click="
!wouldCreateConflict(activeDid) &&
(stepType === 'recipient'
? selectRecipient({ did: activeDid, name: 'You' })
: selectGiver({ did: activeDid, name: 'You' }))
"
>
<font-awesome
:class="{
'text-blue-500 text-5xl mb-1': !wouldCreateConflict(activeDid),
'text-slate-400 text-5xl mb-1': wouldCreateConflict(activeDid)
}"
icon="hand"
/>
<h3
:class="{
'text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden': !wouldCreateConflict(activeDid),
'text-xs text-slate-400 font-medium text-ellipsis whitespace-nowrap overflow-hidden': wouldCreateConflict(activeDid)
}"
>
You
</h3>
</li>
<li
class="cursor-pointer"
@click="
stepType === 'recipient' ? selectRecipient() : selectGiver()
"
>
<font-awesome
icon="circle-question"
class="text-slate-400 text-5xl mb-1"
/>
<h3
class="text-xs text-slate-400 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
(Unnamed)
</h3>
</li>
<li
v-if="allContacts.length === 0"
class="text-xs text-slate-500 italic col-span-full"
>
(Add friends to see more people worthy of recognition.)
</li>
<li
v-for="contact in allContacts.slice(0, 10)"
:key="contact.did"
:class="{
'cursor-pointer': !wouldCreateConflict(contact.did),
'cursor-not-allowed opacity-50': wouldCreateConflict(contact.did)
}"
@click="
!wouldCreateConflict(contact.did) &&
(stepType === 'recipient'
? selectRecipient(contact)
: selectGiver(contact))
"
>
<div class="relative w-fit mx-auto mb-1">
<EntityIcon
:contact="contact"
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full"
/>
<div
class="rounded-full bg-slate-400 absolute bottom-0 right-0 p-1 translate-x-1/3"
>
<font-awesome
icon="clock"
class="block text-white text-xs w-[1em]"
/>
</div>
</div>
<h3
:class="{
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden': !wouldCreateConflict(contact.did) && contact.name,
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden text-slate-400 italic': !contact.name,
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden text-slate-400': wouldCreateConflict(contact.did) && contact.name
}"
>
{{ contact.name || "(No name)" }}
</h3>
</li>
<li v-if="allContacts.length > 0" class="cursor-pointer">
<router-link
:to="{
name: 'contact-gift',
query: {
stepType: stepType,
giverEntityType: giverEntityType,
recipientEntityType: recipientEntityType,
...(stepType === 'giver'
? {
recipientProjectId: toProjectId,
recipientProjectName: receiver?.name,
recipientProjectImage: receiver?.image,
recipientProjectHandleId: receiver?.handleId,
recipientDid: receiver?.did,
}
: {
giverProjectId: fromProjectId,
giverProjectName: giver?.name,
giverProjectImage: giver?.image,
giverProjectHandleId: giver?.handleId,
giverDid: giver?.did,
}),
fromProjectId: fromProjectId,
toProjectId: toProjectId,
showProjects: (showProjects || false).toString(),
isFromProjectView: (isFromProjectView || false).toString(),
},
}"
>
<font-awesome
icon="circle-right"
class="text-blue-500 text-5xl mb-1"
/>
<h3
class="text-xs text-slate-400 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
Show All
</h3>
</router-link>
</li>
</template>
</ul>
<button <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" 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-lg"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="cancel" @click="cancel"
> >
Cancel Cancel
</button> </button>
</div> </div>
<!-- Step 2: Gift -->
<div v-show="!firstStep" id="sectionGiftedGift">
<div class="grid grid-cols-2 gap-2 mb-4">
<!-- Giver Button -->
<button
v-if="
(giverEntityType === 'person' || giverEntityType === 'project') &&
!(isFromProjectView && giverEntityType === 'project')
"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
@click="goBackToStep1('giver')"
>
<div>
<template v-if="giverEntityType === 'project'">
<ProjectIcon
v-if="giver?.handleId"
:entity-id="giver.handleId"
:icon-size="32"
:image-url="giver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</template>
<template v-else>
<EntityIcon
v-if="giver?.did"
:contact="giver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</template>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{
giverEntityType === "project"
? "Benefited from:"
: "Received from:"
}}
</p>
<h3
v-if="giver?.name && giver.name !== giver.did"
class="font-semibold truncate"
>
{{ giver.name }}
</h3>
<h3
v-if="giver?.name && giver.name === giver.did"
class="font-semibold truncate text-slate-400 italic"
>
(No name)
</h3>
<h3
v-else-if="!giver?.name"
class="font-semibold truncate text-slate-400 italic"
>
(Unnamed)
</h3>
</div>
<p class="ms-auto text-sm text-blue-500 pe-1">
<font-awesome icon="pen" title="Change" />
</p>
</button>
<div
v-else
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
>
<div>
<template v-if="giverEntityType === 'project'">
<ProjectIcon
v-if="giver?.handleId"
:entity-id="giver.handleId"
:icon-size="32"
:image-url="giver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</template>
<template v-else>
<EntityIcon
v-if="giver?.did"
:contact="giver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</template>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{
giverEntityType === "project"
? "Benefited from:"
: "Received from:"
}}
</p>
<h3
v-if="giver?.name && giver.name !== giver.did"
class="font-semibold truncate"
>
{{ giver.name }}
</h3>
<h3
v-if="giver?.name && giver.name === giver.did"
class="font-semibold truncate text-slate-400 italic"
>
(No name)
</h3>
<h3
v-else-if="!giver?.name"
class="font-semibold truncate text-slate-400 italic"
>
(Unnamed)
</h3>
</div>
<p class="ms-auto text-sm text-slate-400 pe-1">
<font-awesome icon="lock" title="Can't be changed" />
</p>
</div>
<!-- Recipient Button -->
<button
v-if="recipientEntityType === 'person'"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
@click="goBackToStep1('recipient')"
>
<div>
<EntityIcon
v-if="receiver?.did"
:contact="receiver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
Given to:
</p>
<h3
v-if="receiver?.name && receiver.name !== receiver.did"
class="font-semibold truncate"
>
{{ receiver.name }}
</h3>
<h3
v-if="receiver?.name && receiver.name === receiver.did"
class="font-semibold truncate text-slate-400 italic"
>
(No name)
</h3>
<h3
v-else-if="!receiver?.name"
class="font-semibold truncate text-slate-400 italic"
>
(Unnamed)
</h3>
</div>
<p class="ms-auto text-sm text-blue-500 pe-1">
<font-awesome icon="pen" title="Change" />
</p>
</button>
<div
v-else-if="recipientEntityType === 'project'"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
>
<div>
<ProjectIcon
v-if="receiver?.handleId"
:entity-id="receiver.handleId"
:icon-size="32"
:image-url="receiver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
Given to project:
</p>
<h3
v-if="receiver?.name"
class="font-semibold truncate"
>
{{ receiver.name }}
</h3>
<h3
v-else
class="font-semibold truncate text-slate-400 italic"
>
(Unnamed)
</h3>
</div>
<p class="ms-auto text-sm text-slate-400 pe-1">
<font-awesome icon="lock" title="Can't be changed" />
</p>
</div>
</div>
<input
v-model="description"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2 mb-4 placeholder:italic"
:placeholder="prompt || 'What was given?'"
/>
<div class="flex mb-4">
<button
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
</button>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
/>
<button
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
</button>
<select
v-model="unitCode"
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
>
<option
v-for="(displayName, code) in unitOptions"
:key="code"
:value="code"
>
{{ displayName }}
</option>
</select>
</div>
<router-link
:to="{
name: 'gifted-details',
query: giftedDetailsQuery,
}"
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-lg mb-4"
>
Photo &amp; more options&hellip;
</router-link>
<p class="text-center text-sm mb-4">
<b class="font-medium">Sign &amp; Send</b> to publish to the world
<font-awesome
icon="circle-info"
class="fa-fw text-blue-500 text-base cursor-pointer"
@click="explainData()"
/>
</p>
<!-- Conflict warning -->
<div v-if="hasPersonConflict" class="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
<p class="text-red-700 text-sm text-center">
<font-awesome icon="exclamation-triangle" class="fa-fw mr-1" />
Cannot record: Same person selected as both giver and recipient
</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
:disabled="hasPersonConflict"
:class="{
'block w-full text-center text-md uppercase font-bold 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-1.5 py-2 rounded-lg': !hasPersonConflict,
'block w-full text-center text-md uppercase font-bold bg-gradient-to-b from-slate-300 to-slate-500 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-400 px-1.5 py-2 rounded-lg cursor-not-allowed': hasPersonConflict
}"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
@click="cancel"
>
Cancel
</button>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop, Watch } from "vue-facing-decorator";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
serverMessageForUser, serverMessageForUser,
getHeaders,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { db, retrieveSettingsForActiveAccount } from "../db/index"; import { db, retrieveSettingsForActiveAccount } from "../db/index";
@@ -102,13 +537,38 @@ import * as databaseUtil from "../db/databaseUtil";
import { retrieveAccountDids } from "../libs/util"; import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import EntityIcon from "../components/EntityIcon.vue";
import ProjectIcon from "../components/ProjectIcon.vue";
import { PlanData } from "../interfaces/records";
@Component @Component({
components: {
EntityIcon,
ProjectIcon,
},
})
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop() fromProjectId = ""; @Prop() fromProjectId = "";
@Prop() toProjectId = ""; @Prop() toProjectId = "";
@Prop({ default: false }) showProjects = false;
@Prop() isFromProjectView = false;
@Watch("showProjects")
onShowProjectsChange() {
this.updateEntityTypes();
}
@Watch("fromProjectId")
onFromProjectIdChange() {
this.updateEntityTypes();
}
@Watch("toProjectId")
onToProjectIdChange() {
this.updateEntityTypes();
}
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -119,6 +579,7 @@ export default class GiftedDialog extends Vue {
callbackOnSuccess?: (amount: number) => void = () => {}; callbackOnSuccess?: (amount: number) => void = () => {};
customTitle?: string; customTitle?: string;
description = ""; description = "";
firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description)
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
offerId = ""; offerId = "";
prompt = ""; prompt = "";
@@ -128,6 +589,80 @@ export default class GiftedDialog extends Vue {
libsUtil = libsUtil; libsUtil = libsUtil;
projects: PlanData[] = [];
didInfo = didInfo;
// Computed property to help debug template logic
get shouldShowProjects() {
const result =
(this.stepType === "giver" && this.giverEntityType === "project") ||
(this.stepType === "recipient" && this.recipientEntityType === "project");
return result;
}
// Computed property to check if current selection would create a conflict
get hasPersonConflict() {
// Only check for conflicts when both entities are persons
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
// Check if giver and recipient are the same person
if (this.giver?.did && this.receiver?.did && this.giver.did === this.receiver.did) {
return true;
}
return false;
}
// Computed property to check if a contact would create a conflict when selected
wouldCreateConflict(contactDid: string) {
// Only check for conflicts when both entities are persons
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.did === contactDid;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.did === contactDid;
}
return false;
}
stepType = "giver";
giverEntityType = "person" as "person" | "project";
recipientEntityType = "person" as "person" | "project";
updateEntityTypes() {
// Reset and set entity types based on current context
this.giverEntityType = "person";
this.recipientEntityType = "person";
// Determine entity types based on current context
if (this.showProjects) {
// HomeView "Project" button or ProjectViewView "Given by This"
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.fromProjectId) {
// ProjectViewView "Given by This" button (project is giver)
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.toProjectId) {
// ProjectViewView "Given to This" button (project is recipient)
this.giverEntityType = "person";
this.recipientEntityType = "project";
} else {
// HomeView "Person" button
this.giverEntityType = "person";
this.recipientEntityType = "person";
}
}
async open( async open(
giver?: libsUtil.GiverReceiverInputInfo, giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo, receiver?: libsUtil.GiverReceiverInputInfo,
@@ -140,10 +675,14 @@ export default class GiftedDialog extends Vue {
this.giver = giver; this.giver = giver;
this.prompt = prompt || ""; this.prompt = prompt || "";
this.receiver = receiver; this.receiver = receiver;
// if we show "given to user" selection, default checkbox to true
this.amountInput = "0"; this.amountInput = "0";
this.callbackOnSuccess = callbackOnSuccess; this.callbackOnSuccess = callbackOnSuccess;
this.offerId = offerId || ""; this.offerId = offerId || "";
this.firstStep = !giver;
this.stepType = "giver";
// Update entity types based on current props
this.updateEntityTypes();
try { try {
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
@@ -174,7 +713,16 @@ export default class GiftedDialog extends Vue {
this.allContacts, this.allContacts,
); );
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (
this.giverEntityType === "project" ||
this.recipientEntityType === "project"
) {
await this.loadProjects();
} else {
// Clear projects array when not needed
this.projects = [];
}
} catch (err: any) { } catch (err: any) {
logger.error("Error retrieving settings from database:", err); logger.error("Error retrieving settings from database:", err);
this.$notify( this.$notify(
@@ -224,6 +772,7 @@ export default class GiftedDialog extends Vue {
this.amountInput = "0"; this.amountInput = "0";
this.prompt = ""; this.prompt = "";
this.unitCode = "HUR"; this.unitCode = "HUR";
this.firstStep = true;
} }
async confirm() { async confirm() {
@@ -265,6 +814,20 @@ export default class GiftedDialog extends Vue {
); );
return; return;
} }
// Check for person conflict
if (this.hasPersonConflict) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You cannot select the same person as both giver and recipient.",
},
3000,
);
return;
}
this.close(); this.close();
this.$notify( this.$notify(
@@ -304,20 +867,52 @@ export default class GiftedDialog extends Vue {
unitCode: string = "HUR", unitCode: string = "HUR",
) { ) {
try { try {
// Determine the correct parameters based on entity types
let fromDid: string | undefined;
let toDid: string | undefined;
let fulfillsProjectHandleId: string | undefined;
let providerPlanHandleId: string | undefined;
if (this.giverEntityType === "project" && this.recipientEntityType === "person") {
// Project-to-person gift
fromDid = undefined; // No person giver
toDid = recipientDid as string; // Person recipient
fulfillsProjectHandleId = undefined; // No project recipient
providerPlanHandleId = this.giver?.handleId; // Project giver
} else if (this.giverEntityType === "person" && this.recipientEntityType === "project") {
// Person-to-project gift
fromDid = giverDid as string; // Person giver
toDid = undefined; // No person recipient
fulfillsProjectHandleId = this.toProjectId; // Project recipient
providerPlanHandleId = undefined; // No project giver
} else if (this.giverEntityType === "project" && this.recipientEntityType === "project") {
// Project-to-project gift
fromDid = undefined; // No person giver
toDid = undefined; // No person recipient
fulfillsProjectHandleId = this.toProjectId; // Project recipient
providerPlanHandleId = this.giver?.handleId; // Project giver
} else {
// Person-to-person gift
fromDid = giverDid as string;
toDid = recipientDid as string;
fulfillsProjectHandleId = undefined;
providerPlanHandleId = undefined;
}
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid, this.activeDid,
giverDid as string, fromDid,
recipientDid as string, toDid,
description, description,
amount, amount,
unitCode, unitCode,
this.toProjectId, fulfillsProjectHandleId,
this.offerId, this.offerId,
false, false,
undefined, undefined,
this.fromProjectId, providerPlanHandleId,
); );
if (!result.success) { if (!result.success) {
@@ -378,6 +973,118 @@ export default class GiftedDialog extends Vue {
-1, -1,
); );
} }
selectGiver(contact?: Contact) {
if (contact) {
this.giver = {
did: contact.did,
name: contact.name || contact.did,
};
} else {
this.giver = {
did: "",
name: "",
};
}
this.firstStep = false;
}
goBackToStep1(step: string) {
this.stepType = step;
this.firstStep = true;
}
async loadProjects() {
try {
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
method: "GET",
headers: await getHeaders(this.activeDid),
});
if (response.status !== 200) {
throw new Error("Failed to load projects");
}
const results = await response.json();
if (results.data) {
this.projects = results.data;
}
} catch (error) {
logger.error("Error loading projects:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to load projects",
},
3000,
);
}
}
selectProject(project: PlanData) {
this.giver = {
name: project.name,
image: project.image,
handleId: project.handleId,
};
this.receiver = {
did: this.activeDid,
name: "You",
};
this.firstStep = false;
}
selectRecipient(contact?: Contact) {
if (contact) {
this.receiver = {
did: contact.did,
name: contact.name || contact.did,
};
} else {
this.receiver = {
did: "",
name: "",
};
}
this.firstStep = false;
}
selectRecipientProject(project: PlanData) {
this.receiver = {
// no did, because it's a project
name: project.name,
image: project.image,
handleId: project.handleId,
};
this.firstStep = false;
}
// Computed property for the query parameters
get giftedDetailsQuery() {
return {
amountInput: this.amountInput,
description: this.description,
giverDid: this.giverEntityType === "person" ? this.giver?.did : undefined,
giverName: this.giver?.name,
offerId: this.offerId,
fulfillsProjectId: this.giverEntityType === "person" && this.recipientEntityType === "project"
? this.toProjectId
: undefined,
providerProjectId: this.giverEntityType === "project" && this.recipientEntityType === "person"
? this.giver?.handleId
: this.fromProjectId,
recipientDid: this.receiver?.did,
recipientName: this.receiver?.name,
unitCode: this.unitCode,
};
}
// Computed property to get unit options
get unitOptions() {
return this.libsUtil.UNIT_SHORT;
}
} }
</script> </script>

View File

@@ -29,6 +29,7 @@ import {
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
faCircleQuestion, faCircleQuestion,
faCircleRight,
faCircleUser, faCircleUser,
faClock, faClock,
faCoins, faCoins,
@@ -60,6 +61,7 @@ import {
faLightbulb, faLightbulb,
faLink, faLink,
faLocationDot, faLocationDot,
faLock,
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,
faMagnifyingGlass, faMagnifyingGlass,
@@ -79,6 +81,7 @@ import {
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faThumbtack,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
faUser, faUser,
@@ -111,6 +114,7 @@ library.add(
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
faCircleQuestion, faCircleQuestion,
faCircleRight,
faCircleUser, faCircleUser,
faClock, faClock,
faCoins, faCoins,
@@ -142,6 +146,7 @@ library.add(
faLightbulb, faLightbulb,
faLink, faLink,
faLocationDot, faLocationDot,
faLock,
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,
faMagnifyingGlass, faMagnifyingGlass,
@@ -161,6 +166,7 @@ library.add(
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faThumbtack,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
faUser, faUser,

View File

@@ -50,6 +50,8 @@ import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
export interface GiverReceiverInputInfo { export interface GiverReceiverInputInfo {
did?: string; did?: string;
name?: string; name?: string;
image?: string;
handleId?: string;
} }
export enum OnboardPage { export enum OnboardPage {

View File

@@ -4,14 +4,14 @@
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-2xl text-center font-semibold relative px-7">
<!-- Back --> <!-- Back -->
<router-link <router-link
:to="{ name: 'home' }" :to="{ name: 'home' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome> ><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link> </router-link>
Given by... {{ stepType === "giver" ? "Given by..." : "Given to..." }}
</h1> </h1>
</div> </div>
@@ -19,19 +19,18 @@
<ul class="border-t border-slate-300"> <ul class="border-t border-slate-300">
<li class="border-b border-slate-300 py-3"> <li class="border-b border-slate-300 py-3">
<h2 class="text-base flex gap-4 items-center"> <h2 class="text-base flex gap-4 items-center">
<span class="grow"> <span class="grow flex gap-2 items-center font-medium">
<img <font-awesome
src="../assets/blank-square.svg" icon="circle-question"
width="32" class="text-slate-400 text-4xl"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
/> />
Unnamed/Unknown <span class="italic text-slate-400">(Unnamed/Unknown)</span>
</span> </span>
<span class="text-right"> <span class="text-right">
<button <button
type="button" type="button"
class="block w-full text-center text-sm 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-3 py-1.5 rounded-md" class="block w-full text-center text-sm 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-3 py-1.5 rounded-md"
@click="openDialog()" @click="openDialog('Unnamed')"
> >
<font-awesome icon="gift" class="fa-fw"></font-awesome> <font-awesome icon="gift" class="fa-fw"></font-awesome>
</button> </button>
@@ -44,13 +43,14 @@
class="border-b border-slate-300 py-3" class="border-b border-slate-300 py-3"
> >
<h2 class="text-base flex gap-4 items-center"> <h2 class="text-base flex gap-4 items-center">
<span class="grow font-semibold"> <span class="grow flex gap-2 items-center font-medium">
<EntityIcon <EntityIcon
:contact="contact" :contact="contact"
:icon-size="32" :icon-size="34"
class="inline-block align-middle border border-slate-300 rounded-md mr-1" class="inline-block align-middle border border-slate-300 rounded-full overflow-hidden"
/> />
{{ contact.name || "(no name)" }} <span v-if="contact.name">{{ contact.name }}</span>
<span v-else class="italic text-slate-400">(No name)</span>
</span> </span>
<span class="text-right"> <span class="text-right">
<button <button
@@ -65,7 +65,13 @@
</li> </li>
</ul> </ul>
<GiftedDialog ref="customDialog" :to-project-id="projectId" /> <GiftedDialog
ref="customDialog"
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
:show-projects="showProjects"
:is-from-project-view="isFromProjectView"
/>
</section> </section>
</template> </template>
@@ -97,6 +103,24 @@ export default class ContactGiftingView extends Vue {
description = ""; description = "";
projectId = ""; projectId = "";
prompt = ""; prompt = "";
recipientProjectName = "";
recipientProjectImage = "";
recipientProjectHandleId = "";
// New context parameters
stepType = "giver";
giverEntityType = "person" as "person" | "project";
recipientEntityType = "person" as "person" | "project";
giverProjectId = "";
giverProjectName = "";
giverProjectImage = "";
giverProjectHandleId = "";
giverDid = "";
recipientDid = "";
fromProjectId = "";
toProjectId = "";
showProjects = false;
isFromProjectView = false;
async created() { async created() {
try { try {
@@ -124,9 +148,41 @@ export default class ContactGiftingView extends Vue {
); );
} }
this.projectId = (this.$route.query["projectId"] as string) || ""; this.projectId =
(this.$route.query["recipientProjectId"] as string) || "";
this.recipientProjectName =
(this.$route.query["recipientProjectName"] as string) || "";
this.recipientProjectImage =
(this.$route.query["recipientProjectImage"] as string) || "";
this.recipientProjectHandleId =
(this.$route.query["recipientProjectHandleId"] as string) || "";
this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt; this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt;
// Read new context parameters
this.stepType = (this.$route.query["stepType"] as string) || "giver";
this.giverEntityType =
(this.$route.query["giverEntityType"] as "person" | "project") ||
"person";
this.recipientEntityType =
(this.$route.query["recipientEntityType"] as "person" | "project") ||
"person";
this.giverProjectId =
(this.$route.query["giverProjectId"] as string) || "";
this.giverProjectName =
(this.$route.query["giverProjectName"] as string) || "";
this.giverProjectImage =
(this.$route.query["giverProjectImage"] as string) || "";
this.giverProjectHandleId =
(this.$route.query["giverProjectHandleId"] as string) || "";
this.giverDid = (this.$route.query["giverDid"] as string) || "";
this.recipientDid = (this.$route.query["recipientDid"] as string) || "";
this.fromProjectId = (this.$route.query["fromProjectId"] as string) || "";
this.toProjectId = (this.$route.query["toProjectId"] as string) || "";
this.showProjects =
(this.$route.query["showProjects"] as string) === "true";
this.isFromProjectView =
(this.$route.query["isFromProjectView"] as string) === "true";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
logger.error("Error retrieving settings & contacts:", err); logger.error("Error retrieving settings & contacts:", err);
@@ -144,17 +200,108 @@ export default class ContactGiftingView extends Vue {
} }
} }
openDialog(giver?: GiverReceiverInputInfo) { openDialog(contact?: GiverReceiverInputInfo | "Unnamed") {
const recipient = this.projectId if (contact === "Unnamed") {
? undefined // Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
: { did: this.activeDid, name: "you" }; let recipient: GiverReceiverInputInfo;
(this.$refs.customDialog as GiftedDialog).open( let giver: GiverReceiverInputInfo | undefined;
giver,
recipient, if (this.stepType === "giver") {
undefined, // We're selecting a giver, so recipient is either a project or the current user
"Given by " + (giver?.name || "someone not named"), if (this.recipientEntityType === "project") {
this.prompt, recipient = {
); did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
recipient = { did: this.activeDid, name: "You" };
}
giver = undefined; // Will be set to "Unnamed" in GiftedDialog
} else {
// We're selecting a recipient, so recipient is "Unnamed" and giver is preserved from context
recipient = { did: "", name: "Unnamed" };
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
// no did, because it's a project
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else if (this.giverDid) {
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
this.stepType === "giver" ? "Given by Unnamed" : "Given to Unnamed",
this.prompt,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.customDialog as GiftedDialog).selectGiver();
} else {
// Regular case: contact is a GiverReceiverInputInfo
let giver: GiverReceiverInputInfo;
let recipient: GiverReceiverInputInfo;
if (this.stepType === "giver") {
// We're selecting a giver, so the contact becomes the giver
giver = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Recipient is either a project or the current user
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
recipient = { did: this.activeDid, name: "You" };
}
} else {
// We're selecting a recipient, so the contact becomes the recipient
recipient = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
did: this.giverProjectHandleId,
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else if (this.giverDid) {
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
this.stepType === "giver"
? "Given by " + (contact?.name || "someone not named")
: "Given to " + (contact?.name || "someone not named"),
this.prompt,
);
}
} }
} }
</script> </script>

View File

@@ -4,84 +4,91 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <!-- Breadcrumb -->
<div <div id="ViewBreadcrumb" class="mb-8">
v-if="!hideBackButton" <h1 class="text-2xl text-center font-semibold relative px-7 mb-2">
class="text-lg text-center font-light relative px-7" <!-- Back -->
> <div
<h1 v-if="!hideBackButton"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancelBack()" @click="cancelBack()"
> >
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome> <font-awesome icon="chevron-left" class="fa-fw" />
</div>
What Was Given
</h1> </h1>
<h2 class="text-lg font-normal text-center overflow-hidden">
<div class="truncate">
From
{{
providedByProject
? providerProjectName
: providedByGiver
? giverName
: "someone not named"
}}
</div>
<div class="truncate">
to
{{
givenToProject
? fulfillsProjectName
: givenToRecipient
? recipientName
: "someone not named"
}}
</div>
</h2>
</div> </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
{{
providedByProject
? providerProjectName
: providedByGiver
? giverName
: "someone not named"
}}
</span>
<br />
<span>
to
{{
givenToProject
? fulfillsProjectName
: givenToRecipient
? recipientName
: "someone not named"
}}</span
>
</h1>
<textarea <textarea
v-model="description" v-model="description"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received" placeholder="What was received"
/> />
<div class="flex flex-row justify-center"> <div class="flex mb-4">
<span <button
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20" class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="changeUnitCode()"
>
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()" @click="amountInput === '0' ? null : decrement()"
> >
<font-awesome icon="chevron-left" /> <font-awesome icon="chevron-left" />
</div> </button>
<input <input
id="inputGivenAmount"
v-model="amountInput" v-model="amountInput"
type="number" type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20" class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
/> />
<div <button
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2" class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()" @click="increment()"
> >
<font-awesome icon="chevron-right" /> <font-awesome icon="chevron-right" />
</div> </button>
<select
v-model="unitCode"
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
>
<option
v-for="(displayName, code) in unitOptions"
:key="code"
:value="code"
>
{{ displayName }}
</option>
</select>
</div> </div>
<div class="flex justify-center mt-4" data-testId="imagery"> <div class="flex justify-center mt-4" data-testId="imagery">
<span v-if="imageUrl" class="flex justify-between"> <span v-if="imageUrl" class="flex items-end gap-3">
<a :href="imageUrl" target="_blank"> <a :href="imageUrl" target="_blank">
<img :src="imageUrl" class="h-24 rounded-xl" /> <img :src="imageUrl" class="h-36 rounded-lg" />
</a> </a>
<font-awesome <font-awesome
icon="trash-can" icon="trash-can"
class="text-red-500 fa-fw ml-8 mt-10" class="text-red-500 fa-fw cursor-pointer"
@click="confirmDeleteImage" @click="confirmDeleteImage"
/> />
</span> </span>
@@ -95,22 +102,22 @@
</div> </div>
<ImageMethodDialog ref="imageDialog" default-camera-mode="environment" /> <ImageMethodDialog ref="imageDialog" default-camera-mode="environment" />
<div class="mt-4 flex justify-between gap-2"> <div class="mt-4 sm:flex justify-between gap-2">
<!-- First Column for Giver --> <!-- First Column for Giver -->
<div class="flex-grow border border-slate-400 p-2 rounded-md"> <div class="sm:flex-grow sm:w-1/2 border border-slate-400 p-2 rounded-md overflow-hidden">
<div class="flex"> <div class="flex items-center">
<input <input
v-if="giverDid && !providedByProject" v-if="giverDid && !providedByProject"
v-model="providedByGiver" v-model="providedByGiver"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="flex-shrink-0 h-6 w-6 mr-2"
/> />
<font-awesome <font-awesome
v-else v-else
icon="square" icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm" class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
/> />
<label class="text-sm mt-1"> <label class="text-sm truncate">
{{ {{
giverDid giverDid
? "This was provided by " + giverName + "." ? "This was provided by " + giverName + "."
@@ -120,24 +127,24 @@
<font-awesome <font-awesome
v-if="!giverDid || providedByProject" v-if="!giverDid || providedByProject"
icon="info-circle" icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm" class="text-base cursor-pointer bg-white text-slate-500 ms-1"
@click="notifyUserOfGiver()" @click="notifyUserOfGiver()"
/> />
</div> </div>
<div class="flex"> <div class="flex items-center">
<input <input
v-if="providerProjectId && !providedByGiver" v-if="providerProjectId && !providedByGiver"
v-model="providedByProject" v-model="providedByProject"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="flex-shrink-0 h-6 w-6 mr-2"
/> />
<font-awesome <font-awesome
v-else v-else
icon="square" icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm" class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
/> />
<label class="text-sm mt-1"> <label class="text-sm truncate">
{{ {{
providerProjectId providerProjectId
? "This was provided by " + providerProjectName + "." ? "This was provided by " + providerProjectName + "."
@@ -147,31 +154,31 @@
<font-awesome <font-awesome
v-if="!providerProjectId || providedByGiver" v-if="!providerProjectId || providedByGiver"
icon="info-circle" icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm" class="text-base cursor-pointer bg-white text-slate-500 ms-1"
@click="notifyUserOfProvidingProject()" @click="notifyUserOfProvidingProject()"
/> />
</div> </div>
</div> </div>
<div class="flex-shrink flex justify-center items-center"> <div class="sm:flex-shrink flex justify-center items-center my-1 sm:my-0">
<font-awesome icon="arrow-right" class="fa-fw h-7" /> <font-awesome icon="arrow-right" class="fa-fw h-7 rotate-90 sm:rotate-0" />
</div> </div>
<!-- Third Column for Recipient --> <!-- Third Column for Recipient -->
<div class="flex-grow border border-slate-400 p-2 rounded-md"> <div class="sm:flex-grow sm:w-1/2 border border-slate-400 p-2 rounded-md overflow-hidden">
<div class="flex"> <div class="flex items-center">
<input <input
v-if="recipientDid && !givenToProject" v-if="recipientDid && !givenToProject"
v-model="givenToRecipient" v-model="givenToRecipient"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="flex-shrink-0 h-6 w-6 mr-2"
/> />
<font-awesome <font-awesome
v-else v-else
icon="square" icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm" class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
/> />
<label class="text-sm mt-1"> <label class="text-sm truncate">
{{ {{
recipientDid recipientDid
? "This was given to " + recipientName + "." ? "This was given to " + recipientName + "."
@@ -181,24 +188,24 @@
<font-awesome <font-awesome
v-if="!recipientDid || givenToProject" v-if="!recipientDid || givenToProject"
icon="info-circle" icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm" class="text-base cursor-pointer bg-white text-slate-500 ms-1"
@click="notifyUserOfRecipient()" @click="notifyUserOfRecipient()"
/> />
</div> </div>
<div class="flex"> <div class="flex items-center">
<input <input
v-if="fulfillsProjectId && !givenToRecipient" v-if="fulfillsProjectId && !givenToRecipient"
v-model="givenToProject" v-model="givenToProject"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="flex-shrink-0 h-6 w-6 mr-2"
/> />
<font-awesome <font-awesome
v-else v-else
icon="square" icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm" class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
/> />
<label class="text-sm mt-1"> <label class="text-sm truncate">
{{ {{
fulfillsProjectId fulfillsProjectId
? "This was given to " + fulfillsProjectName + ". " ? "This was given to " + fulfillsProjectName + ". "
@@ -208,7 +215,7 @@
<font-awesome <font-awesome
v-if="!fulfillsProjectId || givenToRecipient" v-if="!fulfillsProjectId || givenToRecipient"
icon="info-circle" icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm" class="text-base cursor-pointer bg-white text-slate-500 ms-1"
@click="notifyUserFulfillsProject()" @click="notifyUserFulfillsProject()"
/> />
</div> </div>
@@ -229,11 +236,11 @@
</router-link> </router-link>
</div> </div>
<p class="text-center mb-2 mt-6 italic"> <p class="text-center text-sm my-4">
Sign & Send to publish to the world <b class="font-medium">Sign &amp; Send</b> to publish to the world
<font-awesome <font-awesome
icon="circle-info" icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer" class="fa-fw text-blue-500 text-base cursor-pointer"
@click="explainData()" @click="explainData()"
/> />
</p> </p>
@@ -910,5 +917,10 @@ export default class GiftedDetails extends Vue {
7000, 7000,
); );
} }
// Computed property to get unit options
get unitOptions() {
return this.libsUtil.UNIT_SHORT;
}
} }
</script> </script>

View File

@@ -118,101 +118,73 @@ Raymer * @version 1.0.0 */
</div> </div>
<div v-else id="sectionRecordSomethingGiven"> <div v-else id="sectionRecordSomethingGiven">
<!-- !isCreatingIdentifier && isRegistered --> <!-- Record Quick-Action -->
<div class="mb-6">
<div class="flex gap-2 items-center mb-2">
<h2 class="text-xl font-bold">Record something given by:</h2>
<button
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
@click="openGiftedPrompts()"
>
<font-awesome
icon="lightbulb"
class="block text-center w-[1em]"
/>
</button>
</div>
<!-- show the actions for recognizing a give --> <div class="grid grid-cols-2 gap-2">
<div class="flex"> <button
<h2 class="text-xl font-bold">What have you seen someone do?</h2> type="button"
<button class="text-center text-base 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-3 py-2 rounded-lg"
class="ml-2 block text-xs text-center 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 rounded-md" @click="openDialogPerson()"
@click="openGiftedPrompts()" >
> <font-awesome icon="user" />
<font-awesome icon="lightbulb" class="fa-fw" /> Person
</button> </button>
<button
type="button"
class="text-center text-base 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-3 py-2 rounded-lg"
@click="openProjectDialog()"
>
<font-awesome icon="folder-open" />
Project
</button>
</div>
</div> </div>
<ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mt-4"
>
<li @click="openDialog()">
<img
src="../assets/blank-square.svg"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
Unnamed/Unknown
</h3>
</li>
<li v-if="allContacts.length === 0" class="text-sm">
(Add friends to see more people worthy of recognition.)
</li>
<li
v-for="contact in allContacts.slice(0, 6)"
:key="contact.did"
@click="openDialog(contact)"
>
<EntityIcon
:contact="contact"
:icon-size="64"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
{{ contact.name || contact.did }}
</h3>
</li>
<li>
<router-link
v-if="allContacts.length >= 6"
:to="{ name: 'contact-gift' }"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
>
... or someone else...
</router-link>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<GiftedDialog ref="customDialog" /> <GiftedDialog ref="customDialog" :show-projects="showProjectsDialog" />
<GiftedPrompts ref="giftedPrompts" /> <GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" /> <FeedFilters ref="feedFilters" />
<div class="relative">
<button
v-if="isRegistered"
class="absolute right-6 bottom-0 transform translate-y-1/2 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="openDialog()"
>
<font-awesome icon="plus" class="fa-fw" />
</button>
</div>
<!-- Results List --> <!-- Results List -->
<div class="mt-4 mb-4"> <div class="mt-4 mb-4">
<div class="flex items-center mb-4"> <div class="flex gap-2 items-center mb-3">
<h2 class="text-xl font-bold flex items-center gap-4"> <h2 class="text-xl font-bold">Latest Activity</h2>
Latest Activity <button
<button v-if="resultsAreFiltered()"
v-if="resultsAreFiltered()" class="block ms-auto text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white" @click="openFeedFilters()"
@click="openFeedFilters()" >
> <font-awesome
<font-awesome icon="filter" class="fa-fw" /> icon="filter"
</button> class="block text-center w-[1em] translate-y-[0.05em]"
<button />
v-else </button>
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white" <button
@click="openFeedFilters()" v-else
> class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
<font-awesome icon="filter" class="fa-fw" /> @click="openFeedFilters()"
</button> >
</h2> <font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button>
</div> </div>
<div <div
@@ -478,6 +450,7 @@ export default class HomeView extends Vue {
selectedImageData: Blob | null = null; selectedImageData: Blob | null = null;
isImageViewerOpen = false; isImageViewerOpen = false;
imageCache: Map<string, Blob | null> = new Map(); imageCache: Map<string, Blob | null> = new Map();
showProjectsDialog = false;
/** /**
* Initializes the component on mount * Initializes the component on mount
@@ -1626,17 +1599,33 @@ export default class HomeView extends Vue {
* @param giver Optional contact info for giver * @param giver Optional contact info for giver
* @param description Optional gift description * @param description Optional gift description
*/ */
openDialog(giver?: GiverReceiverInputInfo, description?: string) { openDialog(giver?: GiverReceiverInputInfo | "Unnamed", description?: string) {
(this.$refs.customDialog as GiftedDialog).open( if (giver === "Unnamed") {
giver, // Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
{ (this.$refs.customDialog as GiftedDialog).open(
did: this.activeDid, undefined,
name: "you", {
} as GiverReceiverInputInfo, did: this.activeDid,
undefined, name: "You",
"Given by " + (giver?.name || "someone not named"), } as GiverReceiverInputInfo,
description, undefined,
); "Given by Unnamed",
description,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.customDialog as GiftedDialog).selectGiver();
} else {
(this.$refs.customDialog as GiftedDialog).open(
giver,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
"Given by " + (giver?.name || "someone not named"),
description,
);
}
} }
/** /**
@@ -1870,5 +1859,18 @@ export default class HomeView extends Vue {
this.$router.push({ name: "contact-qr" }); this.$router.push({ name: "contact-qr" });
} }
} }
openDialogPerson(
giver?: GiverReceiverInputInfo | "Unnamed",
description?: string,
) {
this.showProjectsDialog = false;
this.openDialog(giver, description);
}
openProjectDialog() {
this.showProjectsDialog = true;
(this.$refs.customDialog as any).open();
}
} }
</script> </script>

View File

@@ -214,63 +214,11 @@
</div> </div>
</div> </div>
<div v-if="activeDid && isRegistered"> <GiftedDialog
<div class="text-center"> ref="giveDialogToThis"
<p class="mt-2 mt-4 text-center">Record a contribution from:</p> :to-project-id="projectId"
</div> :is-from-project-view="true"
<ul />
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5 mt-2"
>
<li @click="openGiftDialogToProject({ name: 'you', did: activeDid })">
<font-awesome
icon="hand"
class="fa-fw text-blue-500 text-5xl cursor-pointer"
/>
<h3
class="mt-5 text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
You
</h3>
</li>
<li @click="openGiftDialogToProject()">
<img
src="../assets/blank-square.svg"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
Unnamed/Unknown
</h3>
</li>
<li
v-for="contact in allContacts.slice(0, 5)"
:key="contact.did"
@click="openGiftDialogToProject(contact)"
>
<EntityIcon
:contact="contact"
:icon-size="64"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
{{ contact.name || "(no name)" }}
</h3>
</li>
<li>
<span
v-if="allContacts.length >= 5"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
@click="onClickAllContactsGifting()"
>
... or someone else...
</span>
</li>
</ul>
</div>
<GiftedDialog ref="giveDialogToThis" :to-project-id="projectId" />
<!-- Offers & Gifts to & from this --> <!-- Offers & Gifts to & from this -->
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4"> <div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
@@ -536,7 +484,12 @@
</button> </button>
</div> </div>
</div> </div>
<GiftedDialog ref="giveDialogFromThis" :from-project-id="projectId" /> <GiftedDialog
ref="giveDialogFromThis"
:from-project-id="projectId"
:show-projects="true"
:is-from-project-view="true"
/>
<h3 class="text-lg font-bold mb-3 mt-4"> <h3 class="text-lg font-bold mb-3 mt-4">
Benefitted From This Project Benefitted From This Project
@@ -1270,21 +1223,52 @@ export default class ProjectViewView extends Vue {
); );
} }
openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) { openGiftDialogToProject(
(this.$refs.giveDialogToThis as GiftedDialog).open( contact?: libsUtil.GiverReceiverInputInfo | "Unnamed",
contact, ) {
undefined, if (contact === "Unnamed") {
undefined, // Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
(contact?.name || "Someone not named") + ` gave to this project`, (this.$refs.giveDialogToThis as GiftedDialog).open(
); undefined,
undefined,
undefined,
"Given by Unnamed to this project",
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.giveDialogToThis as GiftedDialog).selectGiver();
} else {
// Open straight to Step 2 with current user as giver and current project as recipient
(this.$refs.giveDialogToThis as GiftedDialog).open(
{
did: this.activeDid,
name: "You",
},
{
did: this.issuer,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
undefined,
`Given to ${this.name}`,
);
}
} }
openGiftDialogFromProject() { openGiftDialogFromProject() {
// Set the project as giver and the current user as recipient
(this.$refs.giveDialogFromThis as GiftedDialog).open( (this.$refs.giveDialogFromThis as GiftedDialog).open(
undefined, {
did: undefined,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
{ did: this.activeDid, name: "You" }, { did: this.activeDid, name: "You" },
undefined, undefined,
`This project gave to you`, `${this.name} gave to you`,
undefined,
undefined,
); );
} }