Files
ember-market-frontend/app/dashboard/admin/ban/page.tsx
g 4ef0fd1a68 Improve nav active state detection logic
Refined the active state logic in NavItem to ensure /dashboard and /dashboard/admin only match exactly, preventing sub-paths from being incorrectly marked as active.
2025-11-30 00:29:57 +00:00

375 lines
14 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
import { UserX, Shield, Search, Ban, Unlock, Loader2 } from "lucide-react";
import { fetchClient } from "@/lib/api-client";
import { useToast } from "@/hooks/use-toast";
interface BlockedUser {
_id: string;
telegramUserId: number;
reason?: string;
blockedBy?: {
_id: string;
username: string;
};
blockedAt: string;
}
export default function AdminBanPage() {
const { toast } = useToast();
const [loading, setLoading] = useState(true);
const [banning, setBanning] = useState(false);
const [unbanning, setUnbanning] = useState<string | null>(null);
const [blockedUsers, setBlockedUsers] = useState<BlockedUser[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [banData, setBanData] = useState({
telegramUserId: "",
reason: "",
additionalDetails: "",
});
useEffect(() => {
fetchBlockedUsers();
}, []);
const fetchBlockedUsers = async () => {
try {
setLoading(true);
const data = await fetchClient<BlockedUser[]>("/admin/blocked-users");
setBlockedUsers(data);
} catch (error) {
console.error("Failed to fetch blocked users:", error);
toast({
title: "Error",
description: "Failed to load blocked users",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
const handleBanUser = async () => {
if (!banData.telegramUserId) {
toast({
title: "Error",
description: "Telegram User ID is required",
variant: "destructive",
});
return;
}
try {
setBanning(true);
await fetchClient("/admin/ban", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
telegramUserId: parseInt(banData.telegramUserId),
reason: banData.additionalDetails || banData.reason || undefined,
}),
});
toast({
title: "Success",
description: "User has been banned",
});
setBanData({ telegramUserId: "", reason: "", additionalDetails: "" });
fetchBlockedUsers();
} catch (error: any) {
console.error("Failed to ban user:", error);
toast({
title: "Error",
description: error.message || "Failed to ban user",
variant: "destructive",
});
} finally {
setBanning(false);
}
};
const handleUnbanUser = async (telegramUserId: number) => {
try {
setUnbanning(telegramUserId.toString());
await fetchClient(`/admin/ban/${telegramUserId}`, {
method: "DELETE",
});
toast({
title: "Success",
description: "User has been unbanned",
});
fetchBlockedUsers();
} catch (error: any) {
console.error("Failed to unban user:", error);
toast({
title: "Error",
description: error.message || "Failed to unban user",
variant: "destructive",
});
} finally {
setUnbanning(null);
}
};
const filteredUsers = blockedUsers.filter((user) => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
return (
user.telegramUserId.toString().includes(query) ||
user.reason?.toLowerCase().includes(query) ||
user.blockedBy?.username?.toLowerCase().includes(query)
);
});
const activeBans = blockedUsers.length;
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Ban Users</h1>
<p className="text-sm text-muted-foreground mt-1">Manage user bans and suspensions</p>
</div>
{/* Stats Cards */}
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Active Bans</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{activeBans}</div>
<p className="text-xs text-muted-foreground">Currently banned</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Bans</CardTitle>
<UserX className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{activeBans}</div>
<p className="text-xs text-muted-foreground">All time bans</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Recent Bans</CardTitle>
<Ban className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{blockedUsers.filter(
(u) => new Date(u.blockedAt) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
).length}
</div>
<p className="text-xs text-muted-foreground">Last 7 days</p>
</CardContent>
</Card>
</div>
{/* Ban User Form */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<UserX className="h-5 w-5 mr-2" />
Ban User
</CardTitle>
<CardDescription>
Enter Telegram User ID and reason for banning
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="telegramUserId">Telegram User ID</Label>
<Input
id="telegramUserId"
type="number"
placeholder="Enter Telegram User ID"
value={banData.telegramUserId}
onChange={(e) => setBanData({...banData, telegramUserId: e.target.value})}
/>
</div>
<div className="space-y-2">
<Label htmlFor="reason">Reason</Label>
<Select value={banData.reason} onValueChange={(value) => setBanData({...banData, reason: value})}>
<SelectTrigger>
<SelectValue placeholder="Select ban reason (optional)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="spam">Spam</SelectItem>
<SelectItem value="fraud">Fraud</SelectItem>
<SelectItem value="harassment">Harassment</SelectItem>
<SelectItem value="policy_violation">Policy Violation</SelectItem>
<SelectItem value="suspicious_activity">Suspicious Activity</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="description">Additional Details (Optional)</Label>
<Input
id="description"
placeholder="Additional context..."
value={banData.additionalDetails}
onChange={(e) => setBanData({...banData, additionalDetails: e.target.value})}
/>
</div>
</div>
<div className="mt-4">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" disabled={banning}>
{banning ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Banning...
</>
) : (
<>
<Ban className="h-4 w-4 mr-2" />
Ban User
</>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Ban</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to ban Telegram User ID "{banData.telegramUserId}"? This action can be reversed later.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleBanUser}>
Confirm Ban
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardContent>
</Card>
{/* Banned Users Table */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Banned Users</CardTitle>
<CardDescription>View and manage current bans</CardDescription>
</div>
<div className="relative w-64">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by User ID, reason, or admin..."
className="pl-8"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Telegram User ID</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Banned By</TableHead>
<TableHead>Date</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredUsers.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
{searchQuery ? "No users found matching your search" : "No banned users"}
</TableCell>
</TableRow>
) : (
filteredUsers.map((user) => (
<TableRow key={user._id}>
<TableCell>
<div className="font-mono text-sm">{user.telegramUserId}</div>
</TableCell>
<TableCell>
{user.reason ? (
<Badge variant="secondary">{user.reason}</Badge>
) : (
<span className="text-muted-foreground">No reason provided</span>
)}
</TableCell>
<TableCell>
{user.blockedBy?.username || "Unknown"}
</TableCell>
<TableCell>
{new Date(user.blockedAt).toLocaleDateString()}
</TableCell>
<TableCell className="text-right">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
disabled={unbanning === user.telegramUserId.toString()}
>
{unbanning === user.telegramUserId.toString() ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Unlock className="h-4 w-4" />
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Unban</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to unban Telegram User ID "{user.telegramUserId}"? They will be able to use the platform again.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => handleUnbanUser(user.telegramUserId)}>
Confirm Unban
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}