215 lines
7.3 KiB
TypeScript
215 lines
7.3 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { Message } from '../types';
|
|
import { messageAPI } from '../services/api';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
|
|
const MessageDetail: React.FC = () => {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const { user } = useAuth();
|
|
const [message, setMessage] = useState<Message | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [replyContent, setReplyContent] = useState('');
|
|
const [sending, setSending] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchMessage();
|
|
}, [id]);
|
|
|
|
const fetchMessage = async () => {
|
|
try {
|
|
const response = await messageAPI.getMessage(id!);
|
|
setMessage(response.data);
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.error || 'Failed to fetch message');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleReply = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!message) return;
|
|
|
|
setSending(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const recipientId = message.senderId === user?.id ? message.receiverId : message.senderId;
|
|
await messageAPI.sendMessage({
|
|
receiverId: recipientId,
|
|
subject: `Re: ${message.subject}`,
|
|
content: replyContent,
|
|
parentMessageId: message.id
|
|
});
|
|
|
|
setReplyContent('');
|
|
fetchMessage(); // Refresh to show the new reply
|
|
alert('Reply sent successfully!');
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.error || 'Failed to send reply');
|
|
} finally {
|
|
setSending(false);
|
|
}
|
|
};
|
|
|
|
const formatDateTime = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
hour: 'numeric',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="container mt-5">
|
|
<div className="text-center">
|
|
<div className="spinner-border" role="status">
|
|
<span className="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!message) {
|
|
return (
|
|
<div className="container mt-5">
|
|
<div className="alert alert-danger" role="alert">
|
|
Message not found
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const isReceiver = message.receiverId === user?.id;
|
|
const otherUser = isReceiver ? message.sender : message.receiver;
|
|
|
|
return (
|
|
<div className="container mt-4">
|
|
<div className="row justify-content-center">
|
|
<div className="col-md-8">
|
|
<button
|
|
className="btn btn-link text-decoration-none mb-3"
|
|
onClick={() => navigate('/messages')}
|
|
>
|
|
<i className="bi bi-arrow-left"></i> Back to Messages
|
|
</button>
|
|
|
|
<div className="card">
|
|
<div className="card-header">
|
|
<div className="d-flex align-items-center">
|
|
{otherUser?.profileImage ? (
|
|
<img
|
|
src={otherUser.profileImage}
|
|
alt={`${otherUser.firstName} ${otherUser.lastName}`}
|
|
className="rounded-circle me-3"
|
|
style={{ width: '50px', height: '50px', objectFit: 'cover' }}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-3"
|
|
style={{ width: '50px', height: '50px' }}
|
|
>
|
|
<i className="bi bi-person-fill text-white"></i>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<h5 className="mb-0">{message.subject}</h5>
|
|
<small className="text-muted">
|
|
{isReceiver ? 'From' : 'To'}: {otherUser?.firstName} {otherUser?.lastName} • {formatDateTime(message.createdAt)}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="card-body">
|
|
<p style={{ whiteSpace: 'pre-wrap' }}>{message.content}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{message.replies && message.replies.length > 0 && (
|
|
<div className="mt-4">
|
|
<h6>Replies</h6>
|
|
{message.replies.map((reply) => (
|
|
<div key={reply.id} className="card mb-2">
|
|
<div className="card-body">
|
|
<div className="d-flex align-items-center mb-2">
|
|
{reply.sender?.profileImage ? (
|
|
<img
|
|
src={reply.sender.profileImage}
|
|
alt={`${reply.sender.firstName} ${reply.sender.lastName}`}
|
|
className="rounded-circle me-2"
|
|
style={{ width: '30px', height: '30px', objectFit: 'cover' }}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="rounded-circle bg-secondary d-flex align-items-center justify-content-center me-2"
|
|
style={{ width: '30px', height: '30px' }}
|
|
>
|
|
<i className="bi bi-person-fill text-white" style={{ fontSize: '0.8rem' }}></i>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<strong>{reply.sender?.firstName} {reply.sender?.lastName}</strong>
|
|
<small className="text-muted ms-2">{formatDateTime(reply.createdAt)}</small>
|
|
</div>
|
|
</div>
|
|
<p className="mb-0" style={{ whiteSpace: 'pre-wrap' }}>{reply.content}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="card mt-4">
|
|
<div className="card-body">
|
|
<h6>Send Reply</h6>
|
|
{error && (
|
|
<div className="alert alert-danger" role="alert">
|
|
{error}
|
|
</div>
|
|
)}
|
|
<form onSubmit={handleReply}>
|
|
<div className="mb-3">
|
|
<textarea
|
|
className="form-control"
|
|
rows={4}
|
|
value={replyContent}
|
|
onChange={(e) => setReplyContent(e.target.value)}
|
|
placeholder="Type your reply..."
|
|
required
|
|
disabled={sending}
|
|
></textarea>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary"
|
|
disabled={sending || !replyContent.trim()}
|
|
>
|
|
{sending ? (
|
|
<>
|
|
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
|
Sending...
|
|
</>
|
|
) : (
|
|
<>
|
|
<i className="bi bi-send-fill me-2"></i>Send Reply
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MessageDetail; |