30 KiB
GiftedDialog Complete Documentation
Table of Contents
- Overview
- Architecture Diagram
- Component Hierarchy
- Data Flow Diagrams
- Component Documentation
- Integration Patterns
- State Management
- Event Flow
- Usage Examples
- 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
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 dialogcancel()
- Close dialog and reset stateconfirm()
- Submit gift and handle responsehandleEntitySelected()
- Process entity selection from step 1handleSubmit()
- 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 contextshouldShowProjects
- Whether to show projects vs peopleshouldShowYouEntity
- Whether to show "You" optionyouSelectable
- Whether "You" can be selectedshowAllRoute
- 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 stepdecrement()
- Decrease value by stephandleInput()
- Process direct inputhandleBlur()
- 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
- Lazy Loading: Components are loaded only when needed
- Virtual Scrolling: For large entity lists
- Debounced Input: Amount input changes are debounced
- Computed Properties: Efficient reactive calculations
- 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