Files
avbeat-com/src/app/admin/suspensions/page.tsx
2026-05-07 16:39:17 +00:00

765 lines
37 KiB
TypeScript

'use client';
import React, { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
interface SuspensionUser {
id: string;
email: string;
full_name: string;
avatar_url: string | null;
role: string;
}
interface Suspension {
id: string;
user_id: string;
suspension_type: 'suspended' | 'banned';
reason: string;
duration_days: number | null;
expires_at: string | null;
is_active: boolean;
lifted_at: string | null;
lift_reason: string | null;
email_sent: boolean;
created_at: string;
user: SuspensionUser;
suspended_by_user: { id: string; full_name: string; email: string };
}
interface UserForSuspend {
id: string;
email: string;
full_name: string;
avatar_url: string | null;
role: string;
suspension_status: string | null;
suspended_until: string | null;
}
const DURATION_OPTIONS = [
{ label: '1 day', value: 1 },
{ label: '3 days', value: 3 },
{ label: '7 days', value: 7 },
{ label: '14 days', value: 14 },
{ label: '30 days', value: 30 },
{ label: '90 days', value: 90 },
{ label: 'Permanent (Ban)', value: null },
];
export default function AdminSuspensionsPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [suspensions, setSuspensions] = useState<Suspension[]>([]);
const [users, setUsers] = useState<UserForSuspend[]>([]);
const [loadingData, setLoadingData] = useState(true);
const [activeTab, setActiveTab] = useState<'active' | 'history' | 'users'>('active');
const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
const [processingIds, setProcessingIds] = useState<Set<string>>(new Set());
// Suspend modal state
const [showSuspendModal, setShowSuspendModal] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserForSuspend | null>(null);
const [suspendReason, setSuspendReason] = useState('');
const [suspendType, setSuspendType] = useState<'suspended' | 'banned'>('suspended');
const [suspendDuration, setSuspendDuration] = useState<number | null>(7);
const [suspending, setSuspending] = useState(false);
// Lift modal state
const [showLiftModal, setShowLiftModal] = useState(false);
const [liftingSuspension, setLiftingSuspension] = useState<Suspension | null>(null);
const [liftReason, setLiftReason] = useState('');
const [lifting, setLifting] = useState(false);
// User search
const [userSearch, setUserSearch] = useState('');
useEffect(() => {
if (!loading && !user) router.replace('/account');
}, [user, loading, router]);
const showNotification = (type: 'success' | 'error', message: string) => {
setNotification({ type, message });
setTimeout(() => setNotification(null), 4000);
};
const fetchSuspensions = useCallback(async (activeOnly: boolean) => {
const res = await fetch(`/api/admin/suspensions?active=${activeOnly}`);
if (res.ok) {
const data = await res.json();
setSuspensions(data.suspensions || []);
}
}, []);
const fetchUsers = useCallback(async () => {
const res = await fetch('/api/admin/users?tab=users');
if (res.ok) {
const data = await res.json();
setUsers(data.users || []);
}
}, []);
const loadData = useCallback(async () => {
setLoadingData(true);
try {
await Promise.all([
fetchSuspensions(activeTab === 'active'),
fetchUsers(),
]);
} finally {
setLoadingData(false);
}
}, [activeTab, fetchSuspensions, fetchUsers]);
useEffect(() => {
if (user) loadData();
}, [user, loadData]);
const handleSuspend = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedUser || !suspendReason.trim()) return;
setSuspending(true);
try {
const res = await fetch('/api/admin/suspensions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: selectedUser.id,
suspension_type: suspendType,
reason: suspendReason.trim(),
duration_days: suspendType === 'banned' ? null : suspendDuration,
}),
});
const data = await res.json();
if (res.ok) {
showNotification('success', `User ${suspendType === 'banned' ? 'banned' : 'suspended'} successfully${data.emailSent ? ' — notification email sent' : ''}`);
setShowSuspendModal(false);
setSuspendReason('');
setSuspendType('suspended');
setSuspendDuration(7);
setSelectedUser(null);
loadData();
} else {
showNotification('error', data.error || 'Failed to suspend user');
}
} catch {
showNotification('error', 'Failed to suspend user');
} finally {
setSuspending(false);
}
};
const handleLiftSuspension = async (e: React.FormEvent) => {
e.preventDefault();
if (!liftingSuspension) return;
setLifting(true);
try {
const res = await fetch('/api/admin/suspensions', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
suspension_id: liftingSuspension.id,
lift_reason: liftReason.trim() || undefined,
}),
});
if (res.ok) {
showNotification('success', 'Suspension lifted successfully');
setShowLiftModal(false);
setLiftReason('');
setLiftingSuspension(null);
loadData();
} else {
const data = await res.json();
showNotification('error', data.error || 'Failed to lift suspension');
}
} catch {
showNotification('error', 'Failed to lift suspension');
} finally {
setLifting(false);
}
};
const openSuspendModal = (u: UserForSuspend) => {
setSelectedUser(u);
setSuspendType('suspended');
setSuspendDuration(7);
setSuspendReason('');
setShowSuspendModal(true);
};
const openLiftModal = (s: Suspension) => {
setLiftingSuspension(s);
setLiftReason('');
setShowLiftModal(true);
};
const formatDate = (d: string) =>
new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
const formatExpiry = (s: Suspension) => {
if (s.suspension_type === 'banned') return 'Permanent';
if (!s.expires_at) return 'Indefinite';
const exp = new Date(s.expires_at);
const now = new Date();
if (exp < now) return 'Expired';
const days = Math.ceil((exp.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
return `${days}d remaining`;
};
const filteredUsers = users.filter((u) => {
if (!userSearch) return true;
return (
u.email.toLowerCase().includes(userSearch.toLowerCase()) ||
(u.full_name || '').toLowerCase().includes(userSearch.toLowerCase())
);
});
const activeSuspensions = suspensions.filter((s) => s.is_active);
const suspendedUserIds = new Set(activeSuspensions.map((s) => s.user_id));
if (loading) {
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
</div>
);
}
return (
<div className="min-h-screen bg-[#0d1117]">
{/* Notification */}
{notification && (
<div className={`fixed top-4 right-4 z-50 px-4 py-3 rounded-lg text-sm font-medium shadow-lg transition-all ${
notification.type === 'success' ?'bg-green-500/20 text-green-400 border border-green-500/30' :'bg-red-500/20 text-red-400 border border-red-500/30'
}`}>
{notification.message}
</div>
)}
{/* Header */}
<div className="bg-[#111827] border-b border-[#1f2937]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-4">
<div className="flex items-center justify-between gap-4 flex-wrap">
<div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-[#888] transition-colors text-sm">
Admin
</Link>
<span className="text-[#333]">/</span>
<h1 className="text-white font-bold text-lg">Suspensions & Bans</h1>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-[#555] bg-[#1f2937] px-2 py-1 rounded">
{activeSuspensions.length} active
</span>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-6">
{/* Stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-6">
<div className="bg-[#111827] border border-[#1f2937] rounded-lg p-4">
<p className="text-[#555] text-xs uppercase tracking-wider mb-1">Active Suspensions</p>
<p className="text-white text-2xl font-bold">{activeSuspensions.filter(s => s.suspension_type === 'suspended').length}</p>
</div>
<div className="bg-[#111827] border border-[#1f2937] rounded-lg p-4">
<p className="text-[#555] text-xs uppercase tracking-wider mb-1">Active Bans</p>
<p className="text-red-400 text-2xl font-bold">{activeSuspensions.filter(s => s.suspension_type === 'banned').length}</p>
</div>
<div className="bg-[#111827] border border-[#1f2937] rounded-lg p-4">
<p className="text-[#555] text-xs uppercase tracking-wider mb-1">Total Users</p>
<p className="text-white text-2xl font-bold">{users.length}</p>
</div>
<div className="bg-[#111827] border border-[#1f2937] rounded-lg p-4">
<p className="text-[#555] text-xs uppercase tracking-wider mb-1">Restricted Users</p>
<p className="text-orange-400 text-2xl font-bold">{suspendedUserIds.size}</p>
</div>
</div>
{/* Tabs */}
<div className="flex items-center gap-1 mb-5 border-b border-[#1f2937]">
{(['active', 'history', 'users'] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${
activeTab === tab
? 'text-[#3b82f6] border-[#3b82f6]'
: 'text-[#555] border-transparent hover:text-[#888]'
}`}
>
{tab === 'active' ? 'Active Restrictions' : tab === 'history' ? 'History' : 'Manage Users'}
{tab === 'active' && activeSuspensions.length > 0 && (
<span className="ml-2 bg-red-400/20 text-red-400 text-xs px-1.5 py-0.5 rounded-full">
{activeSuspensions.length}
</span>
)}
</button>
))}
</div>
{loadingData ? (
<div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
</div>
) : (
<>
{/* Active Restrictions Tab */}
{activeTab === 'active' && (
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
{activeSuspensions.length === 0 ? (
<div className="text-center py-16 text-[#555]">
<svg className="mx-auto mb-3 text-[#333]" width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
<circle cx="12" cy="12" r="10" /><path d="M4.93 4.93l14.14 14.14" />
</svg>
<p className="text-sm">No active suspensions or bans</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[#1f2937]">
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-5 py-3 font-semibold">User</th>
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-4 py-3 font-semibold">Type</th>
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-4 py-3 font-semibold hidden md:table-cell">Reason</th>
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-4 py-3 font-semibold hidden sm:table-cell">Expires</th>
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-4 py-3 font-semibold hidden lg:table-cell">Email</th>
<th className="text-right text-[#555] text-xs uppercase tracking-wider px-5 py-3 font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{activeSuspensions.map((s) => (
<tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] font-bold text-sm flex-shrink-0 overflow-hidden">
{s.user?.avatar_url ? (
<img src={s.user.avatar_url} alt={s.user.full_name || s.user.email} className="w-full h-full object-cover" />
) : (
(s.user?.full_name?.[0] || s.user?.email?.[0] || '?').toUpperCase()
)}
</div>
<div className="min-w-0">
<p className="text-white text-sm font-medium truncate">{s.user?.full_name || '—'}</p>
<p className="text-[#555] text-xs truncate">{s.user?.email}</p>
</div>
</div>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${
s.suspension_type === 'banned' ?'text-red-400 bg-red-400/10' :'text-orange-400 bg-orange-400/10'
}`}>
{s.suspension_type === 'banned' ? '🚫 Banned' : '⏸ Suspended'}
</span>
</td>
<td className="px-4 py-3.5 hidden md:table-cell">
<p className="text-[#ccc] text-xs max-w-[200px] truncate" title={s.reason}>{s.reason}</p>
</td>
<td className="px-4 py-3.5 hidden sm:table-cell">
<span className={`text-xs font-medium ${
s.suspension_type === 'banned' ? 'text-red-400' : 'text-orange-400'
}`}>
{formatExpiry(s)}
</span>
</td>
<td className="px-4 py-3.5 hidden lg:table-cell">
<span className={`text-xs ${s.email_sent ? 'text-green-400' : 'text-[#555]'}`}>
{s.email_sent ? '✓ Sent' : '✗ Not sent'}
</span>
</td>
<td className="px-5 py-3.5">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => openLiftModal(s)}
disabled={processingIds.has(s.id)}
className="text-[#555] hover:text-green-400 hover:bg-green-400/10 transition-colors text-xs font-medium px-2 py-1 rounded disabled:opacity-50"
>
Lift
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* History Tab */}
{activeTab === 'history' && (
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
{suspensions.length === 0 ? (
<div className="text-center py-16 text-[#555]">
<p className="text-sm">No suspension history</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[#1f2937]">
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-5 py-3 font-semibold">User</th>
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-4 py-3 font-semibold">Type</th>
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-4 py-3 font-semibold hidden md:table-cell">Reason</th>
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-4 py-3 font-semibold hidden sm:table-cell">Status</th>
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-4 py-3 font-semibold hidden lg:table-cell">Date</th>
</tr>
</thead>
<tbody>
{suspensions.map((s) => (
<tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] text-xs font-bold flex-shrink-0">
{(s.user?.full_name?.[0] || s.user?.email?.[0] || '?').toUpperCase()}
</div>
<div className="min-w-0">
<p className="text-white text-sm truncate">{s.user?.full_name || s.user?.email || '—'}</p>
</div>
</div>
</td>
<td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${
s.suspension_type === 'banned' ?'text-red-400 bg-red-400/10' :'text-orange-400 bg-orange-400/10'
}`}>
{s.suspension_type}
</span>
</td>
<td className="px-4 py-3.5 hidden md:table-cell">
<p className="text-[#888] text-xs max-w-[200px] truncate">{s.reason}</p>
</td>
<td className="px-4 py-3.5 hidden sm:table-cell">
<span className={`text-xs font-medium ${
s.is_active ? 'text-orange-400' : 'text-[#555]'
}`}>
{s.is_active ? 'Active' : s.lifted_at ? 'Lifted' : 'Expired'}
</span>
</td>
<td className="px-4 py-3.5 hidden lg:table-cell text-[#555] text-xs">
{formatDate(s.created_at)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* Manage Users Tab */}
{activeTab === 'users' && (
<div>
<div className="mb-4">
<div className="relative max-w-xs">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 text-[#555]" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
placeholder="Search users..."
value={userSearch}
onChange={(e) => setUserSearch(e.target.value)}
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#3b82f6]"
/>
</div>
</div>
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[#1f2937]">
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-5 py-3 font-semibold">User</th>
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-4 py-3 font-semibold hidden sm:table-cell">Role</th>
<th className="text-left text-[#555] text-xs uppercase tracking-wider px-4 py-3 font-semibold">Status</th>
<th className="text-right text-[#555] text-xs uppercase tracking-wider px-5 py-3 font-semibold">Actions</th>
</tr>
</thead>
<tbody>
{filteredUsers.map((u) => {
const isSuspended = suspendedUserIds.has(u.id);
const activeSusp = activeSuspensions.find(s => s.user_id === u.id);
return (
<tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] font-bold text-sm flex-shrink-0 overflow-hidden">
{u.avatar_url ? (
<img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" />
) : (
(u.full_name?.[0] || u.email?.[0] || '?').toUpperCase()
)}
</div>
<div className="min-w-0">
<p className="text-white text-sm font-medium truncate">{u.full_name || '—'}</p>
<p className="text-[#555] text-xs truncate">{u.email}</p>
</div>
</div>
</td>
<td className="px-4 py-3.5 hidden sm:table-cell">
<span className="text-[#888] text-xs capitalize">{u.role}</span>
</td>
<td className="px-4 py-3.5">
{isSuspended ? (
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-semibold ${
activeSusp?.suspension_type === 'banned' ?'text-red-400 bg-red-400/10' :'text-orange-400 bg-orange-400/10'
}`}>
<span className="w-1.5 h-1.5 rounded-full bg-current" />
{activeSusp?.suspension_type === 'banned' ? 'Banned' : 'Suspended'}
</span>
) : (
<span className="inline-flex items-center gap-1 text-xs font-medium text-green-400">
<span className="w-1.5 h-1.5 rounded-full bg-green-400" />
Active
</span>
)}
</td>
<td className="px-5 py-3.5">
<div className="flex items-center justify-end gap-2">
{isSuspended && activeSusp ? (
<button
onClick={() => openLiftModal(activeSusp)}
className="text-[#555] hover:text-green-400 hover:bg-green-400/10 transition-colors text-xs font-medium px-2 py-1 rounded"
>
Lift Restriction
</button>
) : (
u.role !== 'admin' && (
<button
onClick={() => openSuspendModal(u)}
className="text-[#555] hover:text-orange-400 hover:bg-orange-400/10 transition-colors text-xs font-medium px-2 py-1 rounded"
>
Suspend / Ban
</button>
)
)}
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
)}
</>
)}
</div>
{/* Suspend Modal */}
{showSuspendModal && selectedUser && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
<div className="bg-[#111827] border border-[#1f2937] rounded-xl w-full max-w-md shadow-2xl">
<div className="flex items-center justify-between px-6 py-4 border-b border-[#1f2937]">
<h2 className="text-white font-bold text-base">Suspend / Ban User</h2>
<button
onClick={() => setShowSuspendModal(false)}
className="text-[#555] hover:text-white transition-colors"
aria-label="Close"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleSuspend} className="px-6 py-5 space-y-4">
{/* User info */}
<div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#3b82f6] font-bold text-sm flex-shrink-0 overflow-hidden">
{selectedUser.avatar_url ? (
<img src={selectedUser.avatar_url} alt={selectedUser.full_name || selectedUser.email} className="w-full h-full object-cover" />
) : (
(selectedUser.full_name?.[0] || selectedUser.email?.[0] || '?').toUpperCase()
)}
</div>
<div className="min-w-0">
<p className="text-white text-sm font-medium truncate">{selectedUser.full_name || '—'}</p>
<p className="text-[#555] text-xs truncate">{selectedUser.email}</p>
</div>
</div>
{/* Type */}
<div>
<label className="block text-[#888] text-xs font-semibold uppercase tracking-wider mb-2">
Action Type
</label>
<div className="grid grid-cols-2 gap-2">
{(['suspended', 'banned'] as const).map((t) => (
<label
key={t}
className={`flex items-center gap-2 p-3 rounded-lg border cursor-pointer transition-colors ${
suspendType === t
? t === 'banned' ? 'border-red-500 bg-red-500/10' : 'border-orange-500 bg-orange-500/10' :'border-[#1f2937] hover:border-[#2a3a50]'
}`}
>
<input
type="radio"
name="suspendType"
value={t}
checked={suspendType === t}
onChange={() => {
setSuspendType(t);
if (t === 'banned') setSuspendDuration(null);
else setSuspendDuration(7);
}}
className="sr-only"
/>
<span className={`w-3 h-3 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${
suspendType === t
? t === 'banned' ? 'border-red-400' : 'border-orange-400' :'border-[#444]'
}`}>
{suspendType === t && <span className={`w-1.5 h-1.5 rounded-full ${t === 'banned' ? 'bg-red-400' : 'bg-orange-400'}`} />}
</span>
<span className={`text-sm font-semibold capitalize ${
suspendType === t
? t === 'banned' ? 'text-red-400' : 'text-orange-400' :'text-[#888]'
}`}>
{t === 'banned' ? '🚫 Ban' : '⏸ Suspend'}
</span>
</label>
))}
</div>
</div>
{/* Duration (only for suspend) */}
{suspendType === 'suspended' && (
<div>
<label className="block text-[#888] text-xs font-semibold uppercase tracking-wider mb-1.5">
Duration
</label>
<select
value={suspendDuration ?? ''}
onChange={(e) => setSuspendDuration(e.target.value ? parseInt(e.target.value) : null)}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]"
>
{DURATION_OPTIONS.filter(d => d.value !== null).map((d) => (
<option key={d.value} value={d.value!}>{d.label}</option>
))}
</select>
</div>
)}
{/* Reason */}
<div>
<label className="block text-[#888] text-xs font-semibold uppercase tracking-wider mb-1.5">
Reason <span className="text-red-400">*</span>
</label>
<textarea
required
value={suspendReason}
onChange={(e) => setSuspendReason(e.target.value)}
placeholder="Explain the reason for this action..."
rows={3}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#3b82f6] resize-none"
/>
</div>
<div className="bg-[#0d1117] border border-[#1f2937] rounded-lg p-3">
<p className="text-[#555] text-xs leading-relaxed">
{suspendType === 'banned' ?'🚫 The user will be permanently banned and cannot post. A notification email will be sent.'
: `⏸ The user will be suspended for ${suspendDuration} day${suspendDuration !== 1 ? 's' : ''} and cannot post. A notification email will be sent.`}
</p>
</div>
<div className="flex items-center gap-3 pt-1">
<button
type="button"
onClick={() => setShowSuspendModal(false)}
className="flex-1 bg-[#1f2937] hover:bg-[#2a3a50] text-[#888] text-sm font-semibold py-2.5 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={suspending || !suspendReason.trim()}
className={`flex-1 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2 ${
suspendType === 'banned' ?'bg-red-600 hover:bg-red-700' :'bg-orange-600 hover:bg-orange-700'
}`}
>
{suspending ? (
<>
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Processing...
</>
) : (
suspendType === 'banned' ? 'Ban User' : 'Suspend User'
)}
</button>
</div>
</form>
</div>
</div>
)}
{/* Lift Suspension Modal */}
{showLiftModal && liftingSuspension && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm">
<div className="bg-[#111827] border border-[#1f2937] rounded-xl w-full max-w-sm shadow-2xl">
<div className="flex items-center justify-between px-6 py-4 border-b border-[#1f2937]">
<h2 className="text-white font-bold text-base">Lift Restriction</h2>
<button
onClick={() => setShowLiftModal(false)}
className="text-[#555] hover:text-white transition-colors"
aria-label="Close"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleLiftSuspension} className="px-6 py-5 space-y-4">
<div className="bg-[#0d1117] rounded-lg p-3">
<p className="text-[#888] text-xs mb-1">Lifting restriction for</p>
<p className="text-white text-sm font-medium">{liftingSuspension.user?.full_name || liftingSuspension.user?.email}</p>
<p className="text-[#555] text-xs mt-1">Reason: {liftingSuspension.reason}</p>
</div>
<div>
<label className="block text-[#888] text-xs font-semibold uppercase tracking-wider mb-1.5">
Lift Reason <span className="text-[#444]">(optional)</span>
</label>
<textarea
value={liftReason}
onChange={(e) => setLiftReason(e.target.value)}
placeholder="Reason for lifting this restriction..."
rows={2}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#3b82f6] resize-none"
/>
</div>
<div className="flex items-center gap-3 pt-1">
<button
type="button"
onClick={() => setShowLiftModal(false)}
className="flex-1 bg-[#1f2937] hover:bg-[#2a3a50] text-[#888] text-sm font-semibold py-2.5 rounded-lg transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={lifting}
className="flex-1 bg-green-600 hover:bg-green-700 disabled:opacity-50 text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
>
{lifting ? (
<>
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Lifting...
</>
) : (
'Lift Restriction'
)}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}