Compare commits
32 Commits
ab784dca76
...
c3851371c0
Author | SHA1 | Date |
---|---|---|
|
c3851371c0 | 2 days ago |
|
b388dc3d8e | 2 days ago |
|
966dbc5164 | 2 days ago |
|
25512d3db1 | 7 days ago |
|
bf9fee7ee9 | 1 week ago |
|
08c46a27d3 | 1 week ago |
|
c9405839c3 | 1 week ago |
|
0e6a9c4f89 | 2 weeks ago |
|
b6278ca148 | 2 weeks ago |
|
d8e237f8cb | 2 weeks ago |
|
4b539ccc55 | 2 weeks ago |
|
ea49173885 | 2 weeks ago |
|
447a7cb089 | 2 weeks ago |
|
c0ddba8898 | 2 weeks ago |
|
fe4ae90849 | 2 weeks ago |
|
ce04312baa | 2 weeks ago |
|
a8cc480960 | 2 weeks ago |
|
357822d713 | 2 weeks ago |
|
ca22161f12 | 2 weeks ago |
|
d3b80fbe47 | 2 weeks ago |
|
0342c872f4 | 2 weeks ago |
|
a7e65b3b49 | 2 weeks ago |
|
eb7605991c | 3 weeks ago |
|
fa21660fd1 | 3 weeks ago |
|
df1c1f0186 | 3 weeks ago |
|
3daf1c8a5c | 3 weeks ago |
|
7eefee1ea5 | 3 weeks ago |
|
140c36a416 | 4 weeks ago |
|
988244b7ae | 2 months ago |
|
4b355a5448 | 2 months ago |
|
b511f9cd24 | 2 months ago |
|
579cecbe6e | 2 months ago |
18 changed files with 2575 additions and 363 deletions
@ -0,0 +1,226 @@ |
|||||
|
/** * AmountInput.vue - Specialized amount input with increment/decrement |
||||
|
controls * * Extracted from GiftedDialog.vue to handle numeric amount input * |
||||
|
with increment/decrement buttons and validation. * * @author Matthew Raymer */ |
||||
|
<template> |
||||
|
<div class="flex flex-grow"> |
||||
|
<button |
||||
|
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2" |
||||
|
:disabled="isAtMinimum" |
||||
|
type="button" |
||||
|
@click.prevent="decrement" |
||||
|
> |
||||
|
<font-awesome icon="chevron-left" /> |
||||
|
</button> |
||||
|
|
||||
|
<input |
||||
|
:id="inputId" |
||||
|
v-model="displayValue" |
||||
|
type="number" |
||||
|
:min="min" |
||||
|
:max="max" |
||||
|
:step="step" |
||||
|
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]" |
||||
|
@input="handleInput" |
||||
|
@blur="handleBlur" |
||||
|
/> |
||||
|
|
||||
|
<button |
||||
|
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2" |
||||
|
:disabled="isAtMaximum" |
||||
|
type="button" |
||||
|
@click.prevent="increment" |
||||
|
> |
||||
|
<font-awesome icon="chevron-right" /> |
||||
|
</button> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue, Watch, Emit } from "vue-facing-decorator"; |
||||
|
import { logger } from "@/utils/logger"; |
||||
|
|
||||
|
/** |
||||
|
* AmountInput - Numeric input with increment/decrement controls |
||||
|
* |
||||
|
* Features: |
||||
|
* - Increment/decrement buttons with validation |
||||
|
* - Configurable min/max values and step size |
||||
|
* - Input validation and formatting |
||||
|
* - Disabled state handling for boundary values |
||||
|
* - Emits update events for v-model compatibility |
||||
|
*/ |
||||
|
@Component |
||||
|
export default class AmountInput extends Vue { |
||||
|
/** Current numeric value */ |
||||
|
@Prop({ required: true }) |
||||
|
value!: number; |
||||
|
|
||||
|
/** Minimum allowed value */ |
||||
|
@Prop({ default: 0 }) |
||||
|
min!: number; |
||||
|
|
||||
|
/** Maximum allowed value */ |
||||
|
@Prop({ default: Number.MAX_SAFE_INTEGER }) |
||||
|
max!: number; |
||||
|
|
||||
|
/** Step size for increment/decrement */ |
||||
|
@Prop({ default: 1 }) |
||||
|
step!: number; |
||||
|
|
||||
|
/** Input element ID for accessibility */ |
||||
|
@Prop({ default: "amount-input" }) |
||||
|
inputId!: string; |
||||
|
|
||||
|
/** Internal display value for input field */ |
||||
|
private displayValue: string = "0"; |
||||
|
|
||||
|
/** |
||||
|
* Initialize display value from prop |
||||
|
*/ |
||||
|
mounted(): void { |
||||
|
logger.debug("[AmountInput] mounted()", { |
||||
|
value: this.value, |
||||
|
min: this.min, |
||||
|
max: this.max, |
||||
|
step: this.step, |
||||
|
}); |
||||
|
this.displayValue = this.value.toString(); |
||||
|
logger.debug("[AmountInput] mounted() - displayValue set", { |
||||
|
displayValue: this.displayValue, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Watch for external value changes |
||||
|
*/ |
||||
|
@Watch("value") |
||||
|
onValueChange(newValue: number): void { |
||||
|
this.displayValue = newValue.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if current value is at minimum |
||||
|
*/ |
||||
|
get isAtMinimum(): boolean { |
||||
|
const result = this.value <= this.min; |
||||
|
logger.debug("[AmountInput] isAtMinimum", { |
||||
|
value: this.value, |
||||
|
min: this.min, |
||||
|
result, |
||||
|
}); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if current value is at maximum |
||||
|
*/ |
||||
|
get isAtMaximum(): boolean { |
||||
|
const result = this.value >= this.max; |
||||
|
logger.debug("[AmountInput] isAtMaximum", { |
||||
|
value: this.value, |
||||
|
max: this.max, |
||||
|
result, |
||||
|
}); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Increment the value by step size |
||||
|
*/ |
||||
|
increment(): void { |
||||
|
logger.debug("[AmountInput] increment() called", { |
||||
|
currentValue: this.value, |
||||
|
step: this.step, |
||||
|
}); |
||||
|
const newValue = Math.min(this.value + this.step, this.max); |
||||
|
logger.debug("[AmountInput] increment() calculated newValue", { |
||||
|
newValue, |
||||
|
}); |
||||
|
this.updateValue(newValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Decrement the value by step size |
||||
|
*/ |
||||
|
decrement(): void { |
||||
|
logger.debug("[AmountInput] decrement() called", { |
||||
|
currentValue: this.value, |
||||
|
step: this.step, |
||||
|
}); |
||||
|
const newValue = Math.max(this.value - this.step, this.min); |
||||
|
logger.debug("[AmountInput] decrement() calculated newValue", { |
||||
|
newValue, |
||||
|
}); |
||||
|
this.updateValue(newValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle direct input changes |
||||
|
*/ |
||||
|
handleInput(): void { |
||||
|
const numericValue = parseFloat(this.displayValue); |
||||
|
if (!isNaN(numericValue)) { |
||||
|
const clampedValue = Math.max(this.min, Math.min(numericValue, this.max)); |
||||
|
this.updateValue(clampedValue); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle input blur - ensure display value matches actual value |
||||
|
*/ |
||||
|
handleBlur(): void { |
||||
|
this.displayValue = this.value.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Update the value and emit change event |
||||
|
*/ |
||||
|
private updateValue(newValue: number): void { |
||||
|
logger.debug("[AmountInput] updateValue() called", { |
||||
|
oldValue: this.value, |
||||
|
newValue, |
||||
|
}); |
||||
|
if (newValue !== this.value) { |
||||
|
logger.debug( |
||||
|
"[AmountInput] updateValue() - values different, updating and emitting", |
||||
|
); |
||||
|
this.displayValue = newValue.toString(); |
||||
|
this.emitUpdateValue(newValue); |
||||
|
} else { |
||||
|
logger.debug( |
||||
|
"[AmountInput] updateValue() - values same, skipping update", |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Emit update:value event |
||||
|
*/ |
||||
|
@Emit("update:value") |
||||
|
emitUpdateValue(value: number): number { |
||||
|
logger.debug("[AmountInput] emitUpdateValue() - emitting value", { |
||||
|
value, |
||||
|
}); |
||||
|
return value; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Remove spinner arrows from number input */ |
||||
|
input[type="number"]::-webkit-outer-spin-button, |
||||
|
input[type="number"]::-webkit-inner-spin-button { |
||||
|
-webkit-appearance: none; |
||||
|
margin: 0; |
||||
|
} |
||||
|
|
||||
|
input[type="number"] { |
||||
|
-moz-appearance: textfield; |
||||
|
} |
||||
|
|
||||
|
/* Disabled button styles */ |
||||
|
button:disabled { |
||||
|
opacity: 0.5; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,264 @@ |
|||||
|
/** * EntityGrid.vue - Unified entity grid layout component * * Extracted from |
||||
|
GiftedDialog.vue to provide a reusable grid layout * for displaying people, |
||||
|
projects, and special entities with selection. * * @author Matthew Raymer */ |
||||
|
<template> |
||||
|
<ul :class="gridClasses"> |
||||
|
<!-- Special entities (You, Unnamed) for people grids --> |
||||
|
<template v-if="entityType === 'people'"> |
||||
|
<!-- "You" entity --> |
||||
|
<SpecialEntityCard |
||||
|
v-if="showYouEntity" |
||||
|
entity-type="you" |
||||
|
label="You" |
||||
|
icon="hand" |
||||
|
:selectable="youSelectable" |
||||
|
:conflicted="youConflicted" |
||||
|
:entity-data="youEntityData" |
||||
|
@entity-selected="handleEntitySelected" |
||||
|
/> |
||||
|
|
||||
|
<!-- "Unnamed" entity --> |
||||
|
<SpecialEntityCard |
||||
|
entity-type="unnamed" |
||||
|
label="Unnamed" |
||||
|
icon="circle-question" |
||||
|
:entity-data="unnamedEntityData" |
||||
|
@entity-selected="handleEntitySelected" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<!-- Empty state message --> |
||||
|
<li |
||||
|
v-if="entities.length === 0" |
||||
|
class="text-xs text-slate-500 italic col-span-full" |
||||
|
> |
||||
|
{{ emptyStateMessage }} |
||||
|
</li> |
||||
|
|
||||
|
<!-- Entity cards (people or projects) --> |
||||
|
<template v-if="entityType === 'people'"> |
||||
|
<PersonCard |
||||
|
v-for="person in displayedEntities" |
||||
|
:key="person.did" |
||||
|
:person="person" |
||||
|
:conflicted="isPersonConflicted(person.did)" |
||||
|
:show-time-icon="true" |
||||
|
@person-selected="handlePersonSelected" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<template v-else-if="entityType === 'projects'"> |
||||
|
<ProjectCard |
||||
|
v-for="project in displayedEntities" |
||||
|
:key="project.handleId" |
||||
|
:project="project" |
||||
|
:active-did="activeDid" |
||||
|
:all-my-dids="allMyDids" |
||||
|
:all-contacts="allContacts" |
||||
|
@project-selected="handleProjectSelected" |
||||
|
/> |
||||
|
</template> |
||||
|
|
||||
|
<!-- Show All navigation --> |
||||
|
<ShowAllCard |
||||
|
v-if="shouldShowAll" |
||||
|
:entity-type="entityType" |
||||
|
:route-name="showAllRoute" |
||||
|
:query-params="showAllQueryParams" |
||||
|
/> |
||||
|
</ul> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue, Emit } from "vue-facing-decorator"; |
||||
|
import PersonCard from "./PersonCard.vue"; |
||||
|
import ProjectCard from "./ProjectCard.vue"; |
||||
|
import SpecialEntityCard from "./SpecialEntityCard.vue"; |
||||
|
import ShowAllCard from "./ShowAllCard.vue"; |
||||
|
import { Contact } from "../db/tables/contacts"; |
||||
|
import { PlanData } from "../interfaces/records"; |
||||
|
|
||||
|
/** |
||||
|
* EntityGrid - Unified grid layout for displaying people or projects |
||||
|
* |
||||
|
* Features: |
||||
|
* - Responsive grid layout for people/projects |
||||
|
* - Special entity integration (You, Unnamed) |
||||
|
* - Conflict detection integration |
||||
|
* - Empty state messaging |
||||
|
* - Show All navigation |
||||
|
* - Event delegation for entity selection |
||||
|
*/ |
||||
|
@Component({ |
||||
|
components: { |
||||
|
PersonCard, |
||||
|
ProjectCard, |
||||
|
SpecialEntityCard, |
||||
|
ShowAllCard, |
||||
|
}, |
||||
|
}) |
||||
|
export default class EntityGrid extends Vue { |
||||
|
/** Type of entities to display */ |
||||
|
@Prop({ required: true }) |
||||
|
entityType!: "people" | "projects"; |
||||
|
|
||||
|
/** Array of entities to display */ |
||||
|
@Prop({ required: true }) |
||||
|
entities!: Contact[] | PlanData[]; |
||||
|
|
||||
|
/** Maximum number of entities to display */ |
||||
|
@Prop({ default: 10 }) |
||||
|
maxItems!: number; |
||||
|
|
||||
|
/** Active user's DID */ |
||||
|
@Prop({ required: true }) |
||||
|
activeDid!: string; |
||||
|
|
||||
|
/** All user's DIDs */ |
||||
|
@Prop({ required: true }) |
||||
|
allMyDids!: string[]; |
||||
|
|
||||
|
/** All contacts */ |
||||
|
@Prop({ required: true }) |
||||
|
allContacts!: Contact[]; |
||||
|
|
||||
|
/** Function to check if a person DID would create a conflict */ |
||||
|
@Prop({ required: true }) |
||||
|
conflictChecker!: (did: string) => boolean; |
||||
|
|
||||
|
/** Whether to show the "You" entity for people grids */ |
||||
|
@Prop({ default: true }) |
||||
|
showYouEntity!: boolean; |
||||
|
|
||||
|
/** Whether the "You" entity is selectable */ |
||||
|
@Prop({ default: true }) |
||||
|
youSelectable!: boolean; |
||||
|
|
||||
|
/** Route name for "Show All" navigation */ |
||||
|
@Prop({ default: "" }) |
||||
|
showAllRoute!: string; |
||||
|
|
||||
|
/** Query parameters for "Show All" navigation */ |
||||
|
@Prop({ default: () => ({}) }) |
||||
|
showAllQueryParams!: Record<string, string>; |
||||
|
|
||||
|
/** |
||||
|
* Computed CSS classes for the grid layout |
||||
|
*/ |
||||
|
get gridClasses(): string { |
||||
|
const baseClasses = "grid gap-x-2 gap-y-4 text-center mb-4"; |
||||
|
|
||||
|
if (this.entityType === "projects") { |
||||
|
return `${baseClasses} grid-cols-3 md:grid-cols-4`; |
||||
|
} else { |
||||
|
return `${baseClasses} grid-cols-4 sm:grid-cols-5 md:grid-cols-6`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computed entities to display (limited by maxItems) |
||||
|
*/ |
||||
|
get displayedEntities(): Contact[] | PlanData[] { |
||||
|
const maxDisplay = this.entityType === "projects" ? 7 : this.maxItems; |
||||
|
return this.entities.slice(0, maxDisplay); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computed empty state message based on entity type |
||||
|
*/ |
||||
|
get emptyStateMessage(): string { |
||||
|
if (this.entityType === "projects") { |
||||
|
return "(No projects found.)"; |
||||
|
} else { |
||||
|
return "(Add friends to see more people worthy of recognition.)"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Whether to show the "Show All" navigation |
||||
|
*/ |
||||
|
get shouldShowAll(): boolean { |
||||
|
return this.entities.length > 0 && this.showAllRoute !== ""; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Whether the "You" entity is conflicted |
||||
|
*/ |
||||
|
get youConflicted(): boolean { |
||||
|
return this.conflictChecker(this.activeDid); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Entity data for the "You" special entity |
||||
|
*/ |
||||
|
get youEntityData(): { did: string; name: string } { |
||||
|
return { |
||||
|
did: this.activeDid, |
||||
|
name: "You", |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Entity data for the "Unnamed" special entity |
||||
|
*/ |
||||
|
get unnamedEntityData(): { did: string; name: string } { |
||||
|
return { |
||||
|
did: "", |
||||
|
name: "Unnamed", |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if a person DID is conflicted |
||||
|
*/ |
||||
|
isPersonConflicted(did: string): boolean { |
||||
|
return this.conflictChecker(did); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle person selection from PersonCard |
||||
|
*/ |
||||
|
handlePersonSelected(person: Contact): void { |
||||
|
this.emitEntitySelected({ |
||||
|
type: "person", |
||||
|
data: person, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle project selection from ProjectCard |
||||
|
*/ |
||||
|
handleProjectSelected(project: PlanData): void { |
||||
|
this.emitEntitySelected({ |
||||
|
type: "project", |
||||
|
data: project, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle special entity selection from SpecialEntityCard |
||||
|
*/ |
||||
|
handleEntitySelected(event: { |
||||
|
type: string; |
||||
|
entityType: string; |
||||
|
data: any; |
||||
|
}): void { |
||||
|
this.emitEntitySelected({ |
||||
|
type: "special", |
||||
|
entityType: event.entityType, |
||||
|
data: event.data, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Emit methods using @Emit decorator |
||||
|
|
||||
|
@Emit("entity-selected") |
||||
|
emitEntitySelected(data: any): any { |
||||
|
return data; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Grid-specific styles if needed */ |
||||
|
</style> |
@ -0,0 +1,254 @@ |
|||||
|
/** * EntitySelectionStep.vue - Entity selection step component * * Extracted |
||||
|
from GiftedDialog.vue to handle the complete step 1 * entity selection interface |
||||
|
with dynamic labeling and grid display. * * @author Matthew Raymer */ |
||||
|
<template> |
||||
|
<div id="sectionGiftedGiver"> |
||||
|
<label class="block font-bold mb-4"> |
||||
|
{{ stepLabel }} |
||||
|
</label> |
||||
|
|
||||
|
<EntityGrid |
||||
|
:entity-type="shouldShowProjects ? 'projects' : 'people'" |
||||
|
:entities="shouldShowProjects ? projects : allContacts" |
||||
|
:max-items="10" |
||||
|
:active-did="activeDid" |
||||
|
:all-my-dids="allMyDids" |
||||
|
:all-contacts="allContacts" |
||||
|
:conflict-checker="conflictChecker" |
||||
|
:show-you-entity="shouldShowYouEntity" |
||||
|
:you-selectable="youSelectable" |
||||
|
:show-all-route="showAllRoute" |
||||
|
:show-all-query-params="showAllQueryParams" |
||||
|
@entity-selected="handleEntitySelected" |
||||
|
/> |
||||
|
|
||||
|
<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="handleCancel" |
||||
|
> |
||||
|
Cancel |
||||
|
</button> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue, Emit } from "vue-facing-decorator"; |
||||
|
import EntityGrid from "./EntityGrid.vue"; |
||||
|
import { Contact } from "../db/tables/contacts"; |
||||
|
import { PlanData } from "../interfaces/records"; |
||||
|
|
||||
|
/** |
||||
|
* Entity data interface for giver/receiver |
||||
|
*/ |
||||
|
interface EntityData { |
||||
|
did?: string; |
||||
|
handleId?: string; |
||||
|
name?: string; |
||||
|
image?: string; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Entity selection event data structure |
||||
|
*/ |
||||
|
interface EntitySelectionEvent { |
||||
|
type: "person" | "project" | "special"; |
||||
|
entityType?: string; |
||||
|
data: Contact | PlanData | EntityData; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* EntitySelectionStep - Complete step 1 entity selection interface |
||||
|
* |
||||
|
* Features: |
||||
|
* - Dynamic step labeling based on context |
||||
|
* - EntityGrid integration for unified entity display |
||||
|
* - Conflict detection and prevention |
||||
|
* - Special entity handling (You, Unnamed) |
||||
|
* - Show All navigation with context preservation |
||||
|
* - Cancel functionality |
||||
|
* - Event delegation for entity selection |
||||
|
*/ |
||||
|
@Component({ |
||||
|
components: { |
||||
|
EntityGrid, |
||||
|
}, |
||||
|
}) |
||||
|
export default class EntitySelectionStep extends Vue { |
||||
|
/** Type of step: 'giver' or 'recipient' */ |
||||
|
@Prop({ required: true }) |
||||
|
stepType!: "giver" | "recipient"; |
||||
|
|
||||
|
/** Type of giver entity: 'person' or 'project' */ |
||||
|
@Prop({ required: true }) |
||||
|
giverEntityType!: "person" | "project"; |
||||
|
|
||||
|
/** Type of recipient entity: 'person' or 'project' */ |
||||
|
@Prop({ required: true }) |
||||
|
recipientEntityType!: "person" | "project"; |
||||
|
|
||||
|
/** Whether to show projects instead of people */ |
||||
|
@Prop({ default: false }) |
||||
|
showProjects!: boolean; |
||||
|
|
||||
|
/** Whether this is from a project view */ |
||||
|
@Prop({ default: false }) |
||||
|
isFromProjectView!: boolean; |
||||
|
|
||||
|
/** Array of available projects */ |
||||
|
@Prop({ required: true }) |
||||
|
projects!: PlanData[]; |
||||
|
|
||||
|
/** Array of available contacts */ |
||||
|
@Prop({ required: true }) |
||||
|
allContacts!: Contact[]; |
||||
|
|
||||
|
/** Active user's DID */ |
||||
|
@Prop({ required: true }) |
||||
|
activeDid!: string; |
||||
|
|
||||
|
/** All user's DIDs */ |
||||
|
@Prop({ required: true }) |
||||
|
allMyDids!: string[]; |
||||
|
|
||||
|
/** Function to check if a DID would create a conflict */ |
||||
|
@Prop({ required: true }) |
||||
|
conflictChecker!: (did: string) => boolean; |
||||
|
|
||||
|
/** Project ID for context (giver) */ |
||||
|
@Prop({ default: "" }) |
||||
|
fromProjectId!: string; |
||||
|
|
||||
|
/** Project ID for context (recipient) */ |
||||
|
@Prop({ default: "" }) |
||||
|
toProjectId!: string; |
||||
|
|
||||
|
/** Current giver entity for context */ |
||||
|
@Prop() |
||||
|
giver?: EntityData | null; |
||||
|
|
||||
|
/** Current receiver entity for context */ |
||||
|
@Prop() |
||||
|
receiver?: EntityData | null; |
||||
|
|
||||
|
/** |
||||
|
* Computed step label based on context |
||||
|
*/ |
||||
|
get stepLabel(): string { |
||||
|
if (this.stepType === "recipient") { |
||||
|
return "Choose who received the gift:"; |
||||
|
} else if (this.showProjects) { |
||||
|
return "Choose a project benefitted from:"; |
||||
|
} else { |
||||
|
return "Choose a person received from:"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Whether to show projects in the grid |
||||
|
*/ |
||||
|
get shouldShowProjects(): boolean { |
||||
|
return ( |
||||
|
(this.stepType === "giver" && this.giverEntityType === "project") || |
||||
|
(this.stepType === "recipient" && this.recipientEntityType === "project") |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Whether to show the "You" entity |
||||
|
*/ |
||||
|
get shouldShowYouEntity(): boolean { |
||||
|
return ( |
||||
|
this.stepType === "recipient" || |
||||
|
(this.stepType === "giver" && this.isFromProjectView) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Whether the "You" entity is selectable |
||||
|
*/ |
||||
|
get youSelectable(): boolean { |
||||
|
return !this.conflictChecker(this.activeDid); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Route name for "Show All" navigation |
||||
|
*/ |
||||
|
get showAllRoute(): string { |
||||
|
if (this.shouldShowProjects) { |
||||
|
return "discover"; |
||||
|
} else if (this.allContacts.length > 0) { |
||||
|
return "contact-gift"; |
||||
|
} |
||||
|
return ""; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Query parameters for "Show All" navigation |
||||
|
*/ |
||||
|
get showAllQueryParams(): Record<string, string> { |
||||
|
if (this.shouldShowProjects) { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
stepType: this.stepType, |
||||
|
giverEntityType: this.giverEntityType, |
||||
|
recipientEntityType: this.recipientEntityType, |
||||
|
...(this.stepType === "giver" |
||||
|
? { |
||||
|
recipientProjectId: this.toProjectId || "", |
||||
|
recipientProjectName: this.receiver?.name || "", |
||||
|
recipientProjectImage: this.receiver?.image || "", |
||||
|
recipientProjectHandleId: this.receiver?.handleId || "", |
||||
|
recipientDid: this.receiver?.did || "", |
||||
|
} |
||||
|
: { |
||||
|
giverProjectId: this.fromProjectId || "", |
||||
|
giverProjectName: this.giver?.name || "", |
||||
|
giverProjectImage: this.giver?.image || "", |
||||
|
giverProjectHandleId: this.giver?.handleId || "", |
||||
|
giverDid: this.giver?.did || "", |
||||
|
}), |
||||
|
fromProjectId: this.fromProjectId, |
||||
|
toProjectId: this.toProjectId, |
||||
|
showProjects: this.showProjects.toString(), |
||||
|
isFromProjectView: this.isFromProjectView.toString(), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle entity selection from EntityGrid |
||||
|
*/ |
||||
|
handleEntitySelected(event: EntitySelectionEvent): void { |
||||
|
this.emitEntitySelected({ |
||||
|
stepType: this.stepType, |
||||
|
...event, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle cancel button click |
||||
|
*/ |
||||
|
handleCancel(): void { |
||||
|
this.emitCancel(); |
||||
|
} |
||||
|
|
||||
|
// Emit methods using @Emit decorator |
||||
|
|
||||
|
@Emit("entity-selected") |
||||
|
emitEntitySelected( |
||||
|
data: EntitySelectionEvent & { stepType: string }, |
||||
|
): EntitySelectionEvent & { stepType: string } { |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
@Emit("cancel") |
||||
|
emitCancel(): void { |
||||
|
// No return value needed |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Component-specific styles if needed */ |
||||
|
</style> |
@ -0,0 +1,145 @@ |
|||||
|
/** * EntitySummaryButton.vue - Displays selected entity with edit capability * |
||||
|
* Extracted from GiftedDialog.vue to handle entity summary display * in the gift |
||||
|
details step with edit functionality. * * @author Matthew Raymer */ |
||||
|
<template> |
||||
|
<component |
||||
|
:is="editable ? 'button' : 'div'" |
||||
|
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2" |
||||
|
@click="handleClick" |
||||
|
> |
||||
|
<!-- Entity Icon/Avatar --> |
||||
|
<div> |
||||
|
<template v-if="entityType === 'project'"> |
||||
|
<ProjectIcon |
||||
|
v-if="entity?.handleId" |
||||
|
:entity-id="entity.handleId" |
||||
|
:icon-size="32" |
||||
|
:image-url="entity.image" |
||||
|
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover" |
||||
|
/> |
||||
|
</template> |
||||
|
<template v-else> |
||||
|
<EntityIcon |
||||
|
v-if="entity?.did" |
||||
|
:contact="entity" |
||||
|
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> |
||||
|
|
||||
|
<!-- Entity Information --> |
||||
|
<div class="text-start min-w-0"> |
||||
|
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase"> |
||||
|
{{ label }} |
||||
|
</p> |
||||
|
<h3 class="font-semibold truncate"> |
||||
|
{{ entity?.name || "Unnamed" }} |
||||
|
</h3> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Edit/Lock Icon --> |
||||
|
<p class="ms-auto text-sm pe-1" :class="iconClasses"> |
||||
|
<font-awesome |
||||
|
:icon="editable ? 'pen' : 'lock'" |
||||
|
:title="editable ? 'Change' : 'Can\'t be changed'" |
||||
|
/> |
||||
|
</p> |
||||
|
</component> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue, Emit } from "vue-facing-decorator"; |
||||
|
import EntityIcon from "./EntityIcon.vue"; |
||||
|
import ProjectIcon from "./ProjectIcon.vue"; |
||||
|
import { Contact } from "../db/tables/contacts"; |
||||
|
|
||||
|
/** |
||||
|
* Entity interface for both person and project entities |
||||
|
*/ |
||||
|
interface EntityData { |
||||
|
did?: string; |
||||
|
handleId?: string; |
||||
|
name?: string; |
||||
|
image?: string; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* EntitySummaryButton - Displays selected entity with optional edit capability |
||||
|
* |
||||
|
* Features: |
||||
|
* - Shows entity avatar (person or project) |
||||
|
* - Displays entity name and role label |
||||
|
* - Handles editable vs locked states |
||||
|
* - Emits edit events when clicked and editable |
||||
|
* - Supports both person and project entity types |
||||
|
*/ |
||||
|
@Component({ |
||||
|
components: { |
||||
|
EntityIcon, |
||||
|
ProjectIcon, |
||||
|
}, |
||||
|
}) |
||||
|
export default class EntitySummaryButton extends Vue { |
||||
|
/** Entity data to display */ |
||||
|
@Prop({ required: true }) |
||||
|
entity!: EntityData | Contact | null; |
||||
|
|
||||
|
/** Type of entity: 'person' or 'project' */ |
||||
|
@Prop({ required: true }) |
||||
|
entityType!: "person" | "project"; |
||||
|
|
||||
|
/** Display label for the entity role */ |
||||
|
@Prop({ required: true }) |
||||
|
label!: string; |
||||
|
|
||||
|
/** Whether the entity can be edited */ |
||||
|
@Prop({ default: true }) |
||||
|
editable!: boolean; |
||||
|
|
||||
|
/** |
||||
|
* Computed CSS classes for the edit/lock icon |
||||
|
*/ |
||||
|
get iconClasses(): string { |
||||
|
return this.editable ? "text-blue-500" : "text-slate-400"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle click event - only emit if editable |
||||
|
*/ |
||||
|
handleClick(): void { |
||||
|
if (this.editable) { |
||||
|
this.emitEditRequested({ |
||||
|
entityType: this.entityType, |
||||
|
entity: this.entity, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Emit methods using @Emit decorator |
||||
|
|
||||
|
@Emit("edit-requested") |
||||
|
emitEditRequested(data: any): any { |
||||
|
return data; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Ensure button styling is consistent */ |
||||
|
button { |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
button:hover { |
||||
|
background-color: #f1f5f9; /* hover:bg-slate-100 */ |
||||
|
} |
||||
|
|
||||
|
div { |
||||
|
cursor: default; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,416 @@ |
|||||
|
/** * GiftDetailsStep.vue - Gift details step component * * Extracted from |
||||
|
GiftedDialog.vue to handle the complete step 2 * gift details form interface |
||||
|
with entity summaries and validation. * * @author Matthew Raymer */ |
||||
|
<template> |
||||
|
<div id="sectionGiftedGift"> |
||||
|
<!-- Entity Summary Buttons --> |
||||
|
<div class="grid grid-cols-2 gap-2 mb-4"> |
||||
|
<!-- Giver Button --> |
||||
|
<EntitySummaryButton |
||||
|
:entity="giver" |
||||
|
:entity-type="giverEntityType" |
||||
|
:label="giverLabel" |
||||
|
:editable="canEditGiver" |
||||
|
@edit-requested="handleEditGiver" |
||||
|
/> |
||||
|
|
||||
|
<!-- Recipient Button --> |
||||
|
<EntitySummaryButton |
||||
|
:entity="receiver" |
||||
|
:entity-type="recipientEntityType" |
||||
|
:label="recipientLabel" |
||||
|
:editable="canEditRecipient" |
||||
|
@edit-requested="handleEditRecipient" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Gift Description Input --> |
||||
|
<input |
||||
|
v-model="localDescription" |
||||
|
type="text" |
||||
|
class="block w-full rounded border border-slate-400 px-3 py-2 mb-4 placeholder:italic" |
||||
|
:placeholder="prompt || 'What was given?'" |
||||
|
@input="handleDescriptionChange" |
||||
|
/> |
||||
|
|
||||
|
<!-- Amount Input and Unit Selection --> |
||||
|
<div class="flex mb-4"> |
||||
|
<AmountInput |
||||
|
:value="localAmount" |
||||
|
:min="0" |
||||
|
input-id="inputGivenAmount" |
||||
|
@update:value="handleAmountChange" |
||||
|
/> |
||||
|
|
||||
|
<select |
||||
|
v-model="localUnitCode" |
||||
|
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2" |
||||
|
@change="handleUnitCodeChange" |
||||
|
> |
||||
|
<option value="HUR">Hours</option> |
||||
|
<option value="USD">US $</option> |
||||
|
<option value="BTC">BTC</option> |
||||
|
<option value="BX">BX</option> |
||||
|
<option value="ETH">ETH</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Photo & More Options Link --> |
||||
|
<router-link |
||||
|
:to="photoOptionsRoute" |
||||
|
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 & more options… |
||||
|
</router-link> |
||||
|
|
||||
|
<!-- Sign & Send Info --> |
||||
|
<p class="text-center text-sm mb-4"> |
||||
|
<b class="font-medium">Sign & Send</b> to publish to the world |
||||
|
<font-awesome |
||||
|
icon="circle-info" |
||||
|
class="fa-fw text-blue-500 text-base cursor-pointer" |
||||
|
@click="handleExplainData" |
||||
|
/> |
||||
|
</p> |
||||
|
|
||||
|
<!-- Conflict Warning --> |
||||
|
<div |
||||
|
v-if="hasConflict" |
||||
|
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> |
||||
|
|
||||
|
<!-- Action Buttons --> |
||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> |
||||
|
<button |
||||
|
:disabled="hasConflict" |
||||
|
:class="submitButtonClasses" |
||||
|
@click="handleSubmit" |
||||
|
> |
||||
|
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-lg" |
||||
|
@click="handleCancel" |
||||
|
> |
||||
|
Cancel |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue, Watch, Emit } from "vue-facing-decorator"; |
||||
|
import EntitySummaryButton from "./EntitySummaryButton.vue"; |
||||
|
import AmountInput from "./AmountInput.vue"; |
||||
|
import { RouteLocationRaw } from "vue-router"; |
||||
|
import { logger } from "@/utils/logger"; |
||||
|
|
||||
|
/** |
||||
|
* Entity data interface for giver/receiver |
||||
|
*/ |
||||
|
interface EntityData { |
||||
|
did?: string; |
||||
|
handleId?: string; |
||||
|
name?: string; |
||||
|
image?: string; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* GiftDetailsStep - Complete step 2 gift details form interface |
||||
|
* |
||||
|
* Features: |
||||
|
* - Entity summary display with edit capability |
||||
|
* - Gift description input with placeholder support |
||||
|
* - Amount input with increment/decrement controls |
||||
|
* - Unit code selection (HUR, USD, BTC, etc.) |
||||
|
* - Photo & more options navigation |
||||
|
* - Conflict detection and warning display |
||||
|
* - Form validation and submission |
||||
|
* - Cancel functionality |
||||
|
*/ |
||||
|
@Component({ |
||||
|
components: { |
||||
|
EntitySummaryButton, |
||||
|
AmountInput, |
||||
|
}, |
||||
|
}) |
||||
|
export default class GiftDetailsStep extends Vue { |
||||
|
/** Giver entity data */ |
||||
|
@Prop({ required: true }) |
||||
|
giver!: EntityData | null; |
||||
|
|
||||
|
/** Receiver entity data */ |
||||
|
@Prop({ required: true }) |
||||
|
receiver!: EntityData | null; |
||||
|
|
||||
|
/** Type of giver entity: 'person' or 'project' */ |
||||
|
@Prop({ required: true }) |
||||
|
giverEntityType!: "person" | "project"; |
||||
|
|
||||
|
/** Type of recipient entity: 'person' or 'project' */ |
||||
|
@Prop({ required: true }) |
||||
|
recipientEntityType!: "person" | "project"; |
||||
|
|
||||
|
/** Gift description */ |
||||
|
@Prop({ default: "" }) |
||||
|
description!: string; |
||||
|
|
||||
|
/** Gift amount */ |
||||
|
@Prop({ default: 0 }) |
||||
|
amount!: number; |
||||
|
|
||||
|
/** Unit code (HUR, USD, etc.) */ |
||||
|
@Prop({ default: "HUR" }) |
||||
|
unitCode!: string; |
||||
|
|
||||
|
/** Input placeholder text */ |
||||
|
@Prop({ default: "" }) |
||||
|
prompt!: string; |
||||
|
|
||||
|
/** Whether this is from a project view */ |
||||
|
@Prop({ default: false }) |
||||
|
isFromProjectView!: boolean; |
||||
|
|
||||
|
/** Whether there's a conflict between giver and receiver */ |
||||
|
@Prop({ default: false }) |
||||
|
hasConflict!: boolean; |
||||
|
|
||||
|
/** Offer ID for context */ |
||||
|
@Prop({ default: "" }) |
||||
|
offerId!: string; |
||||
|
|
||||
|
/** Project ID for context (giver) */ |
||||
|
@Prop({ default: "" }) |
||||
|
fromProjectId!: string; |
||||
|
|
||||
|
/** Project ID for context (recipient) */ |
||||
|
@Prop({ default: "" }) |
||||
|
toProjectId!: string; |
||||
|
|
||||
|
/** Local reactive copies of props for v-model */ |
||||
|
private localDescription: string = ""; |
||||
|
private localAmount: number = 0; |
||||
|
private localUnitCode: string = "HUR"; |
||||
|
|
||||
|
/** |
||||
|
* Initialize local values from props |
||||
|
*/ |
||||
|
mounted(): void { |
||||
|
this.localDescription = this.description; |
||||
|
this.localAmount = this.amount; |
||||
|
this.localUnitCode = this.unitCode; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Watch for external prop changes |
||||
|
*/ |
||||
|
@Watch("description") |
||||
|
onDescriptionChange(newValue: string): void { |
||||
|
this.localDescription = newValue; |
||||
|
} |
||||
|
|
||||
|
@Watch("amount") |
||||
|
onAmountChange(newValue: number): void { |
||||
|
this.localAmount = newValue; |
||||
|
} |
||||
|
|
||||
|
@Watch("unitCode") |
||||
|
onUnitCodeChange(newValue: string): void { |
||||
|
this.localUnitCode = newValue; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computed label for giver entity |
||||
|
*/ |
||||
|
get giverLabel(): string { |
||||
|
return this.giverEntityType === "project" |
||||
|
? "Benefited from:" |
||||
|
: "Received from:"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computed label for recipient entity |
||||
|
*/ |
||||
|
get recipientLabel(): string { |
||||
|
return this.recipientEntityType === "project" |
||||
|
? "Given to project:" |
||||
|
: "Given to:"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Whether the giver can be edited |
||||
|
*/ |
||||
|
get canEditGiver(): boolean { |
||||
|
return !(this.isFromProjectView && this.giverEntityType === "project"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Whether the recipient can be edited |
||||
|
*/ |
||||
|
get canEditRecipient(): boolean { |
||||
|
return this.recipientEntityType === "person"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computed CSS classes for submit button |
||||
|
*/ |
||||
|
get submitButtonClasses(): string { |
||||
|
if (this.hasConflict) { |
||||
|
return "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"; |
||||
|
} |
||||
|
return "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"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computed route for photo & more options |
||||
|
*/ |
||||
|
get photoOptionsRoute(): RouteLocationRaw { |
||||
|
return { |
||||
|
name: "gifted-details", |
||||
|
query: { |
||||
|
amountInput: this.localAmount.toString(), |
||||
|
description: this.localDescription, |
||||
|
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.localUnitCode, |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle description input changes |
||||
|
*/ |
||||
|
handleDescriptionChange(): void { |
||||
|
this.emitUpdateDescription(this.localDescription); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle amount input changes |
||||
|
*/ |
||||
|
handleAmountChange(newAmount: number): void { |
||||
|
logger.debug("[GiftDetailsStep] handleAmountChange() called", { |
||||
|
oldAmount: this.localAmount, |
||||
|
newAmount, |
||||
|
}); |
||||
|
this.localAmount = newAmount; |
||||
|
this.emitUpdateAmount(newAmount); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle unit code selection changes |
||||
|
*/ |
||||
|
handleUnitCodeChange(): void { |
||||
|
this.emitUpdateUnitCode(this.localUnitCode); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle giver edit request |
||||
|
*/ |
||||
|
handleEditGiver(): void { |
||||
|
this.emitEditEntity({ |
||||
|
entityType: "giver", |
||||
|
currentEntity: this.giver, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle recipient edit request |
||||
|
*/ |
||||
|
handleEditRecipient(): void { |
||||
|
this.emitEditEntity({ |
||||
|
entityType: "recipient", |
||||
|
currentEntity: this.receiver, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle explain data info click |
||||
|
*/ |
||||
|
handleExplainData(): void { |
||||
|
this.emitExplainData(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle form submission |
||||
|
*/ |
||||
|
handleSubmit(): void { |
||||
|
if (!this.hasConflict) { |
||||
|
this.emitSubmit({ |
||||
|
description: this.localDescription, |
||||
|
amount: this.localAmount, |
||||
|
unitCode: this.localUnitCode, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle cancel button click |
||||
|
*/ |
||||
|
handleCancel(): void { |
||||
|
this.emitCancel(); |
||||
|
} |
||||
|
|
||||
|
// Emit methods using @Emit decorator |
||||
|
|
||||
|
@Emit("update:description") |
||||
|
emitUpdateDescription(description: string): string { |
||||
|
return description; |
||||
|
} |
||||
|
|
||||
|
@Emit("update:amount") |
||||
|
emitUpdateAmount(amount: number): number { |
||||
|
logger.debug("[GiftDetailsStep] emitUpdateAmount() - emitting amount", { |
||||
|
amount, |
||||
|
}); |
||||
|
return amount; |
||||
|
} |
||||
|
|
||||
|
@Emit("update:unitCode") |
||||
|
emitUpdateUnitCode(unitCode: string): string { |
||||
|
return unitCode; |
||||
|
} |
||||
|
|
||||
|
@Emit("edit-entity") |
||||
|
emitEditEntity(data: any): any { |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
@Emit("explain-data") |
||||
|
emitExplainData(): void { |
||||
|
// No return value needed |
||||
|
} |
||||
|
|
||||
|
@Emit("submit") |
||||
|
emitSubmit(data: any): any { |
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
@Emit("cancel") |
||||
|
emitCancel(): void { |
||||
|
// No return value needed |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Component-specific styles if needed */ |
||||
|
</style> |
@ -0,0 +1,114 @@ |
|||||
|
/** * PersonCard.vue - Individual person display component * * Extracted from |
||||
|
GiftedDialog.vue to handle person entity display * with selection states and |
||||
|
conflict detection. * * @author Matthew Raymer */ |
||||
|
<template> |
||||
|
<li :class="cardClasses" @click="handleClick"> |
||||
|
<div class="relative w-fit mx-auto"> |
||||
|
<EntityIcon |
||||
|
v-if="person.did" |
||||
|
:contact="person" |
||||
|
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1" |
||||
|
/> |
||||
|
<font-awesome |
||||
|
v-else |
||||
|
icon="circle-question" |
||||
|
class="text-slate-400 text-5xl mb-1" |
||||
|
/> |
||||
|
|
||||
|
<!-- Time icon overlay for contacts --> |
||||
|
<div |
||||
|
v-if="person.did && showTimeIcon" |
||||
|
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="nameClasses"> |
||||
|
{{ person.name || person.did || "Unnamed" }} |
||||
|
</h3> |
||||
|
</li> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue, Emit } from "vue-facing-decorator"; |
||||
|
import EntityIcon from "./EntityIcon.vue"; |
||||
|
import { Contact } from "../db/tables/contacts"; |
||||
|
|
||||
|
/** |
||||
|
* PersonCard - Individual person display with selection capability |
||||
|
* |
||||
|
* Features: |
||||
|
* - Person avatar using EntityIcon |
||||
|
* - Selection states (selectable, conflicted, disabled) |
||||
|
* - Time icon overlay for contacts |
||||
|
* - Click event handling |
||||
|
* - Emits click events for parent handling |
||||
|
*/ |
||||
|
@Component({ |
||||
|
components: { |
||||
|
EntityIcon, |
||||
|
}, |
||||
|
}) |
||||
|
export default class PersonCard extends Vue { |
||||
|
/** Contact data to display */ |
||||
|
@Prop({ required: true }) |
||||
|
person!: Contact; |
||||
|
|
||||
|
/** Whether this person can be selected */ |
||||
|
@Prop({ default: true }) |
||||
|
selectable!: boolean; |
||||
|
|
||||
|
/** Whether this person would create a conflict if selected */ |
||||
|
@Prop({ default: false }) |
||||
|
conflicted!: boolean; |
||||
|
|
||||
|
/** Whether to show time icon overlay */ |
||||
|
@Prop({ default: false }) |
||||
|
showTimeIcon!: boolean; |
||||
|
|
||||
|
/** |
||||
|
* Computed CSS classes for the card |
||||
|
*/ |
||||
|
get cardClasses(): string { |
||||
|
if (!this.selectable || this.conflicted) { |
||||
|
return "opacity-50 cursor-not-allowed"; |
||||
|
} |
||||
|
return "cursor-pointer hover:bg-slate-50"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computed CSS classes for the person name |
||||
|
*/ |
||||
|
get nameClasses(): string { |
||||
|
const baseClasses = |
||||
|
"text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"; |
||||
|
|
||||
|
if (this.conflicted) { |
||||
|
return `${baseClasses} text-slate-400`; |
||||
|
} |
||||
|
|
||||
|
return baseClasses; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle card click - only emit if selectable and not conflicted |
||||
|
*/ |
||||
|
handleClick(): void { |
||||
|
if (this.selectable && !this.conflicted) { |
||||
|
this.emitPersonSelected(this.person); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Emit methods using @Emit decorator |
||||
|
|
||||
|
@Emit("person-selected") |
||||
|
emitPersonSelected(person: Contact): Contact { |
||||
|
return person; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Component-specific styles if needed */ |
||||
|
</style> |
@ -0,0 +1,96 @@ |
|||||
|
/** * ProjectCard.vue - Individual project display component * * Extracted from |
||||
|
GiftedDialog.vue to handle project entity display * with selection states and |
||||
|
issuer information. * * @author Matthew Raymer */ |
||||
|
<template> |
||||
|
<li class="cursor-pointer" @click="handleClick"> |
||||
|
<div class="relative w-fit mx-auto"> |
||||
|
<ProjectIcon |
||||
|
:entity-id="project.handleId" |
||||
|
:icon-size="48" |
||||
|
:image-url="project.image" |
||||
|
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1" |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<h3 |
||||
|
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden" |
||||
|
> |
||||
|
{{ project.name }} |
||||
|
</h3> |
||||
|
|
||||
|
<div class="text-xs text-slate-500 truncate"> |
||||
|
<font-awesome icon="user" class="fa-fw text-slate-400" /> |
||||
|
{{ issuerDisplayName }} |
||||
|
</div> |
||||
|
</li> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue, Emit } from "vue-facing-decorator"; |
||||
|
import ProjectIcon from "./ProjectIcon.vue"; |
||||
|
import { PlanData } from "../interfaces/records"; |
||||
|
import { Contact } from "../db/tables/contacts"; |
||||
|
import { didInfo } from "../libs/endorserServer"; |
||||
|
|
||||
|
/** |
||||
|
* ProjectCard - Displays a project entity with selection capability |
||||
|
* |
||||
|
* Features: |
||||
|
* - Shows project icon using ProjectIcon |
||||
|
* - Displays project name and issuer information |
||||
|
* - Handles click events for selection |
||||
|
* - Shows issuer name using didInfo utility |
||||
|
*/ |
||||
|
@Component({ |
||||
|
components: { |
||||
|
ProjectIcon, |
||||
|
}, |
||||
|
}) |
||||
|
export default class ProjectCard extends Vue { |
||||
|
/** Project entity to display */ |
||||
|
@Prop({ required: true }) |
||||
|
project!: PlanData; |
||||
|
|
||||
|
/** Active user's DID for issuer display */ |
||||
|
@Prop({ required: true }) |
||||
|
activeDid!: string; |
||||
|
|
||||
|
/** All user's DIDs for issuer display */ |
||||
|
@Prop({ required: true }) |
||||
|
allMyDids!: string[]; |
||||
|
|
||||
|
/** All contacts for issuer display */ |
||||
|
@Prop({ required: true }) |
||||
|
allContacts!: Contact[]; |
||||
|
|
||||
|
/** |
||||
|
* Computed display name for the project issuer |
||||
|
*/ |
||||
|
get issuerDisplayName(): string { |
||||
|
return didInfo( |
||||
|
this.project.issuerDid, |
||||
|
this.activeDid, |
||||
|
this.allMyDids, |
||||
|
this.allContacts, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle card click - emit project selection |
||||
|
*/ |
||||
|
handleClick(): void { |
||||
|
this.emitProjectSelected(this.project); |
||||
|
} |
||||
|
|
||||
|
// Emit methods using @Emit decorator |
||||
|
|
||||
|
@Emit("project-selected") |
||||
|
emitProjectSelected(project: PlanData): PlanData { |
||||
|
return project; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Component-specific styles if needed */ |
||||
|
</style> |
@ -0,0 +1,66 @@ |
|||||
|
/** * ShowAllCard.vue - Show All navigation card component * * Extracted from |
||||
|
GiftedDialog.vue to handle "Show All" navigation * for both people and projects |
||||
|
entity types. * * @author Matthew Raymer */ |
||||
|
<template> |
||||
|
<li class="cursor-pointer"> |
||||
|
<router-link :to="navigationRoute" class="block text-center"> |
||||
|
<font-awesome icon="circle-right" class="text-blue-500 text-5xl mb-1" /> |
||||
|
<h3 |
||||
|
class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden" |
||||
|
> |
||||
|
Show All |
||||
|
</h3> |
||||
|
</router-link> |
||||
|
</li> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue } from "vue-facing-decorator"; |
||||
|
import { RouteLocationRaw } from "vue-router"; |
||||
|
|
||||
|
/** |
||||
|
* ShowAllCard - Displays "Show All" navigation for entity grids |
||||
|
* |
||||
|
* Features: |
||||
|
* - Provides navigation to full entity listings |
||||
|
* - Supports different routes based on entity type |
||||
|
* - Maintains context through query parameters |
||||
|
* - Consistent visual styling with other cards |
||||
|
*/ |
||||
|
@Component |
||||
|
export default class ShowAllCard extends Vue { |
||||
|
/** Type of entities being shown */ |
||||
|
@Prop({ required: true }) |
||||
|
entityType!: "people" | "projects"; |
||||
|
|
||||
|
/** Route name to navigate to */ |
||||
|
@Prop({ required: true }) |
||||
|
routeName!: string; |
||||
|
|
||||
|
/** Query parameters to pass to the route */ |
||||
|
@Prop({ default: () => ({}) }) |
||||
|
queryParams!: Record<string, string>; |
||||
|
|
||||
|
/** |
||||
|
* Computed navigation route with query parameters |
||||
|
*/ |
||||
|
get navigationRoute(): RouteLocationRaw { |
||||
|
return { |
||||
|
name: this.routeName, |
||||
|
query: this.queryParams, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Ensure router-link styling is consistent */ |
||||
|
a { |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
|
||||
|
a:hover .fa-circle-right { |
||||
|
transform: scale(1.1); |
||||
|
transition: transform 0.2s ease; |
||||
|
} |
||||
|
</style> |
@ -0,0 +1,135 @@ |
|||||
|
/** * SpecialEntityCard.vue - Special entity display component * * Extracted |
||||
|
from GiftedDialog.vue to handle special entities like "You" * and "Unnamed" with |
||||
|
conflict detection and selection capability. * * @author Matthew Raymer */ |
||||
|
<template> |
||||
|
<li :class="cardClasses" @click="handleClick"> |
||||
|
<font-awesome :icon="icon" :class="iconClasses" /> |
||||
|
<h3 :class="nameClasses"> |
||||
|
{{ label }} |
||||
|
</h3> |
||||
|
</li> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue } from "vue-facing-decorator"; |
||||
|
import { Emit } from "vue-facing-decorator"; |
||||
|
|
||||
|
/** |
||||
|
* SpecialEntityCard - Displays special entities with selection capability |
||||
|
* |
||||
|
* Features: |
||||
|
* - Displays special entities like "You" and "Unnamed" |
||||
|
* - Shows appropriate FontAwesome icons |
||||
|
* - Handles conflict states and selection |
||||
|
* - Emits selection events with entity data |
||||
|
* - Configurable styling based on entity type |
||||
|
*/ |
||||
|
@Component({ |
||||
|
emits: ["entity-selected"], |
||||
|
}) |
||||
|
export default class SpecialEntityCard extends Vue { |
||||
|
/** Type of special entity */ |
||||
|
@Prop({ required: true }) |
||||
|
entityType!: "you" | "unnamed"; |
||||
|
|
||||
|
/** Display label for the entity */ |
||||
|
@Prop({ required: true }) |
||||
|
label!: string; |
||||
|
|
||||
|
/** FontAwesome icon name */ |
||||
|
@Prop({ required: true }) |
||||
|
icon!: string; |
||||
|
|
||||
|
/** Whether this entity can be selected */ |
||||
|
@Prop({ default: true }) |
||||
|
selectable!: boolean; |
||||
|
|
||||
|
/** Whether selecting this entity would create a conflict */ |
||||
|
@Prop({ default: false }) |
||||
|
conflicted!: boolean; |
||||
|
|
||||
|
/** Entity data to emit when selected */ |
||||
|
@Prop({ required: true }) |
||||
|
entityData!: { did?: string; name: string }; |
||||
|
|
||||
|
/** |
||||
|
* Computed CSS classes for the card container |
||||
|
*/ |
||||
|
get cardClasses(): string { |
||||
|
const baseClasses = "block"; |
||||
|
|
||||
|
if (!this.selectable || this.conflicted) { |
||||
|
return `${baseClasses} cursor-not-allowed opacity-50`; |
||||
|
} |
||||
|
|
||||
|
return `${baseClasses} cursor-pointer`; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computed CSS classes for the icon |
||||
|
*/ |
||||
|
get iconClasses(): string { |
||||
|
const baseClasses = "text-5xl mb-1"; |
||||
|
|
||||
|
if (this.conflicted) { |
||||
|
return `${baseClasses} text-slate-400`; |
||||
|
} |
||||
|
|
||||
|
// Different colors for different entity types |
||||
|
switch (this.entityType) { |
||||
|
case "you": |
||||
|
return `${baseClasses} text-blue-500`; |
||||
|
case "unnamed": |
||||
|
return `${baseClasses} text-slate-400`; |
||||
|
default: |
||||
|
return `${baseClasses} text-slate-400`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Computed CSS classes for the entity name/label |
||||
|
*/ |
||||
|
get nameClasses(): string { |
||||
|
const baseClasses = |
||||
|
"text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"; |
||||
|
|
||||
|
if (this.conflicted) { |
||||
|
return `${baseClasses} text-slate-400`; |
||||
|
} |
||||
|
|
||||
|
// Different colors for different entity types |
||||
|
switch (this.entityType) { |
||||
|
case "you": |
||||
|
return `${baseClasses} text-blue-500`; |
||||
|
case "unnamed": |
||||
|
return `${baseClasses} text-slate-500 italic`; |
||||
|
default: |
||||
|
return `${baseClasses} text-slate-500`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle card click - only emit if selectable and not conflicted |
||||
|
*/ |
||||
|
handleClick(): void { |
||||
|
if (this.selectable && !this.conflicted) { |
||||
|
this.emitEntitySelected({ |
||||
|
type: "special", |
||||
|
entityType: this.entityType, |
||||
|
data: this.entityData, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Emit methods using @Emit decorator |
||||
|
|
||||
|
@Emit("entity-selected") |
||||
|
emitEntitySelected(data: any): any { |
||||
|
return data; |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Component-specific styles if needed */ |
||||
|
</style> |
Loading…
Reference in new issue