veza/apps/web/src/components/admin/AdminModerationView.tsx
senke 4abdce05db style(ui): P3 AdminModerationView kodo-* → semantic tokens
- border/text: kodo-steel → border-border, kodo-content-dim → text-muted-foreground
- kodo-text-main → text-foreground, kodo-red → destructive (tabs, card, reason, Ban button)
- Report block: bg-kodo-ink border-kodo-steel → bg-muted/50 border-border
- Loader and empty state use muted-foreground

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 19:36:27 +01:00

186 lines
6.4 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
import { Report } from '../../types';
import {
ShieldAlert,
CheckCircle,
Ban,
MessageSquare,
Clock,
Loader2,
} from 'lucide-react';
import { useToast } from '../../components/feedback/ToastProvider';
import { adminService } from '../../services/adminService';
import { logger } from '@/utils/logger';
export const AdminModerationView: React.FC = () => {
const { addToast } = useToast();
const [queue, setQueue] = useState<Report[]>([]);
const [activeTab, setActiveTab] = useState<
'pending' | 'reviewed' | 'resolved'
>('pending');
const [loading, setLoading] = useState(true);
useEffect(() => {
const loadQueue = async () => {
setLoading(true);
try {
const data = await adminService.getModerationQueue('all');
setQueue(data);
} catch (e) {
logger.error('Error loading moderation queue', {
error: e instanceof Error ? e.message : String(e),
stack: e instanceof Error ? e.stack : undefined,
});
} finally {
setLoading(false);
}
};
loadQueue();
}, []);
const filteredQueue = queue.filter((r) =>
activeTab === 'pending'
? r.status === 'pending'
: activeTab === 'reviewed'
? r.status === 'reviewed'
: r.status === 'resolved' || r.status === 'dismissed',
);
const handleAction = async (id: string, action: string) => {
try {
await adminService.resolveReport(id, action);
addToast(`Report ${action}`, 'success');
setQueue(
queue.map((r) =>
r.id === id
? ({
...r,
status: action === 'dismissed' ? 'dismissed' : 'resolved',
} as any)
: r,
),
);
} catch (e) {
addToast('Action failed', 'error');
}
};
return (
<div className="space-y-6 animate-fadeIn pb-20">
<h2 className="text-2xl font-display font-bold text-white mb-6">
MODERATION QUEUE
</h2>
<div className="border-b border-border flex gap-6 mb-6">
{['pending', 'reviewed', 'resolved'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab as any)}
className={`pb-3 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${activeTab === tab ? 'border-destructive text-foreground' : 'border-transparent text-muted-foreground hover:text-foreground'}`}
>
{tab} (
{
queue.filter((r) =>
tab === 'pending'
? r.status === 'pending'
: tab === 'reviewed'
? r.status === 'reviewed'
: r.status === 'resolved' || r.status === 'dismissed',
).length
}
)
</button>
))}
</div>
<div className="space-y-4">
{loading && (
<div className="flex justify-center py-24">
<Loader2 className="w-8 h-8 text-muted-foreground animate-spin" />
</div>
)}
{!loading && filteredQueue.length === 0 && (
<div className="text-center py-24 text-muted-foreground">
<ShieldAlert className="w-12 h-12 mx-auto mb-4 opacity-30" />
<p>All caught up! No reports in this queue.</p>
</div>
)}
{!loading &&
filteredQueue.map((report) => (
<Card
key={report.id}
variant="default"
className="border-l-4 border-l-destructive"
>
<div className="flex flex-col md:flex-row justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-4 mb-2">
<Badge label={report.targetType} variant="terminal" />
<span className="font-bold text-white text-lg">
{report.targetName}
</span>
<span className="text-xs text-muted-foreground font-mono flex items-center gap-1">
<Clock className="w-3 h-3" /> {report.timestamp}
</span>
</div>
<div className="bg-muted/50 p-4 rounded border border-border mb-3">
<div className="text-xs font-bold text-destructive uppercase mb-1">
Reason: {report.reason}
</div>
<p className="text-sm text-foreground">
{report.description}
</p>
</div>
<div className="text-xs text-muted-foreground">
Reported by:{' '}
<span className="text-white">{report.reportedBy}</span>
</div>
</div>
<div className="flex flex-col gap-2 justify-center min-w-[140px]">
<Button
variant="primary"
size="sm"
className="bg-destructive hover:bg-destructive/90 border-destructive text-destructive-foreground"
icon={<Ban className="w-4 h-4" />}
onClick={() => handleAction(report.id, 'banned')}
>
Ban User
</Button>
<Button
variant="secondary"
size="sm"
icon={<CheckCircle className="w-4 h-4" />}
onClick={() => handleAction(report.id, 'resolved')}
>
Resolve
</Button>
<Button
variant="ghost"
size="sm"
icon={<MessageSquare className="w-4 h-4" />}
onClick={() => addToast('Warning sent')}
>
Send Warning
</Button>
<Button
variant="ghost"
size="sm"
className="text-muted-foreground hover:text-foreground"
onClick={() => handleAction(report.id, 'dismissed')}
>
Dismiss
</Button>
</div>
</div>
</Card>
))}
</div>
</div>
);
};