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,114 +10,179 @@ 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>
|
</div>
|
||||||
<Button asChild variant="outline" size="sm">
|
|
||||||
<Link href="/dashboard">Back to Dashboard</Link>
|
|
||||||
</Button>
|
|
||||||
</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>
|
||||||
@@ -126,17 +191,19 @@ export default function AdminBanPage() {
|
|||||||
Ban User
|
Ban User
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Enter user details and reason for banning
|
Enter Telegram User ID and reason for banning
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent>
|
||||||
|
<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}>
|
||||||
|
{banning ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Banning...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Ban className="h-4 w-4 mr-2" />
|
<Ban className="h-4 w-4 mr-2" />
|
||||||
Ban User
|
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>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>User</TableHead>
|
<TableHead>Telegram User ID</TableHead>
|
||||||
<TableHead>Reason</TableHead>
|
<TableHead>Reason</TableHead>
|
||||||
<TableHead>Duration</TableHead>
|
|
||||||
<TableHead>Banned By</TableHead>
|
<TableHead>Banned By</TableHead>
|
||||||
<TableHead>Date</TableHead>
|
<TableHead>Date</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{bannedUsers.map((user) => (
|
{filteredUsers.length === 0 ? (
|
||||||
<TableRow key={user.id}>
|
<TableRow>
|
||||||
<TableCell>
|
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
|
||||||
<div>
|
{searchQuery ? "No users found matching your search" : "No banned users"}
|
||||||
<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>
|
</TableCell>
|
||||||
</TableRow>
|
</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>
|
</TableBody>
|
||||||
</Table>
|
</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>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
<Badge variant="destructive">
|
<Badge variant="destructive">
|
||||||
<Ban className="h-3 w-3 mr-1" />
|
<Ban className="h-3 w-3 mr-1" />
|
||||||
Blocked
|
Blocked
|
||||||
</Badge>
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{user.blockedReason && (
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="max-w-xs">{user.blockedReason}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
) : user.totalOrders > 0 ? (
|
) : user.totalOrders > 0 ? (
|
||||||
<Badge variant="default">Active</Badge>
|
<Badge variant="default">Active</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="secondary">No Orders</Badge>
|
<Badge variant="secondary">No Orders</Badge>
|
||||||
)}
|
)}
|
||||||
{user.blockedReason && (
|
|
||||||
<span className="text-xs text-muted-foreground">{user.blockedReason}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</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