Admin: preset reason options for ban/suspend/activate modals + custom option

This commit is contained in:
Patrick Britton 2026-02-06 12:41:43 -06:00
parent e5fd9bcaa5
commit 1d8ef9135e
2 changed files with 106 additions and 12 deletions

View file

@ -28,6 +28,16 @@ export default function ModerationPage() {
const [statusFilter, setStatusFilter] = useState('pending');
const [reviewingId, setReviewingId] = useState<string | null>(null);
const [reason, setReason] = useState('');
const [customReason, setCustomReason] = useState(false);
const banReasons = [
'Hate speech or slurs',
'Harassment or bullying',
'Spam or scam activity',
'Posting illegal content',
'Repeated violations after warnings',
'Ban evasion (alt account)',
];
const [selected, setSelected] = useState<Set<string>>(new Set());
const [bulkLoading, setBulkLoading] = useState(false);
@ -198,9 +208,36 @@ export default function ModerationPage() {
{reviewingId === item.id && (
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm font-medium text-red-800 mb-2">Ban user and remove content</p>
<input className="input mb-2" placeholder="Reason for ban..." value={reason} onChange={(e) => setReason(e.target.value)} />
<div className="space-y-1.5 mb-3">
{banReasons.map((preset) => (
<button
key={preset}
onClick={() => { setReason(preset); setCustomReason(false); }}
className={`w-full text-left px-3 py-1.5 rounded text-xs border transition-colors ${
reason === preset && !customReason
? 'border-red-400 bg-red-100 text-red-800 font-medium'
: 'border-red-200 hover:border-red-300 text-red-700'
}`}
>
{preset}
</button>
))}
<button
onClick={() => { setCustomReason(true); setReason(''); }}
className={`w-full text-left px-3 py-1.5 rounded text-xs border transition-colors ${
customReason
? 'border-red-400 bg-red-100 text-red-800 font-medium'
: 'border-red-200 hover:border-red-300 text-red-700'
}`}
>
Custom reason...
</button>
</div>
{customReason && (
<input className="input mb-2 text-sm" placeholder="Enter custom reason..." value={reason} onChange={(e) => setReason(e.target.value)} autoFocus />
)}
<div className="flex gap-2">
<button onClick={() => { setReviewingId(null); setReason(''); }} className="btn-secondary text-xs">Cancel</button>
<button onClick={() => { setReviewingId(null); setReason(''); setCustomReason(false); }} className="btn-secondary text-xs">Cancel</button>
<button onClick={() => handleReview(item.id, 'ban_user')} className="btn-danger text-xs" disabled={!reason.trim()}>Confirm Ban</button>
</div>
</div>

View file

@ -16,6 +16,32 @@ export default function UserDetailPage() {
const [actionLoading, setActionLoading] = useState(false);
const [showModal, setShowModal] = useState<string | null>(null);
const [reason, setReason] = useState('');
const [customReason, setCustomReason] = useState(false);
const reasonPresets: Record<string, string[]> = {
banned: [
'Hate speech or slurs',
'Harassment or bullying',
'Spam or scam activity',
'Posting illegal content',
'Impersonation',
'Repeated violations after warnings',
'Ban evasion (alt account)',
],
suspended: [
'Posting inappropriate content',
'Minor harassment',
'Spam behavior',
'Violating community guidelines',
'Cooling-off period after heated exchange',
],
active: [
'Appeal reviewed and approved',
'Ban was issued in error',
'Suspension period served',
'User agreed to follow guidelines',
],
};
const fetchUser = () => {
setLoading(true);
@ -236,21 +262,52 @@ export default function UserDetailPage() {
{/* Status Change Modal */}
{showModal && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50" onClick={() => setShowModal(null)}>
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50" onClick={() => { setShowModal(null); setReason(''); setCustomReason(false); }}>
<div className="card p-6 w-full max-w-md mx-4" onClick={(e) => e.stopPropagation()}>
<h3 className="text-lg font-semibold text-gray-900 mb-1">
{showModal === 'active' ? 'Activate' : showModal === 'suspended' ? 'Suspend' : 'Ban'} User
</h3>
<p className="text-sm text-gray-500 mb-4">Please provide a reason for this action.</p>
<textarea
className="input mb-4"
rows={3}
placeholder="Reason..."
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
<p className="text-sm text-gray-500 mb-4">Select a reason for this action.</p>
<div className="space-y-2 mb-4">
{(reasonPresets[showModal] || []).map((preset) => (
<button
key={preset}
onClick={() => { setReason(preset); setCustomReason(false); }}
className={`w-full text-left px-3 py-2 rounded-lg text-sm border transition-colors ${
reason === preset && !customReason
? 'border-brand-500 bg-brand-50 text-brand-700 font-medium'
: 'border-warm-300 hover:border-gray-400 text-gray-700'
}`}
>
{preset}
</button>
))}
<button
onClick={() => { setCustomReason(true); setReason(''); }}
className={`w-full text-left px-3 py-2 rounded-lg text-sm border transition-colors ${
customReason
? 'border-brand-500 bg-brand-50 text-brand-700 font-medium'
: 'border-warm-300 hover:border-gray-400 text-gray-700'
}`}
>
Custom reason...
</button>
</div>
{customReason && (
<textarea
className="input mb-4"
rows={3}
placeholder="Enter custom reason..."
value={reason}
onChange={(e) => setReason(e.target.value)}
autoFocus
/>
)}
<div className="flex gap-2 justify-end">
<button onClick={() => setShowModal(null)} className="btn-secondary text-sm">Cancel</button>
<button onClick={() => { setShowModal(null); setReason(''); setCustomReason(false); }} className="btn-secondary text-sm">Cancel</button>
<button
onClick={() => handleStatusChange(showModal)}
className={showModal === 'banned' ? 'btn-danger text-sm' : 'btn-primary text-sm'}