feat: implement user reporting functionality with a dedicated modal and service, integrated into the chat header.

This commit is contained in:
pradeepkumar
2026-03-19 05:24:41 +05:30
parent 776aeaac8d
commit 3a5377dd35
4 changed files with 210 additions and 3 deletions

View File

@@ -16,6 +16,7 @@ interface ChatHeaderProps {
typingText?: string; typingText?: string;
onClearChat?: () => void; onClearChat?: () => void;
onDeleteConversation?: () => void; onDeleteConversation?: () => void;
onReportUser?: () => void;
} }
export function ChatHeader({ export function ChatHeader({
@@ -30,6 +31,7 @@ export function ChatHeader({
typingText, typingText,
onClearChat, onClearChat,
onDeleteConversation, onDeleteConversation,
onReportUser,
}: ChatHeaderProps) { }: ChatHeaderProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
@@ -49,9 +51,7 @@ export function ChatHeader({
}, },
{ {
label: 'Report User', label: 'Report User',
onClick: () => { onClick: () => onReportUser?.(),
// Placeholder — no backend endpoint yet
},
}, },
{ {
label: 'Delete Conversation', label: 'Delete Conversation',

View File

@@ -7,6 +7,7 @@ import { ChatHeader } from './ChatHeader';
import { MessageInput } from './MessageInput'; import { MessageInput } from './MessageInput';
import { ChatDropdownMenu, type DropdownMenuItem } from './ChatDropdownMenu'; import { ChatDropdownMenu, type DropdownMenuItem } from './ChatDropdownMenu';
import { NewConversationModal, type ConnectedContact } from './NewConversationModal'; import { NewConversationModal, type ConnectedContact } from './NewConversationModal';
import { ReportUserModal } from './ReportUserModal';
import { useMessaging } from '@/hooks/useMessaging'; import { useMessaging } from '@/hooks/useMessaging';
import { connectionRequestsService, type ConnectionRequest } from '@/services/connection-requests.service'; import { connectionRequestsService, type ConnectionRequest } from '@/services/connection-requests.service';
import { uploadService } from '@/services/upload.service'; import { uploadService } from '@/services/upload.service';
@@ -159,6 +160,7 @@ export function MessagingPage({ initialConversationId }: { initialConversationId
const [isLoadingAgents, setIsLoadingAgents] = useState(false); const [isLoadingAgents, setIsLoadingAgents] = useState(false);
const [isStartingConversation, setIsStartingConversation] = useState<string | null>(null); const [isStartingConversation, setIsStartingConversation] = useState<string | null>(null);
const [isComposeOpen, setIsComposeOpen] = useState(false); const [isComposeOpen, setIsComposeOpen] = useState(false);
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
// Store converted avatar URLs by conversation ID // Store converted avatar URLs by conversation ID
const [conversationAvatarUrls, setConversationAvatarUrls] = useState<Record<string, string>>({}); const [conversationAvatarUrls, setConversationAvatarUrls] = useState<Record<string, string>>({});
// Store converted file URLs by message ID (for S3-stored images/files) // Store converted file URLs by message ID (for S3-stored images/files)
@@ -595,6 +597,17 @@ export function MessagingPage({ initialConversationId }: { initialConversationId
}} }}
/> />
{/* Report user modal */}
{currentConversation && (
<ReportUserModal
isOpen={isReportModalOpen}
onClose={() => setIsReportModalOpen(false)}
reportedUserId={currentConversation.otherParty?.userId || currentConversation.otherParty?.id || ''}
reportedUserName={currentConversation.otherParty?.name || 'User'}
conversationId={currentConversation.id}
/>
)}
<div className="border border-[#00293d]/10 rounded-[20px] bg-white overflow-hidden"> <div className="border border-[#00293d]/10 rounded-[20px] bg-white overflow-hidden">
{/* Header */} {/* Header */}
<div className="flex items-center gap-4 p-4 border-b border-[#00293d]/10"> <div className="flex items-center gap-4 p-4 border-b border-[#00293d]/10">
@@ -818,6 +831,7 @@ export function MessagingPage({ initialConversationId }: { initialConversationId
typingText={typingUserNames.length > 0 ? `${typingUserNames.join(', ')} is typing...` : undefined} typingText={typingUserNames.length > 0 ? `${typingUserNames.join(', ')} is typing...` : undefined}
onClearChat={clearMessages} onClearChat={clearMessages}
onDeleteConversation={handleDeleteConversation} onDeleteConversation={handleDeleteConversation}
onReportUser={() => setIsReportModalOpen(true)}
/> />
{/* Messages area wrapper */} {/* Messages area wrapper */}

View File

@@ -0,0 +1,164 @@
'use client';
import { useState } from 'react';
import { userReportsService } from '@/services/user-reports.service';
const REPORT_REASONS = [
'Spam or scam',
'Harassment or bullying',
'Inappropriate content',
'Fake profile',
'Unprofessional behavior',
'Other',
];
interface ReportUserModalProps {
isOpen: boolean;
onClose: () => void;
reportedUserId: string;
reportedUserName: string;
conversationId?: string;
}
export function ReportUserModal({
isOpen,
onClose,
reportedUserId,
reportedUserName,
conversationId,
}: ReportUserModalProps) {
const [selectedReason, setSelectedReason] = useState('');
const [description, setDescription] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState('');
if (!isOpen) return null;
const handleSubmit = async () => {
if (!selectedReason) {
setError('Please select a reason');
return;
}
setIsSubmitting(true);
setError('');
try {
await userReportsService.createReport({
reportedUserId,
conversationId,
reason: selectedReason,
description: description.trim() || undefined,
});
setSubmitted(true);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Failed to submit report';
// Check for axios error response
const axiosErr = err as { response?: { data?: { message?: string } } };
setError(axiosErr?.response?.data?.message || message);
} finally {
setIsSubmitting(false);
}
};
const handleClose = () => {
setSelectedReason('');
setDescription('');
setSubmitted(false);
setError('');
onClose();
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-[20px] w-full max-w-[440px] overflow-hidden">
{submitted ? (
// Success state
<div className="p-6 text-center">
<div className="w-14 h-14 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-7 h-7 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h3 className="font-fractul font-bold text-[18px] text-[#00293D] mb-2">Report Submitted</h3>
<p className="font-serif text-[14px] text-[#00293D]/70 mb-6">
Thank you for reporting. Our team will review this and take appropriate action.
</p>
<button
onClick={handleClose}
className="px-6 py-2.5 bg-[#E58625] text-white rounded-full font-serif font-semibold text-[14px] hover:bg-[#E58625]/90 transition-colors cursor-pointer"
>
Done
</button>
</div>
) : (
// Report form
<>
<div className="px-6 py-4 border-b border-[#00293d]/10">
<h3 className="font-fractul font-bold text-[16px] text-[#00293D]">
Report {reportedUserName}
</h3>
<p className="font-serif text-[13px] text-[#00293D]/60 mt-1">
Select a reason for reporting this user
</p>
</div>
<div className="px-6 py-4 space-y-2 max-h-[300px] overflow-y-auto">
{REPORT_REASONS.map((reason) => (
<label
key={reason}
className={`flex items-center gap-3 p-3 rounded-[10px] cursor-pointer transition-colors ${
selectedReason === reason
? 'bg-[#E58625]/10 border border-[#E58625]'
: 'border border-[#00293d]/10 hover:bg-[#00293d]/5'
}`}
>
<input
type="radio"
name="report-reason"
value={reason}
checked={selectedReason === reason}
onChange={() => setSelectedReason(reason)}
className="w-4 h-4 text-[#E58625] accent-[#E58625]"
/>
<span className="font-serif text-[14px] text-[#00293D]">{reason}</span>
</label>
))}
{selectedReason && (
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add more details (optional)"
rows={3}
className="w-full mt-3 px-3 py-2 border border-[#00293d]/10 rounded-[10px] font-serif text-[14px] text-[#00293D] placeholder:text-[#00293D]/40 focus:outline-none focus:border-[#E58625] resize-none"
/>
)}
{error && (
<p className="text-red-500 font-serif text-[13px] mt-2">{error}</p>
)}
</div>
<div className="px-6 py-4 border-t border-[#00293d]/10 flex justify-end gap-3">
<button
onClick={handleClose}
className="px-5 py-2.5 border border-[#00293d]/20 rounded-full font-serif font-medium text-[14px] text-[#00293D] hover:bg-[#00293d]/5 transition-colors cursor-pointer"
>
Cancel
</button>
<button
onClick={handleSubmit}
disabled={!selectedReason || isSubmitting}
className="px-5 py-2.5 bg-red-600 text-white rounded-full font-serif font-semibold text-[14px] hover:bg-red-700 transition-colors disabled:opacity-50 cursor-pointer"
>
{isSubmitting ? 'Submitting...' : 'Submit Report'}
</button>
</div>
</>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import api, { ApiResponse } from './api';
interface CreateReportDto {
reportedUserId: string;
conversationId?: string;
reason: string;
description?: string;
}
interface UserReport {
id: string;
reporterId: string;
reportedUserId: string;
conversationId: string | null;
reason: string;
description: string | null;
status: string;
createdAt: string;
}
class UserReportsService {
async createReport(dto: CreateReportDto): Promise<UserReport> {
const response = await api.post<ApiResponse<UserReport>>('/user-reports', dto);
return response.data.data;
}
}
export const userReportsService = new UserReportsService();
export type { CreateReportDto, UserReport };