feat: implement user reporting functionality with a dedicated modal and service, integrated into the chat header.
This commit is contained in:
@@ -16,6 +16,7 @@ interface ChatHeaderProps {
|
||||
typingText?: string;
|
||||
onClearChat?: () => void;
|
||||
onDeleteConversation?: () => void;
|
||||
onReportUser?: () => void;
|
||||
}
|
||||
|
||||
export function ChatHeader({
|
||||
@@ -30,6 +31,7 @@ export function ChatHeader({
|
||||
typingText,
|
||||
onClearChat,
|
||||
onDeleteConversation,
|
||||
onReportUser,
|
||||
}: ChatHeaderProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isMuted, setIsMuted] = useState(false);
|
||||
@@ -49,9 +51,7 @@ export function ChatHeader({
|
||||
},
|
||||
{
|
||||
label: 'Report User',
|
||||
onClick: () => {
|
||||
// Placeholder — no backend endpoint yet
|
||||
},
|
||||
onClick: () => onReportUser?.(),
|
||||
},
|
||||
{
|
||||
label: 'Delete Conversation',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ChatHeader } from './ChatHeader';
|
||||
import { MessageInput } from './MessageInput';
|
||||
import { ChatDropdownMenu, type DropdownMenuItem } from './ChatDropdownMenu';
|
||||
import { NewConversationModal, type ConnectedContact } from './NewConversationModal';
|
||||
import { ReportUserModal } from './ReportUserModal';
|
||||
import { useMessaging } from '@/hooks/useMessaging';
|
||||
import { connectionRequestsService, type ConnectionRequest } from '@/services/connection-requests.service';
|
||||
import { uploadService } from '@/services/upload.service';
|
||||
@@ -159,6 +160,7 @@ export function MessagingPage({ initialConversationId }: { initialConversationId
|
||||
const [isLoadingAgents, setIsLoadingAgents] = useState(false);
|
||||
const [isStartingConversation, setIsStartingConversation] = useState<string | null>(null);
|
||||
const [isComposeOpen, setIsComposeOpen] = useState(false);
|
||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||
// Store converted avatar URLs by conversation ID
|
||||
const [conversationAvatarUrls, setConversationAvatarUrls] = useState<Record<string, string>>({});
|
||||
// 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">
|
||||
{/* Header */}
|
||||
<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}
|
||||
onClearChat={clearMessages}
|
||||
onDeleteConversation={handleDeleteConversation}
|
||||
onReportUser={() => setIsReportModalOpen(true)}
|
||||
/>
|
||||
|
||||
{/* Messages area wrapper */}
|
||||
|
||||
164
src/components/message/ReportUserModal.tsx
Normal file
164
src/components/message/ReportUserModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
29
src/services/user-reports.service.ts
Normal file
29
src/services/user-reports.service.ts
Normal 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 };
|
||||
Reference in New Issue
Block a user