Files
thrilltrack-explorer/docs/moderation/COMPONENTS.md
2025-11-02 21:00:22 +00:00

11 KiB

Moderation Queue Components

Component Reference

ModerationQueue (Root Component)

Location: src/components/moderation/ModerationQueue.tsx

Purpose: Root component for moderation interface. Orchestrates all sub-components and manages UI state.

Props:

interface ModerationQueueProps {
  optimisticallyUpdateStats?: (delta: Partial<{
    pendingSubmissions: number;
    openReports: number;
    flaggedContent: number;
  }>) => void;
}

Ref API:

interface ModerationQueueRef {
  refresh: () => void;
}

Usage:

import { useRef } from 'react';
import { ModerationQueue } from '@/components/moderation/ModerationQueue';

function AdminPanel() {
  const queueRef = useRef<ModerationQueueRef>(null);
  
  return (
    <div>
      <button onClick={() => queueRef.current?.refresh()}>
        Refresh Queue
      </button>
      <ModerationQueue ref={queueRef} />
    </div>
  );
}

ModerationErrorBoundary

Location: src/components/error/ModerationErrorBoundary.tsx

Purpose: Catches React render errors in queue items, preventing full queue crashes.

Props:

interface ModerationErrorBoundaryProps {
  children: ReactNode;
  submissionId?: string;
  fallback?: ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
}

Features:

  • Automatic error logging
  • User-friendly error UI
  • Retry functionality
  • Copy error details button
  • Development-mode stack traces

Usage:

<ModerationErrorBoundary submissionId={item.id}>
  <QueueItem item={item} {...props} />
</ModerationErrorBoundary>

Custom Fallback:

<ModerationErrorBoundary
  submissionId={item.id}
  fallback={<div>Custom error message</div>}
  onError={(error, info) => {
    // Send to monitoring service
    trackError(error, info);
  }}
>
  <QueueItem item={item} {...props} />
</ModerationErrorBoundary>

QueueItem

Location: src/components/moderation/QueueItem.tsx

Purpose: Renders individual submission in queue with all interaction controls.

Props:

interface QueueItemProps {
  item: ModerationItem;
  isMobile: boolean;
  actionLoading: string | null;
  isLockedByMe: boolean;
  isLockedByOther: boolean;
  lockStatus: LockStatus;
  currentLockSubmissionId?: string;
  notes: Record<string, string>;
  isAdmin: boolean;
  isSuperuser: boolean;
  queueIsLoading: boolean;
  onNoteChange: (id: string, value: string) => void;
  onApprove: (item: ModerationItem, action: 'approved' | 'rejected', notes?: string) => void;
  onResetToPending: (item: ModerationItem) => void;
  onRetryFailed: (item: ModerationItem) => void;
  onOpenPhotos: (photos: PhotoForDisplay[], index: number) => void;
  onOpenReviewManager: (submissionId: string) => void;
  onOpenItemEditor: (submissionId: string) => void;
  onClaimSubmission: (submissionId: string) => void;
  onDeleteSubmission: (item: ModerationItem) => void;
  onInteractionFocus: (id: string) => void;
  onInteractionBlur: (id: string) => void;
}

Key Features:

  • Displays submission type, status, timestamps
  • User profile with avatar
  • Validation summary (errors, warnings)
  • Lock status indicators
  • Moderator edit badges
  • Action buttons (approve, reject, claim)
  • Responsive mobile/desktop layouts

Accessibility:

  • Keyboard navigation support
  • ARIA labels on interactive elements
  • Focus management
  • Screen reader compatible

QueueFilters

Location: src/components/moderation/QueueFilters.tsx

Purpose: Filter and sort controls for moderation queue.

Props:

interface QueueFiltersProps {
  activeEntityFilter: EntityFilter;
  activeStatusFilter: StatusFilter;
  sortConfig: SortConfig;
  isMobile: boolean;
  isLoading?: boolean;
  onEntityFilterChange: (filter: EntityFilter) => void;
  onStatusFilterChange: (filter: StatusFilter) => void;
  onSortChange: (config: SortConfig) => void;
  onClearFilters: () => void;
  showClearButton: boolean;
}

Features:

  • Entity type filter (all, reviews, submissions, photos)
  • Status filter (pending, approved, rejected, etc.)
  • Sort controls (date, type, status)
  • Clear filters button
  • Fully accessible (ARIA labels, keyboard navigation)

Usage:

<QueueFilters
  activeEntityFilter={filters.entityFilter}
  activeStatusFilter={filters.statusFilter}
  sortConfig={filters.sortConfig}
  isMobile={isMobile}
  onEntityFilterChange={filters.setEntityFilter}
  onStatusFilterChange={filters.setStatusFilter}
  onSortChange={filters.setSortConfig}
  onClearFilters={filters.clearFilters}
  showClearButton={filters.hasActiveFilters}
/>

ValidationSummary

Location: src/components/moderation/ValidationSummary.tsx

Purpose: Displays validation results for submission items.

Props:

interface ValidationSummaryProps {
  item: {
    item_type: string;
    item_data: SubmissionItemData;
    id?: string;
  };
  onValidationChange?: (result: ValidationResult) => void;
  compact?: boolean;
  validationKey?: number;
}

View Modes:

Compact (for queue items):

  • Status badges (Valid, Errors, Warnings)
  • Always-visible error details (no hover needed)
  • Minimal space usage

Detailed (for review manager):

  • Expandable validation details
  • Full error, warning, and suggestion lists
  • Re-validate button

Usage:

{/* Compact view in queue */}
<ValidationSummary
  item={{
    item_type: 'park',
    item_data: parkData,
    id: itemId
  }}
  compact={true}
  onValidationChange={(result) => {
    if (result.blockingErrors.length > 0) {
      setCanApprove(false);
    }
  }}
/>

{/* Detailed view in editor */}
<ValidationSummary
  item={{
    item_type: 'ride',
    item_data: rideData
  }}
  compact={false}
/>

Hooks Reference

useModerationQueueManager

Location: src/hooks/moderation/useModerationQueueManager.ts

Purpose: Orchestrator hook combining all moderation queue logic.

Usage:

const queueManager = useModerationQueueManager({
  user,
  isAdmin: isAdmin(),
  isSuperuser: isSuperuser(),
  toast,
  settings: {
    refreshMode: 'auto',
    pollInterval: 30000,
    refreshStrategy: 'merge',
    preserveInteraction: true,
    useRealtimeQueue: true,
  },
});

// Access sub-hooks
queueManager.filters.setEntityFilter('reviews');
queueManager.pagination.setCurrentPage(2);
queueManager.queue.claimSubmission(itemId);

// Perform actions
await queueManager.performAction(item, 'approved', 'Looks good!');
await queueManager.deleteSubmission(item);

useModerationQueue

Location: src/hooks/useModerationQueue.ts

Purpose: Lock management and queue statistics.

Features:

  • Claim/release submission locks
  • Lock expiry countdown
  • Lock status checking
  • Queue statistics

Usage:

const queue = useModerationQueue({
  onLockStateChange: () => {
    console.log('Lock state changed');
  }
});

// Claim submission
await queue.claimSubmission('submission-123');

// Extend lock (adds 15 minutes)
await queue.extendLock();

// Release lock
await queue.releaseLock('submission-123');

// Check lock status
const timeRemaining = queue.getTimeRemaining();
const progress = queue.getLockProgress();

Testing Components

Unit Testing Example

import { render, screen } from '@testing-library/react';
import { ModerationErrorBoundary } from '@/components/error/ModerationErrorBoundary';

describe('ModerationErrorBoundary', () => {
  it('catches errors and shows fallback UI', () => {
    const ThrowError = () => {
      throw new Error('Test error');
    };
    
    render(
      <ModerationErrorBoundary submissionId="test-123">
        <ThrowError />
      </ModerationErrorBoundary>
    );
    
    expect(screen.getByText(/queue item error/i)).toBeInTheDocument();
    expect(screen.getByText(/test error/i)).toBeInTheDocument();
  });
  
  it('shows retry button', () => {
    const ThrowError = () => {
      throw new Error('Test error');
    };
    
    render(
      <ModerationErrorBoundary>
        <ThrowError />
      </ModerationErrorBoundary>
    );
    
    expect(screen.getByRole('button', { name: /retry/i })).toBeInTheDocument();
  });
});

Integration Testing Example

import { renderHook, act } from '@testing-library/react';
import { useModerationQueueManager } from '@/hooks/moderation/useModerationQueueManager';

describe('useModerationQueueManager', () => {
  it('filters items correctly', async () => {
    const { result } = renderHook(() => useModerationQueueManager(config));
    
    act(() => {
      result.current.filters.setEntityFilter('reviews');
    });
    
    await waitFor(() => {
      expect(result.current.items.every(item => item.type === 'review')).toBe(true);
    });
  });
});

Accessibility Guidelines

Keyboard Navigation

Queue Filters:

  • Tab: Navigate between filter controls
  • Enter/Space: Open dropdown
  • Arrow keys: Navigate dropdown options
  • Escape: Close dropdown

Queue Items:

  • Tab: Navigate between interactive elements
  • Enter/Space: Activate buttons
  • Escape: Close expanded sections

Screen Reader Support

All components include:

  • Semantic HTML (<button>, <label>, <select>)
  • ARIA labels for icon-only buttons
  • ARIA live regions for dynamic updates
  • Proper heading hierarchy

Focus Management

  • Focus trapped in modals
  • Focus returned to trigger on close
  • Skip links for keyboard users
  • Visible focus indicators

Styling Guidelines

Semantic Tokens

Use design system tokens instead of hardcoded colors:

// ❌ DON'T
<div className="text-white bg-blue-500">

// ✅ DO
<div className="text-foreground bg-primary">

Responsive Design

All components support mobile/desktop layouts:

<div className={`${isMobile ? 'flex-col' : 'flex-row'}`}>

Dark Mode

All components automatically adapt to light/dark mode using CSS variables.


Performance Considerations

Memoization

// QueueItem is memoized
export const QueueItem = memo(({ item, ...props }) => {
  // Component will only re-render if props change
});

Lazy Loading

Large components can be lazy-loaded:

const SubmissionReviewManager = lazy(() => 
  import('./SubmissionReviewManager')
);

Debouncing

Filters use debounced updates to reduce query load:

const filters = useModerationFilters({
  debounceDelay: 300, // Wait 300ms before applying filter
});

Troubleshooting

Error Boundary Not Catching Errors

Issue: Error boundary doesn't catch async errors or event handler errors.

Solution: Error boundaries only catch errors during rendering, lifecycle methods, and constructors. For async errors:

try {
  await performAction();
} catch (error) {
  handleError(error); // Manual error handling
}

Lock Timer Memory Leak

Issue: Lock timer continues after component unmount.

Solution: Already fixed in Phase 4. Timer now checks isMounted flag and cleans up properly.

Validation Summary Not Updating

Issue: Validation summary shows stale data after edits.

Solution: Pass validationKey prop to force re-validation:

<ValidationSummary
  item={item}
  validationKey={editCount} // Increment on each edit
/>

References

  • Architecture: docs/moderation/ARCHITECTURE.md
  • Submission Patterns: docs/moderation/SUBMISSION_PATTERNS.md
  • Type Definitions: src/types/moderation.ts
  • Hooks: src/hooks/moderation/