Browse Source

feat: Phase 2 - Extract layout components from GiftedDialog

- Create EntityGrid.vue for unified entity grid layout
  * Responsive grid layout for people and projects
  * Integrates special entities (You, Unnamed) seamlessly
  * Conflict detection integration with prop-based checker
  * Empty state messaging based on entity type
  * Show All navigation with query parameter support
  * Event delegation for all entity selection types
  * Configurable display limits and grid columns

- Create SpecialEntityCard.vue for special entity handling
  * Handles 'You' and 'Unnamed' entity types
  * FontAwesome icon integration with configurable styling
  * Conflict state handling with visual feedback
  * Entity-type-specific color schemes (blue for You, gray for Unnamed)
  * Emits structured events with entity data

- Create ShowAllCard.vue for navigation functionality
  * Router-link integration with query parameter passing
  * Consistent visual styling with other entity cards
  * Hover effects and animations
  * Maintains context through configurable route params

- Update GiftedDialog-Decomposition-Plan.md
  * Mark Phase 2 as completed
  * Add comprehensive integration examples
  * Update component specifications with actual props/emits
  * Add EntityGrid, SpecialEntityCard, and ShowAllCard usage patterns
  * Update project status and next steps

Phase 2 creates reusable layout components that can replace the complex
grid logic in GiftedDialog. The EntityGrid component provides a clean
API for entity selection while maintaining all existing functionality.

Components: 7 total (4 Phase 1 + 3 Phase 2)
Next: Integration phase - Update GiftedDialog to use new components
matthew-scratch-2025-06-28
Matthew Raymer 2 weeks ago
parent
commit
a559fd3318
  1. 103
      GiftedDialog-Decomposition-Plan.md
  2. 259
      src/components/EntityGrid.vue
  3. 75
      src/components/ShowAllCard.vue
  4. 135
      src/components/SpecialEntityCard.vue

103
GiftedDialog-Decomposition-Plan.md

@ -67,29 +67,42 @@ These components handle pure presentation with minimal business logic:
- **Props**: `value`, `min`, `max`, `step`, `inputId`
- **Emits**: `update:value`
### Phase 2: Extract Layout Components (NEXT)
### Phase 2: Extract Layout Components (✅ COMPLETED)
These components handle layout and entity organization:
#### 5. EntityGrid.vue (PLANNED)
- **Purpose**: Reusable grid for displaying people or projects
#### 5. EntityGrid.vue
- **Purpose**: Unified grid layout 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`
- 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
- **Props**: `entityType`, `entities`, `maxItems`, `activeDid`, `allMyDids`, `allContacts`, `conflictChecker`, `showYouEntity`, `youSelectable`, `showAllRoute`, `showAllQueryParams`
- **Emits**: `entity-selected`
#### 6. SpecialEntityCard.vue (PLANNED)
#### 6. SpecialEntityCard.vue
- **Purpose**: Handle special entities like "You" and "Unnamed"
- **Features**:
- Special icon display (hand, question mark)
- Conflict state handling
- Configurable styling based on entity type
- Click event handling
- **Props**: `entityType`, `label`, `icon`, `conflicted`
- **Props**: `entityType`, `label`, `icon`, `selectable`, `conflicted`, `entityData`
- **Emits**: `entity-selected`
#### 7. ShowAllCard.vue ✅
- **Purpose**: Handle "Show All" navigation functionality
- **Features**:
- Router-link integration
- Query parameter passing
- Consistent visual styling
- Hover effects
- **Props**: `entityType`, `routeName`, `queryParams`
- **Emits**: None (uses router-link)
### Phase 3: Extract Step Components (FUTURE)
These components handle major UI sections:
@ -130,17 +143,23 @@ These components handle major UI sections:
### ✅ Completed Components
**Phase 1: Display 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
**Phase 2: Layout Components**
5. **EntityGrid.vue** - Unified grid layout for entity selection
6. **SpecialEntityCard.vue** - Special entities (You, Unnamed) with conflict handling
7. **ShowAllCard.vue** - Show All navigation with router integration
### 🔄 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
1. **Update GiftedDialog.vue** - Integrate Phase 1 & 2 components incrementally
2. **Test integration** - Ensure functionality remains intact
3. **Create unit tests** - For all new components
4. **Performance validation** - Ensure no regression
### 📋 Future Phases
@ -232,6 +251,58 @@ These components handle major UI sections:
</template>
```
### Using EntityGrid in EntitySelectionStep
```vue
<template>
<div>
<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="wouldCreateConflict"
:show-you-entity="showYouEntity"
:you-selectable="youSelectable"
:show-all-route="showAllRoute"
:show-all-query-params="showAllQueryParams"
@entity-selected="handleEntitySelected"
/>
</div>
</template>
```
### Using SpecialEntityCard Standalone
```vue
<template>
<ul class="grid grid-cols-4 gap-2">
<SpecialEntityCard
entity-type="you"
label="You"
icon="hand"
:conflicted="wouldCreateConflict(activeDid)"
:entity-data="{ did: activeDid, name: 'You' }"
@entity-selected="handleYouSelected"
/>
<SpecialEntityCard
entity-type="unnamed"
label="Unnamed"
icon="circle-question"
:entity-data="{ did: '', name: 'Unnamed' }"
@entity-selected="handleUnnamedSelected"
/>
</ul>
</template>
```
## Migration Strategy
### Backward Compatibility
@ -271,4 +342,4 @@ The completed Phase 1 components (PersonCard, ProjectCard, EntitySummaryButton,
**Author**: Matthew Raymer
**Last Updated**: 2025-01-28
**Status**: Phase 1 Complete, Phase 2 In Progress
**Status**: Phase 1 & 2 Complete, Integration Phase Next

259
src/components/EntityGrid.vue

@ -0,0 +1,259 @@
/**
* 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 } 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 "@/interfaces/contact";
import { PlanData } from "@/interfaces/plan-data";
/**
* EntityGrid - Unified grid layout for entity selection
*
* Features:
* - Responsive grid layout for people or projects
* - Special entity handling (You, Unnamed)
* - Conflict detection integration
* - Empty state messaging
* - Show All navigation
* - Configurable display limits
* - 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, any>;
/**
* 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.$emit("entity-selected", {
type: "person",
data: person,
});
}
/**
* Handle project selection from ProjectCard
*/
handleProjectSelected(project: PlanData): void {
this.$emit("entity-selected", {
type: "project",
data: project,
});
}
/**
* Handle special entity selection from SpecialEntityCard
*/
handleEntitySelected(event: { type: string; entityType: string; data: any }): void {
this.$emit("entity-selected", {
type: "special",
entityType: event.entityType,
data: event.data,
});
}
}
</script>
<style scoped>
/* Grid-specific styles if needed */
</style>

75
src/components/ShowAllCard.vue

@ -0,0 +1,75 @@
/**
* 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, any>;
/**
* 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>

135
src/components/SpecialEntityCard.vue

@ -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";
/**
* 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
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.$emit("entity-selected", {
type: "special",
entityType: this.entityType,
data: this.entityData,
});
}
}
}
</script>
<style scoped>
/* Component-specific styles if needed */
</style>
Loading…
Cancel
Save