Skip to content

[Refactoring] Components: Extract duplicated time formatting logic into shared utility #231

@syed-reza98

Description

@syed-reza98

Problem

Multiple components throughout the codebase duplicate the same relative time formatting logic (e.g., "5m ago", "2h ago", "3d ago"). This logic is copy-pasted across at least 3 files with slight variations, making it:

  • Difficult to maintain (bug fixes must be applied in multiple places)
  • Inconsistent (different components may show different formats)
  • Harder to internationalize (i18n would need to be added 3+ times)
  • Error-prone (easy to miss an update when changing the logic)

Current Code Location

  • Files affected:
    • src/components/dashboard/storefront/editor/version-history-panel.tsx (lines 40-50)
    • src/components/integrations/facebook/messenger-inbox.tsx (lines 120-130)
    • src/components/integrations/facebook/message-thread.tsx (lines 85-95)
  • Pattern: Each file contains 10-15 lines of duplicated time calculation logic
  • Complexity: Low - Simple utility extraction

Proposed Refactoring

Create a centralized time formatting utility that all components can import and use. This provides a single source of truth for time formatting logic across the application.

Benefits

  • Single source of truth - Time formatting logic in one place
  • Easier to maintain - Bug fixes and improvements happen once
  • Consistent UX - All components show times in the same format
  • Easier to internationalize - Add i18n support in one place
  • Better testability - Utility function can be thoroughly unit tested
  • Reduced code - Remove ~30 lines of duplicated code

Suggested Approach

  1. Create a shared utility function in src/lib/utils/time.ts:

    /**
     * Format a date into a human-readable relative time string
     * `@param` date - Date to format (Date object or ISO string)
     * `@param` options - Formatting options
     * `@returns` Formatted string like "5m ago", "2h ago", "3d ago"
     * 
     * `@example`
     * formatRelativeTime(new Date()) // "Just now"
     * formatRelativeTime('2024-02-01T10:00:00Z') // "2h ago"
     */
    export function formatRelativeTime(
      date: Date | string,
      options: {
        justNowThreshold?: number; // milliseconds (default: 60000 = 1 min)
        maxDays?: number; // show actual date after this many days (default: 7)
        locale?: string; // for future i18n support
      } = {}
    ): string {
      const {
        justNowThreshold = 60000,
        maxDays = 7,
      } = options;
    
      const targetDate = typeof date === 'string' ? new Date(date) : date;
      const now = new Date();
      const diffMs = now.getTime() - targetDate.getTime();
    
      // Just now
      if (diffMs < justNowThreshold) {
        return 'Just now';
      }
    
      // Minutes
      const diffMins = Math.floor(diffMs / 60000);
      if (diffMins < 60) {
        return `\$\{diffMins}m ago`;
      }
    
      // Hours
      const diffHours = Math.floor(diffMs / 3600000);
      if (diffHours < 24) {
        return `\$\{diffHours}h ago`;
      }
    
      // Days
      const diffDays = Math.floor(diffMs / 86400000);
      if (diffDays < maxDays) {
        return `\$\{diffDays}d ago`;
      }
    
      // Fallback to formatted date
      return targetDate.toLocaleDateString(undefined, {
        month: 'short',
        day: 'numeric',
        year: targetDate.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
      });
    }
    
    /**
     * Calculate time remaining in a 24-hour window
     * Useful for message response windows, etc.
     * `@param` date - Starting date
     * `@returns` Remaining hours or 0 if outside window
     */
    export function calculateTimeWindow(date: Date | string): number {
      const targetDate = typeof date === 'string' ? new Date(date) : date;
      const diffMs = Date.now() - targetDate.getTime();
      const diffHours = diffMs / (1000 * 60 * 60);
      const isWithinWindow = diffHours < 24;
      return isWithinWindow ? Math.floor(24 - diffHours) : 0;
    }
  2. Refactor version-history-panel.tsx:

    // BEFORE:
    const formatRelativeTime = (dateStr: string) => {
      const date = new Date(dateStr);
      const now = new Date();
      const diffMs = now.getTime() - date.getTime();
      const diffMin = Math.floor(diffMs / 60000);
      const diffHour = Math.floor(diffMs / 3600000);
      const diffDay = Math.floor(diffMs / 86400000);
      
      if (diffMin < 1) return 'Just now';
      if (diffMin < 60) return `\$\{diffMin}m ago`;
      if (diffHour < 24) return `\$\{diffHour}h ago`;
      if (diffDay < 7) return `\$\{diffDay}d ago`;
      return date.toLocaleDateString();
    };
    
    // AFTER:
    import { formatRelativeTime } from '@/lib/utils/time';
    // Use directly: formatRelativeTime(version.publishedAt)
  3. Refactor messenger-inbox.tsx:

    // BEFORE:
    const diffMins = Math.floor(diffMs / 60000);
    const diffHours = Math.floor(diffMs / 3600000);
    const diffDays = Math.floor(diffMs / 86400000);
    
    if (diffMins < 1) return 'Just now';
    if (diffMins < 60) return `\$\{diffMins}m ago`;
    if (diffHours < 24) return `\$\{diffHours}h ago`;
    if (diffDays < 7) return `\$\{diffDays}d ago`;
    return date.toLocaleDateString();
    
    // AFTER:
    import { formatRelativeTime } from '@/lib/utils/time';
    // Use directly in render
  4. Refactor message-thread.tsx to use calculateTimeWindow():

    // BEFORE:
    const diffHours = diffMs / (1000 * 60 * 60);
    const isWithinWindow = diffHours < 24;
    const remaining = isWithinWindow ? Math.floor(24 - diffHours) : 0;
    
    // AFTER:
    import { calculateTimeWindow } from '@/lib/utils/time';
    const remaining = calculateTimeWindow(message.createdAt);

Impact Assessment

  • Effort: Low (1-2 hours) - Simple utility extraction and imports
  • Risk: Very Low - Pure refactoring, easy to test, no logic changes
  • Benefit: Medium - Cleaner code, easier maintenance, consistency
  • Priority: Medium - Nice improvement but not critical

Related Files

Other components that might display relative times (check for similar patterns):

  • src/components/user-notification-bell.tsx
  • src/components/admin/notifications-list.tsx
  • src/components/admin/notification-bell.tsx
  • Any audit log or activity feed components

Testing Strategy

  1. Create unit tests for formatRelativeTime():

    describe('formatRelativeTime', () => {
      it('shows "Just now" for very recent times', () => {
        const now = new Date();
        expect(formatRelativeTime(now)).toBe('Just now');
      });
    
      it('shows minutes for times < 1 hour ago', () => {
        const date = new Date(Date.now() - 30 * 60000);
        expect(formatRelativeTime(date)).toBe('30m ago');
      });
    
      it('shows hours for times < 24 hours ago', () => {
        const date = new Date(Date.now() - 5 * 3600000);
        expect(formatRelativeTime(date)).toBe('5h ago');
      });
    
      it('shows days for times < 7 days ago', () => {
        const date = new Date(Date.now() - 3 * 86400000);
        expect(formatRelativeTime(date)).toBe('3d ago');
      });
    
      it('shows formatted date for times > 7 days ago', () => {
        const date = new Date(Date.now() - 10 * 86400000);
        expect(formatRelativeTime(date)).toMatch(/[A-Z][a-z]{2} \d{1,2}/);
      });
    });
  2. Visual regression testing:

    • Version history panel shows correct relative times
    • Messenger inbox shows correct message times
    • Message thread shows correct response window
  3. Manual testing:

    • Check all affected components render correctly
    • Verify time formats are consistent across the app

Success Metrics

  • ✅ Time formatting logic centralized in src/lib/utils/time.ts
  • ✅ All 3+ affected components use the shared utility
  • ✅ ~30 lines of duplicated code removed
  • ✅ Unit tests achieve 100% coverage of time utility
  • ✅ Consistent time formatting across all components
  • ✅ Future i18n support is easier to implement

AI generated by Daily Codebase Analyzer - Semantic Function Extraction & Refactoring

  • expires on Feb 28, 2026, 1:41 PM UTC

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions