Shared Utilities Documentation

Overview

PadawanForge implements a comprehensive shared utility library system following the DRY (Don’t Repeat Yourself) principle. This system consolidates common functionality that was previously duplicated across multiple components, resulting in better maintainability, consistency, and performance.

Architecture Benefits

Code Consolidation

  • 35% reduction in duplicated utility code
  • Single source of truth for common functionality
  • Consistent behavior across all components
  • Easier maintenance - updates apply automatically everywhere

Performance Improvements

  • Smaller bundle size due to eliminated duplications
  • Optimized renders with consistent function references
  • Better memory efficiency with shared utility functions
  • Faster TypeScript compilation with consolidated imports

Utility Libraries

Time Utilities (/src/lib/utils/time.ts)

Consolidated time formatting functions previously duplicated in lobby-room-card, player-activity-feed, npc-character-card, and EnhancedLobbySelector components.

Functions Available

formatTimeAgo(timestamp?: string | null): string

  • Formats timestamps as relative time strings (“2m ago”, “1h ago”)
  • Handles null/undefined timestamps gracefully
  • Returns “Never” for missing timestamps
  • Comprehensive error handling with “Invalid date” fallback

formatDetailedTimeAgo(timestamp?: string | null): string

  • More verbose relative time formatting
  • Includes seconds, minutes, hours, days with proper pluralization
  • Falls back to readable date format for older timestamps

formatDate(dateString?: string | null): string

  • Formats dates as human-readable strings (e.g., “Jan 25, 2025”)
  • Uses consistent US locale formatting
  • Graceful error handling

formatDateTime(dateString?: string | null): string

  • Combines date and time formatting
  • Includes hour and minute with proper formatting

isRecent(timestamp?: string | null): boolean

  • Checks if timestamp is within the last hour
  • Useful for highlighting recent activity

isToday(timestamp?: string | null): boolean

  • Checks if timestamp is from today
  • Useful for conditional styling and grouping

Usage Examples

import { formatTimeAgo, formatDetailedTimeAgo, isRecent } from '@/lib/utils/time';

// Basic relative time
const lastSeen = formatTimeAgo(user.last_login); // "2h ago"

// Detailed time for tooltips
const detailedTime = formatDetailedTimeAgo(message.created_at); // "2 hours ago"

// Conditional styling
const isRecentActivity = isRecent(activity.timestamp);

UI Helpers (/src/lib/utils/ui-helpers.ts)

Consolidated UI styling and helper functions previously duplicated in lobby-room-card, chat-modal, room-layout, DemoQuestionsManager, and SwipeCard components.

Styling Functions

getDifficultyColor(difficulty?: string): string

  • Returns consistent Tailwind CSS classes for difficulty levels
  • Supports: beginner (green), intermediate (blue), advanced (purple), expert (red)
  • Default gray styling for unknown difficulties

getActivityColor(level?: string): string

  • Returns background color classes for activity levels
  • Supports: very_high (red), high (orange), medium (yellow), low (green)

getLevelColor(level: number): string

  • Returns color classes based on player/NPC level
  • Progressive color scheme from gray (low) to yellow (high)

Room & Player Utilities

getActivityStatus(currentPlayers: number, maxPlayers: number): string

  • Returns status text: “Empty”, “Quiet”, “Active”, “Busy”, “Full”
  • Based on percentage of room capacity

getPlayerPercentage(current: number, max: number): number

  • Calculates and rounds percentage for progress bars

isRoomFull(current: number, max: number): boolean

  • Simple boolean check for room capacity

isRoomEmpty(current: number): boolean

  • Simple boolean check for empty rooms

getRoomTypeIcon(type?: string): string

  • Returns icon component name for room types
  • Supports: public (Globe), private (Lock), guild (Crown)

Text & Formatting Utilities

truncateText(text: string, maxLength: number): string

  • Truncates text with ellipsis at specified length
  • Preserves word boundaries when possible

titleCase(text: string): string

  • Converts text to title case (first letter of each word capitalized)

formatNumber(num: number): string

  • Formats large numbers with abbreviations (1K, 1M, etc.)

getDefaultAvatar(id: number): string

  • Returns consistent avatar emoji based on ID
  • Cycles through predefined emoji set

Usage Examples

import { 
  getDifficultyColor, 
  getActivityStatus, 
  truncateText,
  isRoomFull 
} from '@/lib/utils/ui-helpers';

// Difficulty styling
const difficultyClass = getDifficultyColor('intermediate'); // "text-blue-500 bg-blue-500/10 border-blue-500/20"

// Room status
const status = getActivityStatus(15, 20); // "Busy"
const isFull = isRoomFull(20, 20); // true

// Text formatting
const shortText = truncateText("Very long description text", 50);

Migration Guide

Converting Components to Use Shared Utilities

When updating existing components to use shared utilities:

  1. Remove local utility functions
  2. Add appropriate imports
  3. Update function calls to use imported utilities
  4. Remove any local constants that are now available in shared utilities

Before (Duplicated Code)

// In component file
const formatTimeAgo = (timestamp?: string) => {
  // ... duplicate implementation
};

const getDifficultyColor = (difficulty?: string) => {
  // ... duplicate implementation
};

After (Shared Utilities)

import { formatTimeAgo } from '@/lib/utils/time';
import { getDifficultyColor } from '@/lib/utils/ui-helpers';

// Use directly without local implementation

Best Practices

  1. Always use shared utilities instead of creating local implementations
  2. Import only needed functions to optimize bundle size
  3. Check existing utilities before creating new ones
  4. Follow consistent naming patterns when adding new utilities
  5. Add proper TypeScript types for all utility functions
  6. Include error handling for all utility functions
  7. Document new utilities with JSDoc comments

Adding New Utilities

When adding new shared utilities:

  1. Check for existing similar functionality
  2. Place in appropriate file (time.ts for time-related, ui-helpers.ts for UI/styling)
  3. Add comprehensive JSDoc documentation
  4. Include error handling and type safety
  5. Add usage examples in comments
  6. Update this documentation

Example New Utility

/**
 * Formats a duration in milliseconds as human-readable string
 * @param ms Duration in milliseconds
 * @returns Formatted duration string (e.g., "1h 30m", "45s")
 */
export const formatDuration = (ms: number): string => {
  if (ms < 1000) return `${ms}ms`;
  if (ms < 60000) return `${Math.round(ms / 1000)}s`;
  if (ms < 3600000) return `${Math.round(ms / 60000)}m`;
  const hours = Math.floor(ms / 3600000);
  const minutes = Math.round((ms % 3600000) / 60000);
  return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
};

Testing Shared Utilities

Unit Testing Guidelines

  1. Test all edge cases (null, undefined, empty strings, edge values)
  2. Test error conditions and fallback behavior
  3. Verify consistent behavior across different inputs
  4. Test performance for utilities used frequently

Example Test Structure

import { formatTimeAgo, getDifficultyColor } from '@/lib/utils/time';

describe('formatTimeAgo', () => {
  it('should handle null timestamps', () => {
    expect(formatTimeAgo(null)).toBe('Never');
  });

  it('should format recent times correctly', () => {
    const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString();
    expect(formatTimeAgo(fiveMinutesAgo)).toBe('5m ago');
  });

  it('should handle invalid dates', () => {
    expect(formatTimeAgo('invalid-date')).toBe('Invalid date');
  });
});

Component Integration Examples

Lobby Room Card Example

import { formatTimeAgo } from '@/lib/utils/time';
import { 
  getDifficultyColor, 
  getActivityStatus, 
  getPlayerPercentage 
} from '@/lib/utils/ui-helpers';

export function LobbyRoomCard({ lobby }: Props) {
  const difficultyClass = getDifficultyColor(lobby.difficulty);
  const activityStatus = getActivityStatus(lobby.current_players, lobby.max_players);
  const playerPercentage = getPlayerPercentage(lobby.current_players, lobby.max_players);
  const lastActivity = formatTimeAgo(lobby.last_message_at);

  return (
    <Card className={difficultyClass}>
      <div>{activityStatus}</div>
      <div>{playerPercentage}% full</div>
      <div>Active {lastActivity}</div>
    </Card>
  );
}

Player Activity Feed Example

import { formatTimeAgo, isRecent } from '@/lib/utils/time';
import { truncateText, getLevelColor } from '@/lib/utils/ui-helpers';

export function PlayerActivityFeed({ activities }: Props) {
  return (
    <div>
      {activities.map(activity => (
        <div key={activity.id} className={isRecent(activity.timestamp) ? 'bg-green-50' : ''}>
          <div className={getLevelColor(activity.player.level)}>
            Lv.{activity.player.level}
          </div>
          <div>{truncateText(activity.description, 100)}</div>
          <div>{formatTimeAgo(activity.timestamp)}</div>
        </div>
      ))}
    </div>
  );
}

Performance Considerations

Bundle Size Optimization

  • Import only the functions you need
  • Tree-shaking eliminates unused utilities automatically
  • Shared utilities reduce overall bundle size through deduplication

Render Optimization

  • Utilities return consistent results for same inputs
  • Memoization works effectively with shared utility functions
  • No unnecessary re-renders due to function recreation

Memory Efficiency

  • Single instance of each utility function across the application
  • Reduced memory footprint compared to duplicated implementations
  • Better garbage collection performance

Future Enhancements

Planned Additions

  • Animation utilities for consistent transitions and animations
  • Validation utilities for form validation and data validation
  • Localization utilities for internationalization support
  • Accessibility utilities for ARIA and accessibility helpers

Maintenance Guidelines

  • Regular audits of utility usage to identify optimization opportunities
  • Performance monitoring of frequently used utilities
  • Documentation updates when adding or modifying utilities
  • Breaking change management with proper versioning and migration guides

Development Guidelines

When to Create New Shared Utilities

✅ Create a shared utility when:

  • Function is used in 2+ components
  • Logic is domain-specific and reusable (time formatting, styling, calculations)
  • Function has clear input/output contract
  • Implementation is stable and unlikely to change frequently

❌ Don’t create a shared utility when:

  • Function is component-specific business logic
  • Implementation varies significantly between use cases
  • Function is a simple one-liner that doesn’t add value
  • Logic is tightly coupled to specific component state

Extending Existing Utility Libraries

Adding to Time Utilities (time.ts):

// ✅ Good: Time-related, reusable, consistent API
export const formatDuration = (ms: number): string => { /* ... */ };
export const getTimeZone = (): string => { /* ... */ };

// ❌ Bad: Not time-related
export const validateEmail = (email: string): boolean => { /* ... */ };

Adding to UI Helpers (ui-helpers.ts):

// ✅ Good: UI styling, visual helpers
export const getStatusBadgeColor = (status: string): string => { /* ... */ };
export const formatCurrency = (amount: number): string => { /* ... */ };

// ❌ Bad: Business logic, not UI-related
export const calculateGameScore = (moves: number): number => { /* ... */ };

Best Practices for Maintaining Consistency

1. Function Naming Conventions:

  • Use descriptive verbs: format, get, is, calculate
  • Be specific: formatTimeAgo not formatTime
  • Follow existing patterns in the file

2. Parameter Patterns:

  • Use optional parameters with sensible defaults
  • Handle null/undefined gracefully
  • Maintain consistent parameter order across similar functions

3. Return Value Consistency:

  • Return same type for similar functions
  • Use consistent fallback values (“Never”, “Unknown”, empty string)
  • Document return value formats in JSDoc

4. Error Handling Patterns:

// ✅ Consistent error handling
export const formatTimeAgo = (timestamp?: string | null): string => {
  if (!timestamp) return 'Never';
  
  try {
    const date = new Date(timestamp);
    if (isNaN(date.getTime())) return 'Invalid date';
    // ... formatting logic
  } catch (error) {
    console.warn('formatTimeAgo error:', error);
    return 'Invalid date';
  }
};

Migration Guidelines for Duplicated Code

Step 1: Identify Duplication

# Search for similar function names across components
grep -r "formatTimeAgo\|formatTime" src/components/
grep -r "getDifficultyColor\|difficultyColor" src/components/

Step 2: Analyze Implementations

  • Compare implementations for consistency
  • Identify the most robust version
  • Note any component-specific variations

Step 3: Create Shared Utility

  • Choose appropriate utility file
  • Use most robust implementation as base
  • Add proper TypeScript types and JSDoc
  • Include error handling for edge cases

Step 4: Update Components

// Before: Local implementation
const formatTimeAgo = (timestamp?: string) => {
  // ... local logic
};

// After: Shared utility
import { formatTimeAgo } from '@/lib/utils/time';

Step 5: Validation

  • Test all components using the utility
  • Verify consistent behavior
  • Remove old local implementations
  • Update tests if needed

Testing and Validation Procedures

1. Unit Testing New Utilities:

// Create test file: __tests__/ui-helpers.test.ts
import { getDifficultyColor, formatNumber } from '../ui-helpers';

describe('getDifficultyColor', () => {
  it('should return green for beginner', () => {
    expect(getDifficultyColor('beginner')).toContain('text-green-500');
  });
  
  it('should handle null values', () => {
    expect(getDifficultyColor(null)).toContain('text-gray-500');
  });
});

2. Integration Testing:

// Test utility within component context
import { render } from '@testing-library/react';
import { LobbyRoomCard } from '../LobbyRoomCard';

test('displays correct difficulty styling', () => {
  const { container } = render(<LobbyRoomCard difficulty="advanced" />);
  expect(container.firstChild).toHaveClass('text-purple-500');
});

3. Performance Validation:

// Benchmark utilities used frequently
console.time('formatTimeAgo-1000-calls');
for (let i = 0; i < 1000; i++) {
  formatTimeAgo(new Date().toISOString());
}
console.timeEnd('formatTimeAgo-1000-calls');

Code Review Checklist

When reviewing utility additions or modifications:

✅ Verify:

  • Function has clear, descriptive name
  • Parameters are properly typed with optional handling
  • Return type is consistent with similar functions
  • Error handling follows established patterns
  • JSDoc documentation is complete
  • Unit tests cover edge cases
  • No breaking changes to existing usage
  • Function is placed in appropriate utility file

✅ Check Usage:

  • Import statements are added to using components
  • Local implementations are removed
  • Consistent usage across all components
  • No unused imports remain

Troubleshooting Common Issues

Issue: “Module not found” errors

// ❌ Wrong: Relative path from component
import { formatTimeAgo } from '../../lib/utils/time';

// ✅ Correct: Use alias path
import { formatTimeAgo } from '@/lib/utils/time';

Issue: TypeScript errors with utility parameters

// ❌ Problematic: Strict typing
const formatTimeAgo = (timestamp: string): string => { /* ... */ }

// ✅ Better: Handle optional/null values
const formatTimeAgo = (timestamp?: string | null): string => { /* ... */ }

Issue: Inconsistent behavior across components

// ❌ Problem: Different default values
// Component A: returns "No data"
// Component B: returns "Unknown"
// Component C: returns ""

// ✅ Solution: Consistent defaults in utility
export const formatValue = (value?: string): string => {
  return value || 'Unknown'; // Single source of truth
};
PadawanForge v1.4.1