elmeg-demo/frontend/app/mod/page.tsx
fullsizemalt 499a9fa352
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
feat(frontend): Implement Admin Dashboard for moderation
2025-12-21 02:49:11 -08:00

229 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Check, X, ShieldAlert, AlertTriangle } from "lucide-react"
import { useEffect, useState } from "react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { getApiUrl } from "@/lib/api-config"
import { Badge } from "@/components/ui/badge"
interface PendingNickname {
id: number
performance_id: number
nickname: string
suggested_by: number
created_at: string
}
interface PendingReport {
id: number
entity_type: string
entity_id: number
reason: string
details?: string
user_id: number
created_at: string
status: string
}
export default function ModDashboardPage() {
const [pendingNicknames, setPendingNicknames] = useState<PendingNickname[]>([])
const [pendingReports, setPendingReports] = useState<PendingReport[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState("")
useEffect(() => {
fetchQueue()
}, [])
const fetchQueue = async () => {
const token = localStorage.getItem("token")
if (!token) {
setError("Not authenticated")
setLoading(false)
return
}
try {
const [nicknamesRes, reportsRes] = await Promise.all([
fetch(`${getApiUrl()}/moderation/queue/nicknames`, { headers: { Authorization: `Bearer ${token}` } }),
fetch(`${getApiUrl()}/moderation/queue/reports`, { headers: { Authorization: `Bearer ${token}` } })
])
if (nicknamesRes.status === 403 || reportsRes.status === 403) {
setError("Access denied: Moderators only")
setLoading(false)
return
}
if (!nicknamesRes.ok || !reportsRes.ok) throw new Error("Failed to fetch queue")
setPendingNicknames(await nicknamesRes.json())
setPendingReports(await reportsRes.json())
} catch (err) {
console.error(err)
setError("Failed to load moderation queue")
} finally {
setLoading(false)
}
}
const handleNicknameAction = async (id: number, action: "approve" | "reject") => {
const token = localStorage.getItem("token")
if (!token) return
try {
// Note: Updated backend uses PUT for nicknames
const res = await fetch(`${getApiUrl()}/moderation/nicknames/${id}/${action}`, {
method: "PUT",
headers: { Authorization: `Bearer ${token}` }
})
if (!res.ok) throw new Error(`Failed to ${action}`)
// Remove from list
setPendingNicknames(prev => prev.filter(n => n.id !== id))
} catch (err) {
console.error(err)
alert(`Error: ${err}`)
}
}
const handleReportAction = async (id: number, action: "resolve" | "dismiss") => {
const token = localStorage.getItem("token")
if (!token) return
try {
// Note: Updated backend uses PUT for reports
const res = await fetch(`${getApiUrl()}/moderation/reports/${id}/${action}`, {
method: "PUT",
headers: { Authorization: `Bearer ${token}` }
})
if (!res.ok) throw new Error(`Failed to ${action}`)
setPendingReports(prev => prev.filter(r => r.id !== id))
} catch (err) {
console.error(err)
alert(`Error: ${err}`)
}
}
if (loading) return <div className="p-8 text-center">Loading dashboard...</div>
if (error) return <div className="p-8 text-center text-red-500 font-bold">{error}</div>
return (
<div className="container py-8 flex flex-col gap-6 max-w-5xl">
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-2">
<ShieldAlert className="h-8 w-8 text-primary" />
Moderator Dashboard
</h1>
<Tabs defaultValue="reports">
<TabsList>
<TabsTrigger value="reports">Reports ({pendingReports.length})</TabsTrigger>
<TabsTrigger value="nicknames">Nicknames ({pendingNicknames.length})</TabsTrigger>
</TabsList>
<TabsContent value="reports">
<Card>
<CardHeader>
<CardTitle>User Reports</CardTitle>
</CardHeader>
<CardContent>
{pendingReports.length === 0 ? (
<p className="text-muted-foreground py-8 text-center">No pending reports. Community is safe! 🛡</p>
) : (
<div className="space-y-4">
{pendingReports.map(report => (
<div key={report.id} className="flex flex-col md:flex-row gap-4 justify-between border p-4 rounded-lg bg-red-50/10 border-red-100 dark:border-red-900/20">
<div>
<div className="flex items-center gap-2 mb-1">
<Badge variant="destructive" className="uppercase text-[10px]">
{report.reason}
</Badge>
<span className="font-semibold">{report.entity_type} #{report.entity_id}</span>
</div>
{report.details && (
<p className="text-sm italic text-muted-foreground mb-2">"{report.details}"</p>
)}
<p className="text-xs text-muted-foreground">
Reported by User #{report.user_id} {new Date(report.created_at).toLocaleString()}
</p>
</div>
<div className="flex gap-2 items-center">
<Button
size="sm"
variant="default"
className="bg-red-600 hover:bg-red-700"
onClick={() => handleReportAction(report.id, "resolve")}
>
<Check className="h-4 w-4 mr-1" /> Resolve (Ban/Delete)
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleReportAction(report.id, "dismiss")}
>
<X className="h-4 w-4 mr-1" /> Dismiss
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="nicknames">
<Card>
<CardHeader>
<CardTitle>Pending Nicknames</CardTitle>
</CardHeader>
<CardContent>
{pendingNicknames.length === 0 ? (
<p className="text-muted-foreground py-8 text-center">No pending nicknames.</p>
) : (
<div className="space-y-4">
{pendingNicknames.map((item) => (
<div key={item.id} className="flex items-center justify-between border p-4 rounded-lg">
<div>
<p className="font-bold text-lg">"{item.nickname}"</p>
<p className="text-sm text-muted-foreground">
Performance #{item.performance_id} User #{item.suggested_by}
</p>
<p className="text-xs text-muted-foreground mt-1">
{new Date(item.created_at).toLocaleDateString()}
</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="text-green-600 hover:text-green-700 hover:bg-green-50"
onClick={() => handleNicknameAction(item.id, "approve")}
>
<Check className="h-4 w-4 mr-1" /> Approve
</Button>
<Button
size="sm"
variant="outline"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => handleNicknameAction(item.id, "reject")}
>
<X className="h-4 w-4 mr-1" /> Reject
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
)
}