Skip to content

Commit 1e5cfac

Browse files
Merge pull request #694 from ResearchHub/suspend_user_mod_dashboard
Adding Suspend User Button to Moderation Dashboard
2 parents e86ddcc + e7bd09e commit 1e5cfac

7 files changed

Lines changed: 112 additions & 33 deletions

File tree

app/moderators/audit/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export default function AuditPage() {
100100
loadMore={loadMore}
101101
onDismiss={handleDismiss}
102102
onRemove={handleRemove}
103+
onRefresh={refresh}
103104
view={getViewFromStatus(activeStatus)}
104105
/>
105106
</div>

components/Moderators/AuditContent.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface AuditContentProps {
1616
loadMore: () => void;
1717
onDismiss: (flagIds: ID[]) => Promise<boolean>;
1818
onRemove: (flagIds: ID[]) => Promise<boolean>;
19+
onRefresh?: () => void;
1920
header?: ReactNode;
2021
filters?: ReactNode;
2122
noEntriesElement?: ReactNode;
@@ -31,6 +32,7 @@ export const AuditContent: FC<AuditContentProps> = ({
3132
loadMore,
3233
onDismiss,
3334
onRemove,
35+
onRefresh,
3436
header,
3537
filters,
3638
noEntriesElement,
@@ -56,6 +58,7 @@ export const AuditContent: FC<AuditContentProps> = ({
5658
<AuditItemCard
5759
entry={entry}
5860
onAction={(action) => handleAction(action, entry.id)}
61+
onRefresh={onRefresh}
5962
view={view}
6063
/>
6164
</div>

components/Moderators/AuditItemCard.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { AuditItemPaper } from './AuditItemPaper';
99
interface AuditItemCardProps {
1010
entry: FlaggedContent;
1111
onAction: (action: 'dismiss' | 'remove') => void;
12+
onRefresh?: () => void;
1213
view?: 'pending' | 'dismissed' | 'removed';
1314
}
1415

@@ -34,17 +35,19 @@ const detectContentType = (entry: FlaggedContent): 'comment' | 'post' | 'paper'
3435
}
3536
};
3637

37-
export const AuditItemCard: FC<AuditItemCardProps> = ({ entry, onAction, view }) => {
38+
export const AuditItemCard: FC<AuditItemCardProps> = ({ entry, onAction, onRefresh, view }) => {
3839
const contentType = detectContentType(entry);
3940

4041
// Render the appropriate component based on content type
4142
switch (contentType) {
4243
case 'comment':
43-
return <AuditItemComment entry={entry} onAction={onAction} view={view} />;
44+
return (
45+
<AuditItemComment entry={entry} onAction={onAction} onRefresh={onRefresh} view={view} />
46+
);
4447
case 'post':
45-
return <AuditItemPost entry={entry} onAction={onAction} view={view} />;
48+
return <AuditItemPost entry={entry} onAction={onAction} onRefresh={onRefresh} view={view} />;
4649
case 'paper':
47-
return <AuditItemPaper entry={entry} onAction={onAction} view={view} />;
50+
return <AuditItemPaper entry={entry} onAction={onAction} onRefresh={onRefresh} view={view} />;
4851
default:
4952
return (
5053
<div className="bg-white border border-gray-200 rounded-lg p-4">

components/Moderators/AuditItemComment.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ModerationActions } from './ModerationActions';
1212
interface AuditItemCommentProps {
1313
entry: FlaggedContent;
1414
onAction: (action: 'dismiss' | 'remove') => void;
15+
onRefresh?: () => void;
1516
view?: 'pending' | 'dismissed' | 'removed';
1617
}
1718

@@ -177,9 +178,11 @@ const transformAuditCommentToFeedEntry = (entry: FlaggedContent): FeedEntry => {
177178
export const AuditItemComment: FC<AuditItemCommentProps> = ({
178179
entry,
179180
onAction,
181+
onRefresh,
180182
view = 'pending',
181183
}) => {
182184
const verdict = entry.verdict;
185+
const userInfo = getAuditUserInfo(entry);
183186

184187
// Transform audit entry to feed entry format
185188
const feedEntry = useMemo(() => transformAuditCommentToFeedEntry(entry), [entry]);
@@ -212,6 +215,9 @@ export const AuditItemComment: FC<AuditItemCommentProps> = ({
212215
onRemove={() => onAction('remove')}
213216
view={view}
214217
hasVerdict={!!verdict}
218+
authorId={userInfo.authorId}
219+
authorName={userInfo.name}
220+
onRefresh={onRefresh}
215221
/>
216222
</div>
217223
</div>

components/Moderators/AuditItemPaper.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@ import { truncateText } from '@/utils/stringUtils';
1717
interface AuditItemPaperProps {
1818
readonly entry: FlaggedContent;
1919
readonly onAction: (action: 'dismiss' | 'remove') => void;
20+
readonly onRefresh?: () => void;
2021
readonly view?: 'pending' | 'dismissed' | 'removed';
2122
}
2223

23-
export const AuditItemPaper: FC<AuditItemPaperProps> = ({ entry, onAction, view = 'pending' }) => {
24+
export const AuditItemPaper: FC<AuditItemPaperProps> = ({
25+
entry,
26+
onAction,
27+
onRefresh,
28+
view = 'pending',
29+
}) => {
2430
const userInfo = getAuditUserInfo(entry);
2531
const verdict = entry.verdict;
2632
const contentUrl = getAuditContentUrl(entry);
@@ -138,6 +144,9 @@ export const AuditItemPaper: FC<AuditItemPaperProps> = ({ entry, onAction, view
138144
onRemove={() => onAction('remove')}
139145
view={view}
140146
hasVerdict={!!verdict}
147+
authorId={userInfo.authorId}
148+
authorName={userInfo.name}
149+
onRefresh={onRefresh}
141150
/>
142151
</div>
143152
);

components/Moderators/AuditItemPost.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ModerationActions } from './ModerationActions';
1313
interface AuditItemPostProps {
1414
entry: FlaggedContent;
1515
onAction: (action: 'dismiss' | 'remove') => void;
16+
onRefresh?: () => void;
1617
view?: 'pending' | 'dismissed' | 'removed';
1718
}
1819

@@ -133,9 +134,15 @@ const transformAuditPostToFeedEntry = (
133134
return { feedEntry, relatedWork };
134135
};
135136

136-
export const AuditItemPost: FC<AuditItemPostProps> = ({ entry, onAction, view = 'pending' }) => {
137+
export const AuditItemPost: FC<AuditItemPostProps> = ({
138+
entry,
139+
onAction,
140+
onRefresh,
141+
view = 'pending',
142+
}) => {
137143
const verdict = entry.verdict;
138144
const contentUrl = getAuditContentUrl(entry);
145+
const userInfo = getAuditUserInfo(entry);
139146

140147
// Transform audit entry to feed entry format
141148
const { feedEntry, relatedWork } = useMemo(() => transformAuditPostToFeedEntry(entry), [entry]);
@@ -171,6 +178,9 @@ export const AuditItemPost: FC<AuditItemPostProps> = ({ entry, onAction, view =
171178
onRemove={() => onAction('remove')}
172179
view={view}
173180
hasVerdict={!!verdict}
181+
authorId={userInfo.authorId}
182+
authorName={userInfo.name}
183+
onRefresh={onRefresh}
174184
/>
175185
</div>
176186
</div>
Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,99 @@
11
'use client';
22

3-
import { FC } from 'react';
3+
import { FC, useState } from 'react';
44
import { Button } from '@/components/ui/Button';
5-
import { CheckCircle, XCircle } from 'lucide-react';
5+
import { CheckCircle, XCircle, Ban } from 'lucide-react';
6+
import { ConfirmModal } from '@/components/modals/ConfirmModal';
7+
import { UserModerationService } from '@/services/user-moderation.service';
8+
import { cn } from '@/utils/styles';
9+
import { toast } from 'react-hot-toast';
610

711
interface ModerationActionsProps {
812
onDismiss: () => void;
913
onRemove: () => void;
14+
onRefresh?: () => void;
1015
view?: 'pending' | 'dismissed' | 'removed';
1116
hasVerdict?: boolean;
1217
className?: string;
18+
authorId?: number | null;
19+
authorName?: string;
1320
}
1421

1522
export const ModerationActions: FC<ModerationActionsProps> = ({
1623
onDismiss,
1724
onRemove,
25+
onRefresh,
1826
view = 'pending',
1927
hasVerdict = false,
20-
className = '',
28+
className,
29+
authorId,
30+
authorName,
2131
}) => {
22-
// Only show action buttons for pending items without verdicts
23-
const showActionButtons = view === 'pending' && !hasVerdict;
32+
const [showSuspendConfirm, setShowSuspendConfirm] = useState(false);
2433

25-
if (!showActionButtons) {
34+
if (view !== 'pending' || hasVerdict) {
2635
return null;
2736
}
2837

38+
const handleSuspendUser = async () => {
39+
if (!authorId) return;
40+
try {
41+
await UserModerationService.suspendUser(String(authorId));
42+
toast.success('User has been suspended');
43+
onRefresh?.();
44+
} catch (error) {
45+
console.error('Failed to suspend user:', error);
46+
toast.error('Failed to suspend user');
47+
}
48+
};
49+
2950
return (
30-
<div className={`flex items-center space-x-2 ${className}`}>
31-
<Button
32-
variant="ghost"
33-
size="sm"
34-
onClick={onDismiss}
35-
className="text-green-600 hover:text-green-700 hover:bg-green-50"
36-
>
37-
<CheckCircle className="h-4 w-4 mr-1" />
38-
Dismiss
39-
</Button>
40-
41-
<Button
42-
variant="ghost"
43-
size="sm"
44-
onClick={onRemove}
45-
className="text-red-600 hover:text-red-700 hover:bg-red-50"
46-
>
47-
<XCircle className="h-4 w-4 mr-1" />
48-
Remove
49-
</Button>
50-
</div>
51+
<>
52+
<div className={cn('flex items-center space-x-2', className)}>
53+
<Button
54+
variant="ghost"
55+
size="sm"
56+
onClick={onDismiss}
57+
className="text-green-600 hover:text-green-700 hover:bg-green-50"
58+
>
59+
<CheckCircle className="h-4 w-4 mr-1" />
60+
Dismiss
61+
</Button>
62+
63+
<Button
64+
variant="ghost"
65+
size="sm"
66+
onClick={onRemove}
67+
className="text-red-600 hover:text-red-700 hover:bg-red-50"
68+
>
69+
<XCircle className="h-4 w-4 mr-1" />
70+
Remove
71+
</Button>
72+
73+
{authorId && (
74+
<Button
75+
variant="ghost"
76+
size="sm"
77+
onClick={() => setShowSuspendConfirm(true)}
78+
className="text-red-600 hover:text-red-700 hover:bg-red-50"
79+
>
80+
<Ban className="h-4 w-4 mr-1" />
81+
Remove + Suspend
82+
</Button>
83+
)}
84+
</div>
85+
86+
{authorId && (
87+
<ConfirmModal
88+
isOpen={showSuspendConfirm}
89+
onClose={() => setShowSuspendConfirm(false)}
90+
onConfirm={handleSuspendUser}
91+
title="Remove + Suspend"
92+
message={`Are you sure you want to suspend ${authorName || 'this user'}? This will remove their content and prevent them from using the platform.`}
93+
confirmText="Remove + Suspend"
94+
confirmButtonClass="bg-red-600 hover:bg-red-700"
95+
/>
96+
)}
97+
</>
5198
);
5299
};

0 commit comments

Comments
 (0)