Browse Source
- 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 componentsmatthew-scratch-2025-06-28
4 changed files with 556 additions and 16 deletions
@ -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> |
@ -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> |
@ -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…
Reference in new issue