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;
|
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',
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
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