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 controlsEnter/Space: Open dropdownArrow keys: Navigate dropdown optionsEscape: Close dropdown
Queue Items:
Tab: Navigate between interactive elementsEnter/Space: Activate buttonsEscape: 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/