Files
ember-market-frontend/app/dashboard/admin/ban/page.tsx
g c9c3f766a6 Improve admin ban UX, add product cloning, and enhance auth handling
Refines the admin ban page with better dialog state management and feedback during ban/unban actions. Adds a product cloning feature to the products dashboard and updates the product table to support cloning. Improves error handling in ChatDetail for authentication errors, and enhances middleware to handle auth check timeouts and network errors more gracefully. Also updates BanUserCard to validate user ID and ensure correct request formatting.
2025-12-27 20:58:08 +00:00

402 lines
15 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 [banDialogOpen, setBanDialogOpen] = useState(false);
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 (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
if (!banData.telegramUserId) {
toast({
title: "Error",
description: "Telegram User ID is required",
variant: "destructive",
});
return;
}
try {
setBanning(true);
const response = await fetchClient("/admin/ban", {
method: "POST",
body: {
telegramUserId: parseInt(banData.telegramUserId),
reason: banData.additionalDetails || banData.reason || undefined,
},
});
toast({
title: "Success",
description: "User has been banned",
});
setBanData({ telegramUserId: "", reason: "", additionalDetails: "" });
setBanDialogOpen(false);
await 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, e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
try {
setUnbanning(telegramUserId.toString());
await fetchClient(`/admin/ban/${telegramUserId}`, {
method: "DELETE",
});
toast({
title: "Success",
description: "User has been unbanned",
});
await 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
open={banDialogOpen}
onOpenChange={(open) => {
if (!banning) {
setBanDialogOpen(open);
}
}}
>
<AlertDialogTrigger asChild>
<Button variant="destructive" disabled={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 disabled={banning}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.preventDefault();
handleBanUser(e);
}}
disabled={banning}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{banning ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Banning...
</>
) : (
"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 disabled={unbanning === user.telegramUserId.toString()}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => handleUnbanUser(user.telegramUserId, e)}
disabled={unbanning === user.telegramUserId.toString()}
>
{unbanning === user.telegramUserId.toString() ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Unbanning...
</>
) : (
"Confirm Unban"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}