117 lines
5.7 KiB
TypeScript
117 lines
5.7 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 '../../context/ToastContext';
|
||
|
|
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-3xl font-display font-bold text-white mb-6">MODERATION QUEUE</h2>
|
||
|
|
|
||
|
|
<div className="border-b border-kodo-steel 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-kodo-red text-white' : 'border-transparent text-gray-500 hover:text-gray-300'}`}
|
||
|
|
>
|
||
|
|
{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-20"><Loader2 className="w-8 h-8 text-kodo-cyan animate-spin" /></div>}
|
||
|
|
|
||
|
|
{!loading && filteredQueue.length === 0 && (
|
||
|
|
<div className="text-center py-20 text-gray-500">
|
||
|
|
<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-kodo-red">
|
||
|
|
<div className="flex flex-col md:flex-row justify-between gap-4">
|
||
|
|
<div className="flex-1">
|
||
|
|
<div className="flex items-center gap-3 mb-2">
|
||
|
|
<Badge label={report.targetType} variant="terminal" />
|
||
|
|
<span className="font-bold text-white text-lg">{report.targetName}</span>
|
||
|
|
<span className="text-xs text-gray-500 font-mono flex items-center gap-1">
|
||
|
|
<Clock className="w-3 h-3" /> {report.timestamp}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<div className="bg-kodo-ink p-3 rounded border border-kodo-steel/50 mb-3">
|
||
|
|
<div className="text-xs font-bold text-kodo-red uppercase mb-1">Reason: {report.reason}</div>
|
||
|
|
<p className="text-sm text-gray-300">{report.description}</p>
|
||
|
|
</div>
|
||
|
|
<div className="text-xs text-gray-500">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-red-600 hover:bg-red-700 border-red-500 text-white" 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-gray-500 hover:text-white" onClick={() => handleAction(report.id, 'dismissed')}>
|
||
|
|
Dismiss
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|