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";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -10,133 +10,200 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
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 { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
||||||
import { UserX, Shield, Search, Ban, Unlock } from "lucide-react";
|
import { UserX, Shield, Search, Ban, Unlock, Loader2 } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { fetchClient } from "@/lib/api-client";
|
||||||
import Link from "next/link";
|
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() {
|
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({
|
const [banData, setBanData] = useState({
|
||||||
username: "",
|
telegramUserId: "",
|
||||||
reason: "",
|
reason: "",
|
||||||
duration: "",
|
additionalDetails: "",
|
||||||
description: ""
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mock data for banned users
|
useEffect(() => {
|
||||||
const bannedUsers = [
|
fetchBlockedUsers();
|
||||||
{
|
}, []);
|
||||||
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"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleBanUser = () => {
|
const fetchBlockedUsers = async () => {
|
||||||
// Handle ban user logic
|
try {
|
||||||
console.log("Banning user:", banData);
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div>
|
||||||
<div>
|
<h1 className="text-2xl font-semibold tracking-tight">Ban Users</h1>
|
||||||
<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>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid gap-4 md:grid-cols-4">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium">Active Bans</CardTitle>
|
<CardTitle className="text-sm font-medium">Active Bans</CardTitle>
|
||||||
<Shield className="h-4 w-4 text-muted-foreground" />
|
<Shield className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<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>
|
<p className="text-xs text-muted-foreground">Currently banned</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<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" />
|
<UserX className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">3</div>
|
<div className="text-2xl font-bold">{activeBans}</div>
|
||||||
<p className="text-xs text-muted-foreground">Permanent suspensions</p>
|
<p className="text-xs text-muted-foreground">All time bans</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<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" />
|
<Ban className="h-4 w-4 text-muted-foreground" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-2xl font-bold">9</div>
|
<div className="text-2xl font-bold">
|
||||||
<p className="text-xs text-muted-foreground">Time-limited bans</p>
|
{blockedUsers.filter(
|
||||||
</CardContent>
|
(u) => new Date(u.blockedAt) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
||||||
</Card>
|
).length}
|
||||||
<Card>
|
</div>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<p className="text-xs text-muted-foreground">Last 7 days</p>
|
||||||
<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>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
{/* Ban User Form */}
|
||||||
{/* Ban User Form */}
|
<Card>
|
||||||
<Card>
|
<CardHeader>
|
||||||
<CardHeader>
|
<CardTitle className="flex items-center">
|
||||||
<CardTitle className="flex items-center">
|
<UserX className="h-5 w-5 mr-2" />
|
||||||
<UserX className="h-5 w-5 mr-2" />
|
Ban User
|
||||||
Ban User
|
</CardTitle>
|
||||||
</CardTitle>
|
<CardDescription>
|
||||||
<CardDescription>
|
Enter Telegram User ID and reason for banning
|
||||||
Enter user details and reason for banning
|
</CardDescription>
|
||||||
</CardDescription>
|
</CardHeader>
|
||||||
</CardHeader>
|
<CardContent>
|
||||||
<CardContent className="space-y-4">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="username">Username</Label>
|
<Label htmlFor="telegramUserId">Telegram User ID</Label>
|
||||||
<Input
|
<Input
|
||||||
id="username"
|
id="telegramUserId"
|
||||||
placeholder="Enter username to ban"
|
type="number"
|
||||||
value={banData.username}
|
placeholder="Enter Telegram User ID"
|
||||||
onChange={(e) => setBanData({...banData, username: e.target.value})}
|
value={banData.telegramUserId}
|
||||||
|
onChange={(e) => setBanData({...banData, telegramUserId: e.target.value})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -144,7 +211,7 @@ export default function AdminBanPage() {
|
|||||||
<Label htmlFor="reason">Reason</Label>
|
<Label htmlFor="reason">Reason</Label>
|
||||||
<Select value={banData.reason} onValueChange={(value) => setBanData({...banData, reason: value})}>
|
<Select value={banData.reason} onValueChange={(value) => setBanData({...banData, reason: value})}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select ban reason" />
|
<SelectValue placeholder="Select ban reason (optional)" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="spam">Spam</SelectItem>
|
<SelectItem value="spam">Spam</SelectItem>
|
||||||
@@ -158,43 +225,37 @@ export default function AdminBanPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="duration">Duration</Label>
|
<Label htmlFor="description">Additional Details (Optional)</Label>
|
||||||
<Select value={banData.duration} onValueChange={(value) => setBanData({...banData, duration: value})}>
|
<Input
|
||||||
<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
|
|
||||||
id="description"
|
id="description"
|
||||||
placeholder="Provide additional context for the ban..."
|
placeholder="Additional context..."
|
||||||
value={banData.description}
|
value={banData.additionalDetails}
|
||||||
onChange={(e) => setBanData({...banData, description: e.target.value})}
|
onChange={(e) => setBanData({...banData, additionalDetails: e.target.value})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive" className="w-full">
|
<Button variant="destructive" disabled={banning}>
|
||||||
<Ban className="h-4 w-4 mr-2" />
|
{banning ? (
|
||||||
Ban User
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Banning...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
|
Ban User
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Confirm Ban</AlertDialogTitle>
|
<AlertDialogTitle>Confirm Ban</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<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>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
@@ -205,82 +266,108 @@ export default function AdminBanPage() {
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</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 */}
|
{/* Banned Users Table */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Recent Bans</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
<CardDescription>View and manage current and past bans</CardDescription>
|
<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>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
{loading ? (
|
||||||
<TableHeader>
|
<div className="flex items-center justify-center py-8">
|
||||||
<TableRow>
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
<TableHead>User</TableHead>
|
</div>
|
||||||
<TableHead>Reason</TableHead>
|
) : (
|
||||||
<TableHead>Duration</TableHead>
|
<Table>
|
||||||
<TableHead>Banned By</TableHead>
|
<TableHeader>
|
||||||
<TableHead>Date</TableHead>
|
<TableRow>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Telegram User ID</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead>Reason</TableHead>
|
||||||
</TableRow>
|
<TableHead>Banned By</TableHead>
|
||||||
</TableHeader>
|
<TableHead>Date</TableHead>
|
||||||
<TableBody>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
{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>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
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 { Search, Ban, UserCheck, UserX, Package, DollarSign } from "lucide-react";
|
||||||
import { fetchServer } from "@/lib/api";
|
import { fetchServer } from "@/lib/api";
|
||||||
|
|
||||||
@@ -172,21 +173,27 @@ export default async function AdminUsersPage() {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-col space-y-1">
|
{user.isBlocked ? (
|
||||||
{user.isBlocked ? (
|
<TooltipProvider>
|
||||||
<Badge variant="destructive">
|
<Tooltip>
|
||||||
<Ban className="h-3 w-3 mr-1" />
|
<TooltipTrigger asChild>
|
||||||
Blocked
|
<Badge variant="destructive">
|
||||||
</Badge>
|
<Ban className="h-3 w-3 mr-1" />
|
||||||
) : user.totalOrders > 0 ? (
|
Blocked
|
||||||
<Badge variant="default">Active</Badge>
|
</Badge>
|
||||||
) : (
|
</TooltipTrigger>
|
||||||
<Badge variant="secondary">No Orders</Badge>
|
{user.blockedReason && (
|
||||||
)}
|
<TooltipContent>
|
||||||
{user.blockedReason && (
|
<p className="max-w-xs">{user.blockedReason}</p>
|
||||||
<span className="text-xs text-muted-foreground">{user.blockedReason}</span>
|
</TooltipContent>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : user.totalOrders > 0 ? (
|
||||||
|
<Badge variant="default">Active</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary">No Orders</Badge>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{user.firstOrderDate
|
{user.firstOrderDate
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"commitHash": "3548cc3",
|
"commitHash": "1001911",
|
||||||
"buildTime": "2025-11-28T20:00:38.186Z"
|
"buildTime": "2025-11-28T20:06:53.158Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user