Refactor admin ban page to use API and improve UX
Replaced mock data in the admin ban page with real API calls for fetching, banning, and unbanning users. Improved form validation, loading states, and search functionality. Updated banned users table to show live data and added confirmation dialogs for ban/unban actions. Enhanced user status display in the admin users page with tooltips for blocked reasons.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
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";
|
||||
@@ -10,114 +10,179 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
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 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
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({
|
||||
username: "",
|
||||
telegramUserId: "",
|
||||
reason: "",
|
||||
duration: "",
|
||||
description: ""
|
||||
additionalDetails: "",
|
||||
});
|
||||
|
||||
// Mock data for banned users
|
||||
const bannedUsers = [
|
||||
{
|
||||
id: "1",
|
||||
username: "spam_user",
|
||||
email: "spam@example.com",
|
||||
reason: "Spam",
|
||||
bannedBy: "admin1",
|
||||
banDate: "2024-01-15",
|
||||
duration: "Permanent",
|
||||
status: "active"
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
username: "fraud_vendor",
|
||||
email: "fraud@example.com",
|
||||
reason: "Fraud",
|
||||
bannedBy: "admin1",
|
||||
banDate: "2024-01-20",
|
||||
duration: "30 days",
|
||||
status: "active"
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
username: "policy_violator",
|
||||
email: "violator@example.com",
|
||||
reason: "Policy Violation",
|
||||
bannedBy: "admin1",
|
||||
banDate: "2024-01-25",
|
||||
duration: "7 days",
|
||||
status: "expired"
|
||||
}
|
||||
];
|
||||
useEffect(() => {
|
||||
fetchBlockedUsers();
|
||||
}, []);
|
||||
|
||||
const handleBanUser = () => {
|
||||
// Handle ban user logic
|
||||
console.log("Banning user:", banData);
|
||||
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 className="flex items-center justify-between">
|
||||
<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>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/dashboard">Back to Dashboard</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<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">12</div>
|
||||
<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">Permanent Bans</CardTitle>
|
||||
<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">3</div>
|
||||
<p className="text-xs text-muted-foreground">Permanent suspensions</p>
|
||||
<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">Temporary Bans</CardTitle>
|
||||
<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">9</div>
|
||||
<p className="text-xs text-muted-foreground">Time-limited 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">Appeals Pending</CardTitle>
|
||||
<Unlock className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">2</div>
|
||||
<p className="text-xs text-muted-foreground">Awaiting review</p>
|
||||
<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>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Ban User Form */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -126,17 +191,19 @@ export default function AdminBanPage() {
|
||||
Ban User
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Enter user details and reason for banning
|
||||
Enter Telegram User ID and reason for banning
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Label htmlFor="telegramUserId">Telegram User ID</Label>
|
||||
<Input
|
||||
id="username"
|
||||
placeholder="Enter username to ban"
|
||||
value={banData.username}
|
||||
onChange={(e) => setBanData({...banData, username: e.target.value})}
|
||||
id="telegramUserId"
|
||||
type="number"
|
||||
placeholder="Enter Telegram User ID"
|
||||
value={banData.telegramUserId}
|
||||
onChange={(e) => setBanData({...banData, telegramUserId: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -144,7 +211,7 @@ export default function AdminBanPage() {
|
||||
<Label htmlFor="reason">Reason</Label>
|
||||
<Select value={banData.reason} onValueChange={(value) => setBanData({...banData, reason: value})}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select ban reason" />
|
||||
<SelectValue placeholder="Select ban reason (optional)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="spam">Spam</SelectItem>
|
||||
@@ -158,43 +225,37 @@ export default function AdminBanPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="duration">Duration</Label>
|
||||
<Select value={banData.duration} onValueChange={(value) => setBanData({...banData, duration: value})}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select ban duration" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1_day">1 Day</SelectItem>
|
||||
<SelectItem value="7_days">7 Days</SelectItem>
|
||||
<SelectItem value="30_days">30 Days</SelectItem>
|
||||
<SelectItem value="90_days">90 Days</SelectItem>
|
||||
<SelectItem value="permanent">Permanent</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Additional Details</Label>
|
||||
<Textarea
|
||||
<Label htmlFor="description">Additional Details (Optional)</Label>
|
||||
<Input
|
||||
id="description"
|
||||
placeholder="Provide additional context for the ban..."
|
||||
value={banData.description}
|
||||
onChange={(e) => setBanData({...banData, description: e.target.value})}
|
||||
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" className="w-full">
|
||||
<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 user "{banData.username}"? This action cannot be undone.
|
||||
Are you sure you want to ban Telegram User ID "{banData.telegramUserId}"? This action can be reversed later.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
@@ -205,82 +266,108 @@ export default function AdminBanPage() {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Search Banned Users */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Search Banned Users</CardTitle>
|
||||
<CardDescription>Look up existing bans and their status</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Search by username or email..." className="pl-8" />
|
||||
</div>
|
||||
<Button variant="outline" className="w-full">
|
||||
Search Bans
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Banned Users Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Bans</CardTitle>
|
||||
<CardDescription>View and manage current and past bans</CardDescription>
|
||||
<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>User</TableHead>
|
||||
<TableHead>Telegram User ID</TableHead>
|
||||
<TableHead>Reason</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead>Banned By</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{bannedUsers.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<div>
|
||||
<div className="font-medium">{user.username}</div>
|
||||
<div className="text-sm text-muted-foreground">{user.email}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{user.reason}</TableCell>
|
||||
<TableCell>{user.duration}</TableCell>
|
||||
<TableCell>{user.bannedBy}</TableCell>
|
||||
<TableCell>{user.banDate}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={user.status === "active" ? "destructive" : "secondary"}
|
||||
>
|
||||
{user.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
{user.status === "active" && (
|
||||
<Button variant="outline" size="sm">
|
||||
<Unlock className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm">
|
||||
View Details
|
||||
</Button>
|
||||
</div>
|
||||
{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>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { Search, Ban, UserCheck, UserX, Package, DollarSign } from "lucide-react";
|
||||
import { fetchServer } from "@/lib/api";
|
||||
|
||||
@@ -172,21 +173,27 @@ export default async function AdminUsersPage() {
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col space-y-1">
|
||||
{user.isBlocked ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge variant="destructive">
|
||||
<Ban className="h-3 w-3 mr-1" />
|
||||
Blocked
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
{user.blockedReason && (
|
||||
<TooltipContent>
|
||||
<p className="max-w-xs">{user.blockedReason}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : user.totalOrders > 0 ? (
|
||||
<Badge variant="default">Active</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">No Orders</Badge>
|
||||
)}
|
||||
{user.blockedReason && (
|
||||
<span className="text-xs text-muted-foreground">{user.blockedReason}</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.firstOrderDate
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"commitHash": "3548cc3",
|
||||
"buildTime": "2025-11-28T20:00:38.186Z"
|
||||
"commitHash": "1001911",
|
||||
"buildTime": "2025-11-28T20:06:53.158Z"
|
||||
}
|
||||
Reference in New Issue
Block a user