Browse Source
- Create PersonCard.vue for individual person entity display * Handles selection states and conflict detection * Includes time icon overlay for contacts * Emits person-selected events - Create ProjectCard.vue for individual project entity display * Shows project icon, name, and issuer information * Handles project selection events * Reusable for project listings - Create EntitySummaryButton.vue for selected entity display * Supports both person and project entities * Shows editable vs locked states with appropriate icons * Handles edit-requested events for step 2 functionality - Create AmountInput.vue for numeric input with controls * Increment/decrement buttons with validation * Configurable min/max values and step size * v-model compatible for form integration * Proper input validation and boundary checking - Add comprehensive GiftedDialog-Decomposition-Plan.md * Documents complete 4-phase refactoring strategy * Provides implementation progress tracking * Includes integration examples and migration strategy * Outlines benefits: maintainability, testability, reusability This Phase 1 extraction creates reusable display components that can be immediately integrated into GiftedDialog and used throughout the app. The incremental approach reduces refactoring risk while preparing for future Pinia state management integration. Next: Phase 2 - Extract layout components (EntityGrid, SpecialEntityCard)matthew-scratch-2025-06-28
5 changed files with 805 additions and 0 deletions
@ -0,0 +1,274 @@ |
|||||
|
# GiftedDialog Component Decomposition Plan |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
This document outlines a comprehensive plan to refactor the GiftedDialog component by breaking it into smaller, more manageable sub-components. This approach will improve maintainability, testability, and reusability while preparing the codebase for future Pinia integration. |
||||
|
|
||||
|
## Current State Analysis |
||||
|
|
||||
|
The GiftedDialog component (1060 lines) is a complex Vue component that handles: |
||||
|
|
||||
|
- **Two-step wizard UI**: Entity selection → Gift details |
||||
|
- **Multiple entity types**: Person/Project as giver/recipient |
||||
|
- **Complex conditional rendering**: Based on context and entity types |
||||
|
- **Form validation and submission**: Gift recording with API integration |
||||
|
- **State management**: UI flow, entity selection, form data |
||||
|
|
||||
|
### Key Challenges |
||||
|
|
||||
|
1. **Large single file**: Difficult to navigate and maintain |
||||
|
2. **Mixed concerns**: UI logic, business logic, and API calls in one place |
||||
|
3. **Complex state**: Multiple interconnected reactive properties |
||||
|
4. **Testing difficulty**: Hard to test individual features in isolation |
||||
|
5. **Reusability**: Components like entity grids could be reused elsewhere |
||||
|
|
||||
|
## Decomposition Strategy |
||||
|
|
||||
|
### Phase 1: Extract Display Components (✅ COMPLETED) |
||||
|
|
||||
|
These components handle pure presentation with minimal business logic: |
||||
|
|
||||
|
#### 1. PersonCard.vue ✅ |
||||
|
- **Purpose**: Display individual person entities with selection capability |
||||
|
- **Features**: |
||||
|
- Person avatar using EntityIcon |
||||
|
- Selection states (selectable, conflicted, disabled) |
||||
|
- Time icon overlay for contacts |
||||
|
- Click event handling |
||||
|
- **Props**: `person`, `selectable`, `conflicted`, `showTimeIcon` |
||||
|
- **Emits**: `person-selected` |
||||
|
|
||||
|
#### 2. ProjectCard.vue ✅ |
||||
|
- **Purpose**: Display individual project entities with selection capability |
||||
|
- **Features**: |
||||
|
- Project icon using ProjectIcon |
||||
|
- Project name and issuer information |
||||
|
- Click event handling |
||||
|
- **Props**: `project`, `activeDid`, `allMyDids`, `allContacts` |
||||
|
- **Emits**: `project-selected` |
||||
|
|
||||
|
#### 3. EntitySummaryButton.vue ✅ |
||||
|
- **Purpose**: Display selected entity with edit capability in step 2 |
||||
|
- **Features**: |
||||
|
- Entity avatar (person or project) |
||||
|
- Entity name and role label |
||||
|
- Editable vs locked states |
||||
|
- Edit button functionality |
||||
|
- **Props**: `entity`, `entityType`, `label`, `editable` |
||||
|
- **Emits**: `edit-requested` |
||||
|
|
||||
|
#### 4. AmountInput.vue ✅ |
||||
|
- **Purpose**: Specialized numeric input with increment/decrement controls |
||||
|
- **Features**: |
||||
|
- Increment/decrement buttons with validation |
||||
|
- Configurable min/max values and step size |
||||
|
- Input validation and formatting |
||||
|
- v-model compatibility |
||||
|
- **Props**: `value`, `min`, `max`, `step`, `inputId` |
||||
|
- **Emits**: `update:value` |
||||
|
|
||||
|
### Phase 2: Extract Layout Components (NEXT) |
||||
|
|
||||
|
These components handle layout and entity organization: |
||||
|
|
||||
|
#### 5. EntityGrid.vue (PLANNED) |
||||
|
- **Purpose**: Reusable grid for displaying people or projects |
||||
|
- **Features**: |
||||
|
- Responsive grid layout |
||||
|
- Entity type switching (people/projects) |
||||
|
- "Show All" navigation |
||||
|
- Empty state handling |
||||
|
- **Props**: `entities`, `entityType`, `gridCols`, `maxItems` |
||||
|
- **Emits**: `entity-selected`, `show-all-clicked` |
||||
|
|
||||
|
#### 6. SpecialEntityCard.vue (PLANNED) |
||||
|
- **Purpose**: Handle special entities like "You" and "Unnamed" |
||||
|
- **Features**: |
||||
|
- Special icon display (hand, question mark) |
||||
|
- Conflict state handling |
||||
|
- Click event handling |
||||
|
- **Props**: `entityType`, `label`, `icon`, `conflicted` |
||||
|
- **Emits**: `entity-selected` |
||||
|
|
||||
|
### Phase 3: Extract Step Components (FUTURE) |
||||
|
|
||||
|
These components handle major UI sections: |
||||
|
|
||||
|
#### 7. EntitySelectionStep.vue (PLANNED) |
||||
|
- **Purpose**: Complete step 1 entity selection interface |
||||
|
- **Features**: |
||||
|
- Dynamic step type handling (giver/recipient) |
||||
|
- Entity type switching (people/projects) |
||||
|
- Conflict detection integration |
||||
|
- Cancel functionality |
||||
|
- **Props**: `stepType`, `entityType`, `entities`, `conflicts`, `context` |
||||
|
- **Emits**: `entity-selected`, `cancel` |
||||
|
|
||||
|
#### 8. GiftDetailsStep.vue (PLANNED) |
||||
|
- **Purpose**: Complete step 2 gift details form |
||||
|
- **Features**: |
||||
|
- Entity summary display |
||||
|
- Gift description input |
||||
|
- Amount input with controls |
||||
|
- Form validation |
||||
|
- Submit functionality |
||||
|
- **Props**: `giver`, `receiver`, `context`, `initialValues` |
||||
|
- **Emits**: `submit`, `back`, `entity-edit-requested` |
||||
|
|
||||
|
### Phase 4: Refactor Main Component (FINAL) |
||||
|
|
||||
|
#### 9. GiftedDialog.vue (PLANNED REFACTOR) |
||||
|
- **Purpose**: Orchestrate sub-components and manage overall state |
||||
|
- **Responsibilities**: |
||||
|
- Step navigation logic |
||||
|
- Entity conflict detection |
||||
|
- API integration for gift recording |
||||
|
- Success/error handling |
||||
|
- Dialog visibility management |
||||
|
|
||||
|
## Implementation Progress |
||||
|
|
||||
|
### ✅ Completed Components |
||||
|
|
||||
|
1. **PersonCard.vue** - Individual person display with selection |
||||
|
2. **ProjectCard.vue** - Individual project display with selection |
||||
|
3. **EntitySummaryButton.vue** - Selected entity display with edit capability |
||||
|
4. **AmountInput.vue** - Numeric input with increment/decrement controls |
||||
|
|
||||
|
### 🔄 Next Steps |
||||
|
|
||||
|
1. **Create EntityGrid.vue** - Unified grid layout for entities |
||||
|
2. **Create SpecialEntityCard.vue** - Handle "You" and "Unnamed" entities |
||||
|
3. **Update GiftedDialog.vue** - Integrate new components incrementally |
||||
|
4. **Test integration** - Ensure functionality remains intact |
||||
|
|
||||
|
### 📋 Future Phases |
||||
|
|
||||
|
1. **Extract EntitySelectionStep.vue** - Complete step 1 logic |
||||
|
2. **Extract GiftDetailsStep.vue** - Complete step 2 logic |
||||
|
3. **Refactor main component** - Minimal orchestration logic |
||||
|
4. **Add comprehensive tests** - Unit tests for each component |
||||
|
5. **Prepare for Pinia** - State management migration |
||||
|
|
||||
|
## Benefits of This Approach |
||||
|
|
||||
|
### 1. Incremental Refactoring |
||||
|
- Each phase can be implemented and tested independently |
||||
|
- Reduces risk of breaking existing functionality |
||||
|
- Allows for gradual improvement over time |
||||
|
|
||||
|
### 2. Improved Maintainability |
||||
|
- Smaller, focused components are easier to understand |
||||
|
- Clear separation of concerns |
||||
|
- Easier to locate and fix bugs |
||||
|
|
||||
|
### 3. Enhanced Testability |
||||
|
- Individual components can be unit tested in isolation |
||||
|
- Easier to mock dependencies |
||||
|
- Better test coverage possible |
||||
|
|
||||
|
### 4. Better Reusability |
||||
|
- Components like EntityGrid can be used in other views |
||||
|
- PersonCard and ProjectCard can be used throughout the app |
||||
|
- AmountInput can be reused for other numeric inputs |
||||
|
|
||||
|
### 5. Pinia Preparation |
||||
|
- Smaller components make state management migration easier |
||||
|
- Clear data flow patterns emerge |
||||
|
- Easier to identify what state should be global vs local |
||||
|
|
||||
|
## Integration Examples |
||||
|
|
||||
|
### Using PersonCard in EntityGrid |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<ul class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-2 gap-y-4"> |
||||
|
<PersonCard |
||||
|
v-for="person in people" |
||||
|
:key="person.did" |
||||
|
:person="person" |
||||
|
:conflicted="wouldCreateConflict(person.did)" |
||||
|
@person-selected="handlePersonSelected" |
||||
|
/> |
||||
|
</ul> |
||||
|
</template> |
||||
|
``` |
||||
|
|
||||
|
### Using AmountInput in GiftDetailsStep |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<AmountInput |
||||
|
:value="amount" |
||||
|
:min="0" |
||||
|
:max="1000" |
||||
|
input-id="gift-amount" |
||||
|
@update:value="amount = $event" |
||||
|
/> |
||||
|
</template> |
||||
|
``` |
||||
|
|
||||
|
### Using EntitySummaryButton in GiftDetailsStep |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="grid grid-cols-2 gap-2"> |
||||
|
<EntitySummaryButton |
||||
|
:entity="giver" |
||||
|
entity-type="person" |
||||
|
label="Received from:" |
||||
|
:editable="canEditGiver" |
||||
|
@edit-requested="handleEditGiver" |
||||
|
/> |
||||
|
<EntitySummaryButton |
||||
|
:entity="receiver" |
||||
|
entity-type="person" |
||||
|
label="Given to:" |
||||
|
:editable="canEditReceiver" |
||||
|
@edit-requested="handleEditReceiver" |
||||
|
/> |
||||
|
</div> |
||||
|
</template> |
||||
|
``` |
||||
|
|
||||
|
## Migration Strategy |
||||
|
|
||||
|
### Backward Compatibility |
||||
|
- Maintain existing API and prop interfaces |
||||
|
- Ensure all existing functionality works unchanged |
||||
|
- Preserve all event emissions and callbacks |
||||
|
|
||||
|
### Testing Strategy |
||||
|
- Create unit tests for each new component |
||||
|
- Maintain existing integration tests |
||||
|
- Add visual regression tests for UI components |
||||
|
|
||||
|
### Performance Considerations |
||||
|
- Monitor bundle size impact |
||||
|
- Ensure no performance regression |
||||
|
- Optimize component loading if needed |
||||
|
|
||||
|
## Security Considerations |
||||
|
|
||||
|
### Input Validation |
||||
|
- AmountInput includes proper numeric validation |
||||
|
- All user inputs are validated before processing |
||||
|
- XSS prevention through proper Vue templating |
||||
|
|
||||
|
### Data Handling |
||||
|
- No sensitive data stored in component state |
||||
|
- Proper prop validation and type checking |
||||
|
- Secure API communication maintained |
||||
|
|
||||
|
## Conclusion |
||||
|
|
||||
|
This decomposition plan provides a structured approach to refactoring the GiftedDialog component while maintaining functionality and preparing for future enhancements. The incremental approach reduces risk and allows for continuous improvement of the codebase. |
||||
|
|
||||
|
The completed Phase 1 components (PersonCard, ProjectCard, EntitySummaryButton, AmountInput) provide a solid foundation for the remaining phases and demonstrate the benefits of component decomposition in terms of maintainability, testability, and reusability. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Last Updated**: 2025-01-28 |
||||
|
**Status**: Phase 1 Complete, Phase 2 In Progress |
@ -0,0 +1,174 @@ |
|||||
|
/** |
||||
|
* 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"> |
||||
|
<button |
||||
|
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2" |
||||
|
:disabled="isAtMinimum" |
||||
|
@click="decrement" |
||||
|
> |
||||
|
<font-awesome icon="chevron-left" /> |
||||
|
</button> |
||||
|
|
||||
|
<input |
||||
|
:id="inputId" |
||||
|
:value="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" |
||||
|
@click="increment" |
||||
|
> |
||||
|
<font-awesome icon="chevron-right" /> |
||||
|
</button> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { Component, Prop, Vue, Watch } from "vue-facing-decorator"; |
||||
|
|
||||
|
/** |
||||
|
* 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 { |
||||
|
this.displayValue = this.value.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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 { |
||||
|
return this.value <= this.min; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if current value is at maximum |
||||
|
*/ |
||||
|
get isAtMaximum(): boolean { |
||||
|
return this.value >= this.max; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Increment the value by step size |
||||
|
*/ |
||||
|
increment(): void { |
||||
|
const newValue = Math.min(this.value + this.step, this.max); |
||||
|
this.updateValue(newValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Decrement the value by step size |
||||
|
*/ |
||||
|
decrement(): void { |
||||
|
const newValue = Math.max(this.value - this.step, this.min); |
||||
|
this.updateValue(newValue); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle direct input changes |
||||
|
*/ |
||||
|
handleInput(event: Event): void { |
||||
|
const target = event.target as HTMLInputElement; |
||||
|
this.displayValue = target.value; |
||||
|
|
||||
|
const numericValue = parseFloat(target.value); |
||||
|
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 { |
||||
|
if (newValue !== this.value) { |
||||
|
this.displayValue = newValue.toString(); |
||||
|
this.$emit("update:value", newValue); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</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,143 @@ |
|||||
|
/** |
||||
|
* 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 } from "vue-facing-decorator"; |
||||
|
import EntityIcon from "./EntityIcon.vue"; |
||||
|
import ProjectIcon from "./ProjectIcon.vue"; |
||||
|
import { Contact } from "@/interfaces/contact"; |
||||
|
|
||||
|
/** |
||||
|
* 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.$emit("edit-requested", { |
||||
|
entityType: this.entityType, |
||||
|
entity: this.entity, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</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,119 @@ |
|||||
|
/** |
||||
|
* 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 } from "vue-facing-decorator"; |
||||
|
import EntityIcon from "./EntityIcon.vue"; |
||||
|
import { Contact } from "@/interfaces/contact"; |
||||
|
|
||||
|
/** |
||||
|
* PersonCard - Displays a person entity with selection capability |
||||
|
* |
||||
|
* Features: |
||||
|
* - Shows person avatar using EntityIcon |
||||
|
* - Handles selection states (selectable, conflicted, disabled) |
||||
|
* - Displays time icon overlay for contacts |
||||
|
* - Emits click events for parent handling |
||||
|
*/ |
||||
|
@Component({ |
||||
|
components: { |
||||
|
EntityIcon, |
||||
|
}, |
||||
|
}) |
||||
|
export default class PersonCard extends Vue { |
||||
|
/** Person entity to display */ |
||||
|
@Prop({ required: true }) |
||||
|
person!: Contact; |
||||
|
|
||||
|
/** Whether this person can be selected */ |
||||
|
@Prop({ default: true }) |
||||
|
selectable!: boolean; |
||||
|
|
||||
|
/** Whether selecting this person would create a conflict */ |
||||
|
@Prop({ default: false }) |
||||
|
conflicted!: boolean; |
||||
|
|
||||
|
/** Whether to show the time icon overlay */ |
||||
|
@Prop({ default: true }) |
||||
|
showTimeIcon!: boolean; |
||||
|
|
||||
|
/** |
||||
|
* 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 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.$emit("person-selected", this.person); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Component-specific styles if needed */ |
||||
|
</style> |
@ -0,0 +1,95 @@ |
|||||
|
/** |
||||
|
* 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 } from "vue-facing-decorator"; |
||||
|
import ProjectIcon from "./ProjectIcon.vue"; |
||||
|
import { PlanData } from "@/interfaces/plan-data"; |
||||
|
import { Contact } from "@/interfaces/contact"; |
||||
|
import { didInfo } from "@/libs/util"; |
||||
|
|
||||
|
/** |
||||
|
* 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.$emit("project-selected", this.project); |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
/* Component-specific styles if needed */ |
||||
|
</style> |
Loading…
Reference in new issue