mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-26 17:26:59 -05:00
feat: Implement all 7 phases
This commit is contained in:
524
docs/moderation/COMPONENTS.md
Normal file
524
docs/moderation/COMPONENTS.md
Normal file
@@ -0,0 +1,524 @@
|
||||
# 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:**
|
||||
```typescript
|
||||
interface ModerationQueueProps {
|
||||
optimisticallyUpdateStats?: (delta: Partial<{
|
||||
pendingSubmissions: number;
|
||||
openReports: number;
|
||||
flaggedContent: number;
|
||||
}>) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Ref API:**
|
||||
```typescript
|
||||
interface ModerationQueueRef {
|
||||
refresh: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```tsx
|
||||
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:**
|
||||
```typescript
|
||||
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:**
|
||||
```tsx
|
||||
<ModerationErrorBoundary submissionId={item.id}>
|
||||
<QueueItem item={item} {...props} />
|
||||
</ModerationErrorBoundary>
|
||||
```
|
||||
|
||||
**Custom Fallback:**
|
||||
```tsx
|
||||
<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:**
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
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:**
|
||||
```tsx
|
||||
<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:**
|
||||
```typescript
|
||||
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:**
|
||||
```tsx
|
||||
{/* 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:**
|
||||
```typescript
|
||||
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:**
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```tsx
|
||||
// ❌ DON'T
|
||||
<div className="text-white bg-blue-500">
|
||||
|
||||
// ✅ DO
|
||||
<div className="text-foreground bg-primary">
|
||||
```
|
||||
|
||||
### Responsive Design
|
||||
|
||||
All components support mobile/desktop layouts:
|
||||
|
||||
```tsx
|
||||
<div className={`${isMobile ? 'flex-col' : 'flex-row'}`}>
|
||||
```
|
||||
|
||||
### Dark Mode
|
||||
|
||||
All components automatically adapt to light/dark mode using CSS variables.
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Memoization
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
const SubmissionReviewManager = lazy(() =>
|
||||
import('./SubmissionReviewManager')
|
||||
);
|
||||
```
|
||||
|
||||
### Debouncing
|
||||
|
||||
Filters use debounced updates to reduce query load:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
<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/`
|
||||
Reference in New Issue
Block a user