You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

46 KiB

GiftedDialog Complete Documentation

Table of Contents

  1. Overview
  2. Architecture Diagram
  3. Component Hierarchy
  4. Data Flow Diagrams
  5. Component Documentation
  6. Integration Patterns
  7. State Management
  8. Event Flow
  9. Usage Examples
  10. Testing Strategy

Overview

The GiftedDialog system is a sophisticated multi-step dialog for recording gifts between people and projects in the TimeSafari application. It consists of a main orchestrating component and 9 specialized child components that handle different aspects of the gift recording workflow.

Key Features

  • Two-Step Workflow: Entity selection → Gift details

  • Multi-Entity Support: People, projects, and special entities

  • Conflict Detection: Prevents invalid gift combinations

  • Responsive Design: Works across all device sizes

  • Accessibility: Full keyboard navigation and screen reader support

  • Validation: Comprehensive form validation and error handling

  • Flexible Integration: Can be embedded in any view with different contexts

Design Principles

  • Single Responsibility: Each component has one clear purpose

  • Composition over Inheritance: Built through component composition

  • Reactive Data Flow: Vue's reactivity system for state management

  • Progressive Enhancement: Core functionality works without JavaScript

  • Privacy by Design: Respects user privacy and data sovereignty

Architecture Diagram

graph TB
    subgraph "GiftedDialog System"
        GD[GiftedDialog.vue<br/>Main Orchestrator]

        subgraph "Step 1: Entity Selection"
            ESS[EntitySelectionStep.vue<br/>Step Controller]
            EG[EntityGrid.vue<br/>Layout Manager]
            PC[PersonCard.vue<br/>Person Display]
            PRC[ProjectCard.vue<br/>Project Display]
            SEC[SpecialEntityCard.vue<br/>Special Entities]
            SAC[ShowAllCard.vue<br/>Navigation]
        end

        subgraph "Step 2: Gift Details"
            GDS[GiftDetailsStep.vue<br/>Form Controller]
            ESB[EntitySummaryButton.vue<br/>Entity Summary]
            AI[AmountInput.vue<br/>Amount Control]
        end

        subgraph "External Dependencies"
            EI[EntityIcon.vue<br/>Icon Renderer]
            PI[ProjectIcon.vue<br/>Project Icons]
            DB[(Database Layer)]
            API[Endorser API]
            ROUTER[Vue Router]
        end
    end

    GD --> ESS
    GD --> GDS
    ESS --> EG
    EG --> PC
    EG --> PRC
    EG --> SEC
    EG --> SAC
    GDS --> ESB
    GDS --> AI
    PC --> EI
    PRC --> PI
    ESB --> EI
    ESB --> PI
    GD --> DB
    GD --> API
    SAC --> ROUTER

ASCII Art Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│                           GiftedDialog System                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                    GiftedDialog.vue                                 │    │
│  │                    Main Orchestrator                                │    │
│  │  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐      │    │
│  │  │   Dialog State  │  │  Step Control   │  │  API Integration│      │    │
│  │  │   Management    │  │   (1 ↔ 2)       │  │   & Validation  │      │    │
│  │  │  │  │  │  │  │     └─────────────────┘  └─────────────────┘      │    │
│  │  └──────────────────────────────────────────────────────────────────┘    │
│                                    │                                        │
│                                    ▼                                        │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                    Step 1: Entity Selection                         │    │
│  │                                                                     │    │
│  │  ┌─────────────────────────────────────────────────────────────┐    │    │
│  │  │              EntitySelectionStep.vue                        │    │    │
│  │  │              Step Controller                                │    │    │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │    │    │
│  │  │  │ Context     │  │ Conflict    │  │ Dynamic     │          │    │    │
│  │  │  │ Awareness   │  │ Detection   │  │ Labeling    │          │    │    │
│  │  │  └─────────────┘  └─────────────┘  └─────────────┘          │    │    │
│  │  └─────────────────────────────────────────────────────────────┘    │    │
│  │                                    │                                │    │
│  │                                    ▼                                │    │
│  │  ┌─────────────────────────────────────────────────────────────┐    │    │
│  │  │                  EntityGrid.vue                             │    │    │
│  │  │                  Layout Manager                             │    │    │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │    │    │
│  │  │  │ Responsive  │  │ Event       │  │ Empty State │          │    │    │
│  │  │  │ Grid Layout │  │ Delegation  │  │ Handling    │          │    │    │
│  │  │  └─────────────┘  └─────────────┘  └─────────────┘          │    │    │
│  │  └─────────────────────────────────────────────────────────────┘    │    │
│  │                                    │                                │    │
│  │                                    ▼                                │    │
│  │  ┌─────────────────────────────────────────────────────────────┐    │    │
│  │  │              Entity Display Components                      │    │    │
│  │  │  ┌─────────── ──┐  ┌─────────────┐  ┌─────────────┐         │    │    │
│  │  │  │ PersonCard   │  │ProjectCard  │  │SpecialEntity│         │    │    │
│  │  │  │ Avatar, Name │  │Icon, Name   │  │Card (You,   │         │    │    │
│  │  │  │ Selection    │  │Issuer Info  │  │Unnamed)     │         │    │    │
│  │  │  └──────────── ─┘  └─────────────┘  └─────────────┘         │    │    │
│  │  └─────────────────────────────────────────────────────────────┘    │    │
│  │                                    │                                │    │
│  │                                    ▼                                │    │
│  │  ┌─────────────────────────────────────────────────────────────┐    │    │
│  │  │                  ShowAllCard.vue                            │    │    │
│  │  │                  Navigation Component                       │    │    │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │    │    │
│  │  │  │ Router      │  │ Query       │  │ Context     │          │    │    │
│  │  │  │ Integration │  │ Parameters  │  │ Preservation│          │    │    │
│  │  │  └─────────────┘  └─────────────┘  └─────────────┘          │    │    │
│  │  └─────────────────────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                    │                                        │
│                                    ▼                                        │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                    Step 2: Gift Details                             │    │
│  │                                                                     │    │
│  │  ┌─────────────────────────────────────────────────────────────┐    │    │
│  │  │              GiftDetailsStep.vue                            │    │    │
│  │  │              Form Controller                                │    │    │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │    │    │
│  │  │  │ Form        │  │ Validation  │  │ Conflict    │          │    │    │
│  │  │  │ Management  │  │ & Error     │  │ Warning     │          │    │    │
│  │  │  │             │  │ Handling    │  │ Display     │          │    │    │
│  │  │  └─────────────┘  └─────────────┘  └─────────────┘          │    │    │
│  │  └─────────────────────────────────────────────────────────────┘    │    │
│  │                                    │                                │    │
│  │                                    ▼                                │    │
│  │  ┌─────────────────────────────────────────────────────────────┐    │    │
│  │  │              Form Input Components                          │    │    │
│  │  │  ┌────────────  ─┐  ┌─────────────┐  ┌─────────────┐        │    │    │
│  │  │  │EntitySummary  │  │AmountInput  │  │Description  │        │    │    │
│  │  │  │Button         │  │Increment/   │  │Input        │        │    │    │
│  │  │  │Edit Capability│  │Decrement    │  │Placeholder  │        │    │    │
│  │  │  └───────────  ──┘  └─────────────┘  └─────────────┘        │    │    │
│  │  └─────────────────────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                    │                                        │
│                                    ▼                                        │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                    External Dependencies                            │    │
│  │                                                                     │    │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐ │    │
│  │  │ EntityIcon  │  │ProjectIcon  │  │ Database    │  │ Endorser    │ │    │
│  │  │ Avatar      │  │ Project     │  │ Layer       │  │ API         │ │    │
│  │  │ Renderer    │  │ Images      │  │ SQLite      │  │ Claims      │ │    │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘ │    │
│  │                                                                     │    │
│  │  ┌─────────────────────────────────────────────────────────────┐    │    │
│  │  │                    Vue Router                               │    │    │
│  │  │              Navigation & Deep Linking                      │    │    │
│  │  └─────────────────────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────────────────────┘    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Data Flow:
┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   User      │──▶│ GiftedDialog│──▶│ Step 1      │──▶│ Entity      │
│ Interaction │    │ Main        │    │ Selection   │    │ Selection   │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘
                           │                   │                   │
                           ▼                   ▼                   ▼
┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ Success/    │◀──│ API         │◀──│ Step 2      │◀──│ Form        │
│ Error       │    │ Integration │    │ Details     │    │ Submission  │
│ Handling    │    │             │    │             │    │             │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘

Component Communication:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Event Flow: entity-selected → update:amount → submit → gift-recorded        │
│                                                                             │
│ State Flow: Dialog State → Step State → Form State → API State              │
│                                                                             │
│ Data Flow: Props Down → Events Up → Callbacks → Parent Notification         │
└─────────────────────────────────────────────────────────────────────────────┘

Component Hierarchy

Main Component

  • GiftedDialog.vue - Main orchestrating component

Step Components

  • EntitySelectionStep.vue - Step 1 controller

  • GiftDetailsStep.vue - Step 2 controller

Layout Components

  • EntityGrid.vue - Unified entity grid layout

  • EntitySummaryButton.vue - Selected entity display

Display Components

  • PersonCard.vue - Individual person display

  • ProjectCard.vue - Individual project display

  • SpecialEntityCard.vue - Special entities (You, Unnamed)

  • ShowAllCard.vue - Navigation component

Input Components

  • AmountInput.vue - Numeric input with controls

Data Flow Diagrams

Step 1: Entity Selection Flow

sequenceDiagram
    participant User
    participant GiftedDialog
    participant EntitySelectionStep
    participant EntityGrid
    participant PersonCard
    participant SpecialEntityCard

    User->>GiftedDialog: Open dialog
    GiftedDialog->>EntitySelectionStep: Render step 1
    EntitySelectionStep->>EntityGrid: Pass entities & config
    EntityGrid->>PersonCard: Render people
    EntityGrid->>SpecialEntityCard: Render special entities

    User->>PersonCard: Click person
    PersonCard->>EntityGrid: emit person-selected
    EntityGrid->>EntitySelectionStep: emit entity-selected
    EntitySelectionStep->>GiftedDialog: emit entity-selected
    GiftedDialog->>GiftedDialog: Update state & advance to step 2

Step 2: Gift Details Flow

sequenceDiagram
    participant User
    participant GiftedDialog
    participant GiftDetailsStep
    participant EntitySummaryButton
    participant AmountInput

    GiftedDialog->>GiftDetailsStep: Render step 2
    GiftDetailsStep->>EntitySummaryButton: Display selected entities
    GiftDetailsStep->>AmountInput: Render amount input

    User->>AmountInput: Click increment
    AmountInput->>GiftDetailsStep: emit update:value
    GiftDetailsStep->>GiftedDialog: emit update:amount
    GiftedDialog->>GiftedDialog: Update amount state

    User->>GiftDetailsStep: Click submit
    GiftDetailsStep->>GiftedDialog: emit submit
    GiftedDialog->>GiftedDialog: Process & submit gift

Conflict Detection Flow

flowchart TD
    A[User Selects Entity] --> B{Is Person-to-Person?}
    B -->|No| F[Allow Selection]
    B -->|Yes| C{Same DID?}
    C -->|No| F
    C -->|Yes| D[Mark as Conflicted]
    D --> E[Disable Selection]
    F --> G[Enable Selection]
    G --> H[Proceed to Next Step]
    E --> I[Show Conflict Warning]

Component Documentation

1. GiftedDialog.vue

Purpose: Main orchestrating component that manages the overall dialog state and workflow.

Key Responsibilities:

  • Dialog visibility and lifecycle management

  • Step navigation (1 → 2)

  • Entity conflict detection

  • API integration for gift submission

  • Success/error handling

  • Child component coordination

Props:

interface GiftedDialogProps {
  fromProjectId?: string;      // Project ID when project is giver
  toProjectId?: string;        // Project ID when project is recipient
  showProjects?: boolean;      // Whether to show projects
  isFromProjectView?: boolean; // Context flag for project views
}

Key Methods:

  • open() - Initialize and show dialog

  • cancel() - Close dialog and reset state

  • confirm() - Submit gift and handle response

  • handleEntitySelected() - Process entity selection from step 1

  • handleSubmit() - Process form submission from step 2

State Management:

interface GiftedDialogState {
  visible: boolean;
  currentStep: number;
  giver?: GiverReceiverInputInfo;
  receiver?: GiverReceiverInputInfo;
  description: string;
  amountInput: string;
  unitCode: string;
  // ... additional state properties
}

2. EntitySelectionStep.vue

Purpose: Complete step 1 interface for entity selection with dynamic labeling and context awareness.

Key Features:

  • Dynamic step labeling based on context

  • EntityGrid integration for unified display

  • Conflict detection and prevention

  • Special entity handling

  • Context preservation for navigation

Props:

interface EntitySelectionStepProps {
  stepType: 'giver' | 'recipient';
  giverEntityType: 'person' | 'project';
  recipientEntityType: 'person' | 'project';
  showProjects: boolean;
  isFromProjectView: boolean;
  projects: PlanData[];
  allContacts: Contact[];
  activeDid: string;
  allMyDids: string[];
  conflictChecker: (did: string) => boolean;
  fromProjectId?: string;
  toProjectId?: string;
  giver?: any;
  receiver?: any;
}

Computed Properties:

  • stepLabel - Dynamic label based on context

  • shouldShowProjects - Whether to show projects vs people

  • shouldShowYouEntity - Whether to show "You" option

  • youSelectable - Whether "You" can be selected

  • showAllRoute - Navigation route for "Show All"

  • showAllQueryParams - Query parameters for navigation

3. GiftDetailsStep.vue

Purpose: Complete step 2 interface for gift details with form validation and entity summaries.

Key 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.)

  • Conflict detection and warning display

  • Form validation and submission

Props:

interface GiftDetailsStepProps {
  giver: EntityData | null;
  receiver: EntityData | null;
  giverEntityType: 'person' | 'project';
  recipientEntityType: 'person' | 'project';
  description: string;
  amount: number;
  unitCode: string;
  prompt: string;
  isFromProjectView: boolean;
  hasConflict: boolean;
  offerId: string;
  fromProjectId: string;
  toProjectId: string;
}

Local State:

interface GiftDetailsLocalState {
  localDescription: string;
  localAmount: number;
  localUnitCode: string;
}

4. EntityGrid.vue

Purpose: Unified grid layout for displaying people, projects, and special entities with responsive design.

Key Features:

  • Responsive grid layout (3-6 columns based on entity type)

  • Special entity integration (You, Unnamed)

  • Conflict detection integration

  • Empty state messaging

  • Show All navigation

  • Event delegation for entity selection

Props:

interface EntityGridProps {
  entityType: 'people' | 'projects';
  entities: Contact[] | PlanData[];
  maxItems: number;
  activeDid: string;
  allMyDids: string[];
  allContacts: Contact[];
  conflictChecker: (did: string) => boolean;
  showYouEntity: boolean;
  youSelectable: boolean;
  showAllRoute: string;
  showAllQueryParams: Record<string, any>;
}

Grid Layout:

  • People: 4-6 columns (responsive)
  • Projects: 3-4 columns (responsive)
  • Special entities: Always shown for people grids
  • Show All: Shown when entities > maxItems

5. PersonCard.vue

Purpose: Individual person display with avatar, name, and selection states.

Key Features:

  • EntityIcon integration for avatar display

  • Conflict state visualization

  • Time icon display for activity context

  • Hover and selection states

  • Accessibility support

Props:

interface PersonCardProps {
  person: Contact;
  conflicted: boolean;
  showTimeIcon: boolean;
}

Visual States:

  • Normal: Default appearance

  • Conflicted: Red border, disabled state

  • Hovered: Elevated appearance

  • Selected: Highlighted border

6. ProjectCard.vue

Purpose: Individual project display with project icon, name, and issuer information.

Key Features:

  • ProjectIcon integration for project images

  • Issuer information display

  • Project name with ellipsis overflow

  • Selection state management

  • Accessibility support

Props:

interface ProjectCardProps {
  project: PlanData;
  activeDid: string;
  allMyDids: string[];
  allContacts: Contact[];
}

Display Elements:

  • Project icon/image

  • Project name (truncated)

  • Issuer information

  • Selection indicator

7. SpecialEntityCard.vue

Purpose: Special entity display for "You" and "Unnamed" with conflict detection.

Key Features:

  • FontAwesome icon integration

  • Conflict state handling

  • Conditional selectability

  • Consistent styling with other cards

  • Accessibility support

Props:

interface SpecialEntityCardProps {
  entityType: 'you' | 'unnamed';
  label: string;
  icon: string;
  selectable: boolean;
  conflicted: boolean;
  entityData: any;
}

Entity Types:

  • You: Represents the current user

  • Unnamed: Represents anonymous giver

8. ShowAllCard.vue

Purpose: Navigation component for "Show All" functionality with router integration.

Key Features:

  • Vue Router integration

  • Query parameter passing

  • Context preservation

  • Consistent styling

  • Icon-based navigation

Props:

interface ShowAllCardProps {
  entityType: 'people' | 'projects';
  routeName: string;
  queryParams: Record<string, any>;
}

Navigation Logic:

  • People → ContactGiftingView with context

  • Projects → DiscoverView

  • Query parameters preserve dialog state

9. EntitySummaryButton.vue

Purpose: Selected entity display with edit capability for step 2.

Key Features:

  • EntityIcon/ProjectIcon integration

  • Edit button functionality

  • Entity type detection

  • Consistent styling

  • Accessibility support

Props:

interface EntitySummaryButtonProps {
  entity: EntityData | null;
  entityType: 'person' | 'project';
  label: string;
  editable: boolean;
}

Display Elements:

  • Entity icon/avatar

  • Entity name

  • Edit button (when editable)

  • Type-specific styling

10. AmountInput.vue

Purpose: Specialized numeric input with increment/decrement controls and validation.

Key Features:

  • Increment/decrement buttons

  • Configurable min/max values and step size

  • Input validation and formatting

  • Disabled state handling for boundary values

  • v-model compatibility

Props:

interface AmountInputProps {
  value: number;
  min: number;
  max: number;
  step: number;
  inputId: string;
}

Key Methods:

  • increment() - Increase value by step

  • decrement() - Decrease value by step

  • handleInput() - Process direct input

  • handleBlur() - Validate on blur

Validation Logic:

  • Minimum/maximum bounds checking

  • Step size validation

  • Numeric input sanitization

  • Real-time feedback

Integration Patterns

1. Dialog Opening Pattern

// From any parent component
const giftedDialog = this.$refs.giftedDialog as GiftedDialog;

// Basic opening
giftedDialog.open();

// With pre-selected entities
giftedDialog.open(
  giverEntity,      // Pre-selected giver
  receiverEntity,   // Pre-selected receiver
  offerId,          // Offer context
  customTitle,      // Custom dialog title
  prompt,           // Custom input prompt
  successCallback   // Success handler
);

2. Context-Aware Opening

// From project view - project as giver
giftedDialog.open(
  projectEntity,    // Project as giver
  undefined,        // User selects receiver
  undefined,        // No offer
  "Gift from Project",
  "What did this project provide?"
);

// From project view - project as receiver
giftedDialog.open(
  undefined,        // User selects giver
  projectEntity,    // Project as receiver
  undefined,        // No offer
  "Gift to Project",
  "What was contributed to this project?"
);

3. Event Handling Pattern

// In parent component
export default class ParentComponent extends Vue {
  handleGiftRecorded(amount: number) {
    // Handle successful gift recording
    this.$notify({
      title: "Gift Recorded",
      text: `Successfully recorded gift of ${amount} hours`,
      type: "success"
    });

    // Refresh data if needed
    this.loadActivities();
  }

  openGiftDialog() {
    const dialog = this.$refs.giftedDialog as GiftedDialog;
    dialog.open(
      undefined,
      undefined,
      undefined,
      undefined,
      undefined,
      this.handleGiftRecorded
    );
  }
}

State Management

Internal State Flow

stateDiagram-v2
    [*] --> Closed
    Closed --> Step1 : open()
    Step1 --> Step2 : entity selected
    Step2 --> Step1 : edit entity
    Step2 --> Submitting : submit form
    Submitting --> Success : API success
    Submitting --> Error : API error
    Success --> Closed : auto-close
    Error --> Step2 : retry
    Step1 --> Closed : cancel
    Step2 --> Closed : cancel

Entity Type Determination

flowchart TD
    A[Dialog Opens] --> B{fromProjectId set?}
    B -->|Yes| C[Giver = Project]
    B -->|No| D{toProjectId set?}
    D -->|Yes| E[Receiver = Project]
    D -->|No| F{showProjects true?}
    F -->|Yes| G[Show Project Selection]
    F -->|No| H[Show Person Selection]
    C --> I[Show Person Selection for Receiver]
    E --> J[Show Person Selection for Giver]
    G --> K[User Selects Project Type]
    H --> L[User Selects Person]

Conflict Detection Logic

// Conflict detection algorithm
function wouldCreateConflict(selectedDid: string): boolean {
  // Only applies to person-to-person gifts
  if (giverEntityType !== "person" || recipientEntityType !== "person") {
    return false;
  }

  // Check if selecting same person for both roles
  if (stepType === "giver") {
    return receiver?.did === selectedDid;
  } else if (stepType === "recipient") {
    return giver?.did === selectedDid;
  }

  return false;
}

Event Flow

Entity Selection Events

sequenceDiagram
    participant Card as PersonCard/ProjectCard
    participant Grid as EntityGrid
    participant Step as EntitySelectionStep
    participant Dialog as GiftedDialog

    Card->>Grid: emit entity-selected
    Grid->>Step: emit entity-selected
    Step->>Dialog: emit entity-selected
    Dialog->>Dialog: handleEntitySelected()
    Dialog->>Dialog: updateState()
    Dialog->>Dialog: advanceToStep2()

Form Submission Events

sequenceDiagram
    participant Form as GiftDetailsStep
    participant Dialog as GiftedDialog
    participant API as Endorser API
    participant Parent as Parent Component

    Form->>Dialog: emit submit
    Dialog->>Dialog: handleSubmit()
    Dialog->>API: createAndSubmitGive()
    API->>Dialog: response
    Dialog->>Dialog: handleSuccess/Error()
    Dialog->>Parent: callbackOnSuccess()
    Dialog->>Dialog: close()

Amount Input Events

sequenceDiagram
    participant Input as AmountInput
    participant Step as GiftDetailsStep
    participant Dialog as GiftedDialog

    Input->>Step: emit update:value
    Step->>Dialog: emit update:amount
    Dialog->>Dialog: handleAmountUpdate()
    Dialog->>Dialog: updateAmountInput()

Usage Examples

Basic Usage

<template>
  <div>
    <button @click="openGiftDialog">Record Gift</button>

    <GiftedDialog
      ref="giftedDialog"
      :show-projects="false"
      :is-from-project-view="false"
    />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";

@Component({
  components: { GiftedDialog }
})
export default class BasicExample extends Vue {
  openGiftDialog() {
    const dialog = this.$refs.giftedDialog as GiftedDialog;
    dialog.open();
  }
}
</script>

Project Context Usage

<template>
  <div>
    <button @click="recordGiftFromProject">Gift from Project</button>
    <button @click="recordGiftToProject">Gift to Project</button>

    <GiftedDialog
      ref="giftedDialog"
      :from-project-id="project.handleId"
      :show-projects="true"
      :is-from-project-view="true"
    />
  </div>
</template>

<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { PlanData } from "@/interfaces/records";

@Component({
  components: { GiftedDialog }
})
export default class ProjectExample extends Vue {
  @Prop({ required: true })
  project!: PlanData;

  recordGiftFromProject() {
    const dialog = this.$refs.giftedDialog as GiftedDialog;
    const projectEntity = {
      did: this.project.handleId,
      name: this.project.name,
      handleId: this.project.handleId,
      image: this.project.image
    };

    dialog.open(
      projectEntity,
      undefined,
      undefined,
      "Gift from Project",
      "What did this project provide?",
      this.handleGiftSuccess
    );
  }

  recordGiftToProject() {
    const dialog = this.$refs.giftedDialog as GiftedDialog;
    const projectEntity = {
      did: this.project.handleId,
      name: this.project.name,
      handleId: this.project.handleId,
      image: this.project.image
    };

    dialog.open(
      undefined,
      projectEntity,
      undefined,
      "Gift to Project",
      "What was contributed to this project?",
      this.handleGiftSuccess
    );
  }

  handleGiftSuccess(amount: number) {
    this.$notify({
      title: "Success",
      text: `Recorded ${amount} hour gift`,
      type: "success"
    });
  }
}
</script>

Advanced Integration

<template>
  <div>
    <GiftedDialog
      ref="giftedDialog"
      :show-projects="showProjectsInDialog"
      :is-from-project-view="isProjectContext"
      :from-project-id="contextProjectId"
      :to-project-id="recipientProjectId"
    />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue";
import { GiverReceiverInputInfo } from "@/libs/util";

@Component({
  components: { GiftedDialog }
})
export default class AdvancedExample extends Vue {
  private contextProjectId = "";
  private recipientProjectId = "";

  get showProjectsInDialog(): boolean {
    return this.$route.name === "projects" || this.hasProjectContext;
  }

  get isProjectContext(): boolean {
    return !!this.contextProjectId || !!this.recipientProjectId;
  }

  get hasProjectContext(): boolean {
    return this.$route.params.projectId !== undefined;
  }

  async openContextualDialog() {
    const dialog = this.$refs.giftedDialog as GiftedDialog;

    // Determine context from route
    if (this.$route.name === "project-view") {
      this.contextProjectId = this.$route.params.projectId as string;
    }

    // Load project data if needed
    let projectEntity: GiverReceiverInputInfo | undefined;
    if (this.contextProjectId) {
      projectEntity = await this.loadProjectEntity(this.contextProjectId);
    }

    // Open with appropriate context
    dialog.open(
      projectEntity,
      undefined,
      this.$route.query.offerId as string,
      this.generateContextualTitle(),
      this.generateContextualPrompt(),
      this.handleContextualSuccess
    );
  }

  private async loadProjectEntity(projectId: string): Promise<GiverReceiverInputInfo> {
    // Load project data from API or database
    const project = await this.fetchProject(projectId);
    return {
      did: project.handleId,
      name: project.name,
      handleId: project.handleId,
      image: project.image
    };
  }

  private generateContextualTitle(): string {
    if (this.contextProjectId) {
      return "Record Project Gift";
    }
    return "Record Gift";
  }

  private generateContextualPrompt(): string {
    if (this.contextProjectId) {
      return "What did this project provide or receive?";
    }
    return "What was given?";
  }

  private handleContextualSuccess(amount: number) {
    // Handle success with context awareness
    this.$notify({
      title: "Gift Recorded",
      text: `Successfully recorded ${amount} hour gift`,
      type: "success"
    });

    // Navigate or refresh based on context
    if (this.$route.name === "project-view") {
      this.$router.push({ name: "project-activities" });
    } else {
      this.refreshActivities();
    }
  }
}
</script>

Testing Strategy

Unit Testing

// AmountInput.spec.ts
import { mount } from '@vue/test-utils';
import AmountInput from '@/components/AmountInput.vue';

describe('AmountInput', () => {
  it('increments value when increment button clicked', async () => {
    const wrapper = mount(AmountInput, {
      props: { value: 5, min: 0, max: 10, step: 1 }
    });

    const incrementButton = wrapper.find('[data-testid="increment-button"]');
    await incrementButton.trigger('click');

    expect(wrapper.emitted('update:value')).toBeTruthy();
    expect(wrapper.emitted('update:value')![0]).toEqual([6]);
  });

  it('disables decrement button at minimum value', () => {
    const wrapper = mount(AmountInput, {
      props: { value: 0, min: 0, max: 10, step: 1 }
    });

    const decrementButton = wrapper.find('[data-testid="decrement-button"]');
    expect(decrementButton.attributes('disabled')).toBeDefined();
  });
});

Integration Testing

// GiftedDialog.spec.ts
import { mount } from '@vue/test-utils';
import GiftedDialog from '@/components/GiftedDialog.vue';

describe('GiftedDialog Integration', () => {
  it('completes full gift recording workflow', async () => {
    const wrapper = mount(GiftedDialog, {
      props: { showProjects: false }
    });

    // Open dialog
    await wrapper.vm.open();
    expect(wrapper.vm.visible).toBe(true);
    expect(wrapper.vm.currentStep).toBe(1);

    // Select giver
    const personCard = wrapper.find('[data-testid="person-card-0"]');
    await personCard.trigger('click');

    // Should advance to step 2
    expect(wrapper.vm.currentStep).toBe(2);
    expect(wrapper.vm.giver).toBeDefined();

    // Fill form
    const descriptionInput = wrapper.find('[data-testid="description-input"]');
    await descriptionInput.setValue('Test gift');

    const amountInput = wrapper.find('[data-testid="amount-input"]');
    await amountInput.setValue('5');

    // Submit form
    const submitButton = wrapper.find('[data-testid="submit-button"]');
    await submitButton.trigger('click');

    // Should emit success event
    expect(wrapper.emitted('gift-recorded')).toBeTruthy();
  });
});

End-to-End Testing

// giftedDialog.e2e.ts
import { test, expect } from '@playwright/test';

test('Gift recording workflow', async ({ page }) => {
  await page.goto('/');

  // Open gift dialog
  await page.click('[data-testid="record-gift-button"]');

  // Step 1: Select entities
  await expect(page.locator('.dialog')).toBeVisible();
  await expect(page.locator('text=Choose a person received from:')).toBeVisible();

  // Select a person
  await page.click('[data-testid="person-card"]:first-child');

  // Step 2: Fill details
  await expect(page.locator('text=What was given?')).toBeVisible();
  await page.fill('[data-testid="description-input"]', 'Helped with coding');

  // Increment amount
  await page.click('[data-testid="increment-button"]');
  await page.click('[data-testid="increment-button"]');

  // Submit
  await page.click('[data-testid="submit-button"]');

  // Verify success
  await expect(page.locator('.notification.success')).toBeVisible();
  await expect(page.locator('.dialog')).not.toBeVisible();
});

Performance Considerations

Optimization Strategies

  1. Lazy Loading: Components are loaded only when needed

  2. Virtual Scrolling: For large entity lists

  3. Debounced Input: Amount input changes are debounced

  4. Computed Properties: Efficient reactive calculations

  5. Event Delegation: Minimal event listeners

Memory Management

// Cleanup in component destruction
export default class GiftedDialog extends Vue {
  beforeUnmount() {
    // Clear references
    this.callbackOnSuccess = undefined;
    this.allContacts = [];
    this.projects = [];

    // Cancel pending API requests
    this.cancelPendingRequests();
  }
}

Bundle Size Optimization

  • Tree-shaking for unused FontAwesome icons

  • Dynamic imports for heavy components

  • Shared dependencies between components

  • Minimal external dependencies

Accessibility Features

Keyboard Navigation

  • Tab order follows visual flow

  • Enter/Space for button activation

  • Escape to close dialog

  • Arrow keys for amount input

Screen Reader Support

<template>
  <div
    role="dialog"
    aria-labelledby="dialog-title"
    aria-modal="true"
    aria-describedby="dialog-description"
  >
    <h2 id="dialog-title">Record Gift</h2>
    <p id="dialog-description">
      Choose who gave and received the gift, then provide details.
    </p>

    <!-- Form elements with proper labels -->
    <label for="description-input">Gift Description</label>
    <input
      id="description-input"
      aria-describedby="description-help"
      aria-required="true"
    />
    <div id="description-help">
      Describe what was given or the service provided
    </div>
  </div>
</template>

Focus Management

export default class GiftedDialog extends Vue {
  private previousFocus: HTMLElement | null = null;

  open() {
    // Store current focus
    this.previousFocus = document.activeElement as HTMLElement;

    // Show dialog
    this.visible = true;

    // Focus first interactive element
    this.$nextTick(() => {
      const firstInput = this.$el.querySelector('button, input, select');
      if (firstInput) {
        (firstInput as HTMLElement).focus();
      }
    });
  }

  cancel() {
    this.visible = false;

    // Restore previous focus
    if (this.previousFocus) {
      this.previousFocus.focus();
    }
  }
}

Security Considerations

Input Validation

// Sanitize and validate all inputs
validateDescription(description: string): boolean {
  // Remove potentially dangerous characters
  const sanitized = description.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');

  // Check length limits
  if (sanitized.length > 500) {
    return false;
  }

  return true;
}

validateAmount(amount: number): boolean {
  // Check numeric bounds
  if (amount < 0 || amount > 10000) {
    return false;
  }

  // Check for valid number
  if (!Number.isFinite(amount)) {
    return false;
  }

  return true;
}

DID Privacy Protection

// Ensure DIDs are only shared with authorized contacts
function shouldShowDid(contact: Contact, currentUser: string): boolean {
  // Only show DID if user has explicitly authorized this contact
  return contact.registeredByDid === currentUser ||
         contact.did === currentUser ||
         contact.isPublic === true;
}

API Security

// Secure API communication
async function submitGift(giftData: GiftData): Promise<void> {
  // Validate data before sending
  if (!this.validateGiftData(giftData)) {
    throw new Error('Invalid gift data');
  }

  // Sign request with user's private key
  const signedRequest = await this.signRequest(giftData);

  // Send to endorser server with proper headers
  const response = await fetch('/api/claims', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${this.getAuthToken()}`,
      'X-API-Version': '1.0'
    },
    body: JSON.stringify(signedRequest)
  });

  if (!response.ok) {
    throw new Error('Failed to submit gift');
  }
}

Conclusion

The GiftedDialog system represents a sophisticated, well-architected solution for gift recording in the TimeSafari application. Through careful decomposition into focused components, comprehensive state management, and thoughtful user experience design, it provides a robust foundation for community gift tracking while maintaining privacy, security, and accessibility standards.

The modular architecture ensures maintainability and testability, while the flexible integration patterns allow for seamless embedding in various contexts throughout the application. The comprehensive documentation and testing strategies ensure long-term sustainability and ease of development.

Author: Matthew Raymer Last Updated: 2025-06-30 Version: 1.0.0