Files
Hotel-Booking/Frontend/src/features/notifications/components/ChatWidget.tsx
Iliyan Angelov e43a95eafb updates
2025-12-09 00:14:21 +02:00

737 lines
31 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import { MessageCircle, X, Send, Minimize2, Maximize2, Clock } from 'lucide-react';
import chatService, { type Chat, type ChatMessage } from '../services/chatService';
import contactService, { type ContactFormData } from '../../content/services/contactService';
import useAuthStore from '../../../store/useAuthStore';
import { useCompanySettings } from '../../../shared/contexts/CompanySettingsContext';
import { toast } from 'react-toastify';
import ConfirmationDialog from '../../../shared/components/ConfirmationDialog';
import { formatWorkingHours } from '../../../shared/utils/format';
interface ChatWidgetProps {
onClose?: () => void;
}
const ChatWidget: React.FC<ChatWidgetProps> = ({ onClose }) => {
const [isOpen, setIsOpen] = useState(false);
const [isMinimized, setIsMinimized] = useState(false);
const [chat, setChat] = useState<Chat | null>(null);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [newMessage, setNewMessage] = useState('');
const [loading, setLoading] = useState(false);
const [ws, setWs] = useState<WebSocket | null>(null);
const [visitorInfo, setVisitorInfo] = useState({
name: '',
email: '',
phone: ''
});
const [showVisitorForm, setShowVisitorForm] = useState(false);
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const [showEndChatDialog, setShowEndChatDialog] = useState(false);
const [inquiry, setInquiry] = useState('');
const [inquiryEmail, setInquiryEmail] = useState('');
const [submittingInquiry, setSubmittingInquiry] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const { isAuthenticated, userInfo } = useAuthStore();
const { settings } = useCompanySettings();
// Check if current time is within business hours from settings
const isBusinessHours = () => {
const now = new Date();
const hour = now.getHours();
const startHour = settings.chat_working_hours_start || 9;
const endHour = settings.chat_working_hours_end || 17;
return hour >= startHour && hour < endHour;
};
const [isWithinBusinessHours, setIsWithinBusinessHours] = useState(isBusinessHours());
// Update business hours check periodically
useEffect(() => {
const checkBusinessHours = () => {
setIsWithinBusinessHours(isBusinessHours());
};
checkBusinessHours();
const interval = setInterval(checkBusinessHours, 60000); // Check every minute
return () => clearInterval(interval);
}, [settings.chat_working_hours_start, settings.chat_working_hours_end]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const validateVisitorForm = (): boolean => {
const errors: Record<string, string> = {};
if (!visitorInfo.name.trim()) {
errors.name = 'Name is required';
}
if (!visitorInfo.email.trim()) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(visitorInfo.email)) {
errors.email = 'Please enter a valid email address';
}
if (!visitorInfo.phone.trim()) {
errors.phone = 'Phone is required';
}
setFormErrors(errors);
return Object.keys(errors).length === 0;
};
const createChat = async () => {
if (!isAuthenticated) {
if (!validateVisitorForm()) {
return;
}
}
try {
setLoading(true);
const response = await chatService.createChat(
isAuthenticated
? undefined
: {
visitor_name: visitorInfo.name,
visitor_email: visitorInfo.email,
visitor_phone: visitorInfo.phone
}
);
setChat(response.data);
setShowVisitorForm(false);
const messagesResponse = await chatService.getMessages(response.data.id);
setMessages(messagesResponse.data);
connectWebSocket(response.data.id);
// Show success message - chat is ready to use
toast.success('Chat started! You can now send messages.');
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to start chat');
} finally {
setLoading(false);
}
};
const connectWebSocket = (chatId: number) => {
if (ws) {
ws.close();
}
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8000';
const normalizedBase = baseUrl.replace(/\/$/, '');
const wsProtocol = normalizedBase.startsWith('https') ? 'wss' : 'ws';
const wsBase = normalizedBase.replace(/^https?/, wsProtocol);
const wsUrl = `${wsBase}/api/chat/ws/${chatId}?user_type=visitor`;
const websocket = new WebSocket(wsUrl);
websocket.onopen = () => {
console.log('WebSocket connected for chat', chatId);
};
websocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'new_message') {
setMessages(prev => {
const exists = prev.find(m => m.id === data.data.id);
if (exists) return prev;
return [...prev, data.data];
});
} else if (data.type === 'chat_accepted') {
if (chat) {
const updatedChat = {
...chat,
status: 'active' as const,
staff_name: data.data.staff_name,
staff_id: data.data.staff_id
};
setChat(updatedChat);
toast.success(`Chat accepted by ${data.data.staff_name}`);
}
} else if (data.type === 'chat_closed') {
toast.info('Chat has been closed');
if (chat) {
setChat({ ...chat, status: 'closed' });
// Optionally reset after a delay to allow user to see the closed state
// The user can click "Start New Chat" button to start fresh
}
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
websocket.onerror = (error) => {
console.error('WebSocket error:', error);
};
websocket.onclose = (event) => {
console.log('WebSocket disconnected', event.code, event.reason);
if (event.code !== 1000 && chat) {
console.log('Attempting to reconnect WebSocket...');
setTimeout(() => {
if (chat) {
connectWebSocket(chat.id);
}
}, 3000);
}
};
setWs(websocket);
};
useEffect(() => {
if (!chat || !isOpen || isMinimized) return;
const pollInterval = setInterval(async () => {
try {
const messagesResponse = await chatService.getMessages(chat.id);
if (messagesResponse.success) {
setMessages(messagesResponse.data);
}
if (chat.status === 'pending') {
try {
const chatResponse = await chatService.getChat(chat.id);
if (chatResponse.success) {
const updatedChat = chatResponse.data;
if (updatedChat.status !== chat.status ||
updatedChat.staff_name !== chat.staff_name ||
updatedChat.staff_id !== chat.staff_id) {
setChat(updatedChat);
if (updatedChat.status === 'active' && updatedChat.staff_name && chat.status === 'pending') {
toast.success(`Chat accepted by ${updatedChat.staff_name}`);
}
}
}
} catch (error) {
console.debug('Could not poll chat status:', error);
}
}
} catch (error) {
console.error('Error polling chat updates:', error);
}
}, 5000);
return () => clearInterval(pollInterval);
}, [chat, isOpen, isMinimized]);
const handleOpen = () => {
setIsOpen(true);
setIsMinimized(false);
// If outside business hours, don't show chat options
if (!isWithinBusinessHours) {
return;
}
// If there's a closed chat, reset chat state but keep visitor info
if (chat && chat.status === 'closed') {
setChat(null);
setMessages([]);
setNewMessage('');
setShowVisitorForm(false);
// Don't auto-start, let user click the button
} else if (isAuthenticated && !chat && !loading) {
createChat();
} else if (!isAuthenticated && !chat) {
// If visitor info exists, show the "Start New Chat" button instead of form
if (visitorInfo.name && visitorInfo.email && visitorInfo.phone) {
setShowVisitorForm(false);
} else {
setShowVisitorForm(true);
}
}
};
const handleSubmitInquiry = async () => {
if (!inquiryEmail.trim() || !inquiry.trim()) {
toast.error('Please provide your email and inquiry');
return;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inquiryEmail)) {
toast.error('Please enter a valid email address');
return;
}
try {
setSubmittingInquiry(true);
const contactData: ContactFormData = {
name: visitorInfo.name || 'Chat Visitor',
email: inquiryEmail,
phone: visitorInfo.phone || '',
subject: 'Chat Inquiry - Outside Business Hours',
message: inquiry
};
await contactService.submitContactForm(contactData);
toast.success('Your inquiry has been sent! We will get back to you as soon as possible.');
setInquiry('');
setInquiryEmail('');
setIsOpen(false);
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to send inquiry. Please try again.');
} finally {
setSubmittingInquiry(false);
}
};
const handleClose = () => {
if (ws) {
ws.close();
setWs(null);
}
setIsOpen(false);
if (onClose) onClose();
};
const handleEndChat = () => {
if (!chat) return;
setShowEndChatDialog(true);
};
const confirmEndChat = async () => {
if (!chat) return;
try {
setShowEndChatDialog(false);
await chatService.closeChat(chat.id);
toast.success('Chat ended');
if (ws) {
ws.close();
setWs(null);
}
// Reset chat state but preserve visitor info for new chat
setChat(null);
setMessages([]);
setNewMessage('');
setShowVisitorForm(false);
// Keep visitor info so they can start a new chat with same info
} catch (error: any) {
toast.error(error.response?.data?.detail || 'Failed to end chat');
}
};
const handleSend = async () => {
if (!newMessage.trim() || !chat) return;
const messageText = newMessage.trim();
setNewMessage('');
const tempMessage: ChatMessage = {
id: Date.now(),
chat_id: chat.id,
sender_type: 'visitor',
sender_name: isAuthenticated ? userInfo?.name : (visitorInfo.name || 'Guest'),
message: messageText,
is_read: false,
created_at: new Date().toISOString()
};
setMessages(prev => [...prev, tempMessage]);
try {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'message',
message: messageText
}));
} else {
await chatService.sendMessage(chat.id, messageText);
}
} catch (error: any) {
toast.error('Failed to send message');
setMessages(prev => prev.filter(m => m.id !== tempMessage.id));
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<>
<div className="fixed bottom-6 right-6 z-50" style={{ willChange: 'transform' }}>
{!isOpen ? (
<button
onClick={handleOpen}
className="bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-slate-900 p-5 rounded-full shadow-2xl shadow-[var(--luxury-gold)]/40 hover:from-[var(--luxury-gold-dark)] hover:to-[var(--luxury-gold)] transition-all duration-300 hover:scale-110 flex items-center justify-center group relative"
aria-label="Open chat"
>
<div className="absolute inset-0 bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] rounded-full blur-xl opacity-50 group-hover:opacity-70 transition-opacity"></div>
<MessageCircle className="w-6 h-6 group-hover:scale-110 transition-transform relative z-10" strokeWidth={2.5} />
{chat && chat.status === 'pending' && (
<span className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full animate-pulse border-2 border-white shadow-lg z-10"></span>
)}
</button>
) : (
<div
className={`luxury-glass rounded-2xl shadow-2xl border border-[var(--luxury-gold)]/30 flex flex-col transition-all duration-300 ${
isMinimized ? 'w-80 h-16' : 'w-96 h-[600px]'
}`}
style={{ transformOrigin: 'bottom right' }}
>
{}
<div className="bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-slate-900 p-4 rounded-t-2xl flex items-center justify-between relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-[var(--luxury-gold)]/20 to-[var(--luxury-gold-dark)]/20"></div>
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-transparent via-slate-900/20 to-transparent"></div>
<div className="flex items-center gap-3 relative z-10">
<div className="bg-slate-900/10 p-2 rounded-lg backdrop-blur-sm">
<MessageCircle className="w-5 h-5 text-slate-900" strokeWidth={2.5} />
</div>
<div>
<h3 className="font-serif font-semibold text-slate-900 tracking-wide">
{!isWithinBusinessHours
? 'Leave Your Inquiry'
: chat?.status === 'active' && chat?.staff_name
? `Chat with ${chat.staff_name}`
: chat?.status === 'closed'
? 'Chat Ended'
: 'Support Chat'}
</h3>
{!isWithinBusinessHours ? (
<p className="text-xs text-slate-700/80 font-light flex items-center gap-1">
<Clock className="w-3 h-3" />
Chat available {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined
? formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)
: '9:00 AM - 5:00 PM'}
</p>
) : chat?.status === 'pending' ? (
<p className="text-xs text-slate-700/80 font-light">Waiting for staff...</p>
) : chat?.status === 'active' ? (
<p className="text-xs text-slate-700/80 font-light">Online</p>
) : chat?.status === 'closed' ? (
<p className="text-xs text-slate-700/80 font-light">This chat has ended</p>
) : null}
</div>
</div>
<div className="flex items-center gap-2 relative z-10">
{chat && chat.status !== 'closed' && (
<button
onClick={handleEndChat}
className="px-3 py-1.5 text-xs bg-red-500/20 hover:bg-red-500/30 rounded-lg transition-all duration-200 border border-red-400/30 text-red-700 font-semibold hover:shadow-md"
title="End chat"
>
End Chat
</button>
)}
<button
onClick={() => setIsMinimized(!isMinimized)}
className="p-2 hover:bg-slate-900/10 rounded-lg transition-all duration-200 backdrop-blur-sm"
aria-label={isMinimized ? 'Maximize' : 'Minimize'}
>
{isMinimized ? (
<Maximize2 className="w-4 h-4 text-slate-900" />
) : (
<Minimize2 className="w-4 h-4 text-slate-900" />
)}
</button>
<button
onClick={handleClose}
className="p-2 hover:bg-slate-900/10 rounded-lg transition-all duration-200 backdrop-blur-sm"
aria-label="Close chat widget"
>
<X className="w-4 h-4 text-slate-900" />
</button>
</div>
</div>
{!isMinimized && (
<>
{}
{!isWithinBusinessHours ? (
<div className="flex-1 overflow-y-auto p-4 bg-gradient-to-b from-slate-50/50 to-white">
<div className="max-w-md mx-auto">
<div className="mb-6 p-4 bg-gradient-to-r from-[var(--luxury-gold)]/10 to-[var(--luxury-gold-dark)]/10 rounded-xl border border-[var(--luxury-gold)]/20">
<div className="flex items-start gap-3">
<Clock className="w-5 h-5 text-[var(--luxury-gold)] mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-semibold text-slate-900 mb-1">Chat Hours</p>
<p className="text-xs text-slate-600 font-light">
Our chat support is available Monday to Friday, {settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined
? formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)
: '9:00 AM - 5:00 PM'}.
Please leave your inquiry below and we'll get back to you as soon as possible.
</p>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Your Email <span className="text-red-500">*</span>
</label>
<input
type="email"
value={inquiryEmail}
onChange={(e) => setInquiryEmail(e.target.value)}
className="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/30 focus:border-[var(--luxury-gold)] transition-all duration-200 font-light"
placeholder="your.email@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Your Inquiry <span className="text-red-500">*</span>
</label>
<textarea
value={inquiry}
onChange={(e) => setInquiry(e.target.value)}
rows={6}
className="w-full px-4 py-2.5 border-2 border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/30 focus:border-[var(--luxury-gold)] transition-all duration-200 font-light resize-none"
placeholder="Please describe your inquiry or question..."
/>
</div>
<button
onClick={handleSubmitInquiry}
disabled={submittingInquiry || !inquiryEmail.trim() || !inquiry.trim()}
className="w-full bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-slate-900 px-4 py-3 rounded-xl hover:from-[var(--luxury-gold-dark)] hover:to-[var(--luxury-gold)] disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-all duration-300 font-semibold shadow-lg shadow-[var(--luxury-gold)]/30 hover:shadow-xl disabled:shadow-none"
>
{submittingInquiry ? 'Sending...' : 'Send Inquiry'}
</button>
<p className="text-xs text-slate-500 text-center font-light">
We'll respond to your inquiry as soon as possible.
</p>
</div>
</div>
</div>
) : showVisitorForm && !chat && (
<div className="flex-1 overflow-y-auto p-4 bg-gradient-to-b from-slate-50/50 to-white">
<div className="max-w-md mx-auto">
<h3 className="text-lg font-serif font-semibold text-slate-900 mb-4 tracking-wide">
Please provide your information to start chatting
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Name <span className="text-red-500">*</span>
</label>
<input
type="text"
value={visitorInfo.name}
onChange={(e) => {
setVisitorInfo({ ...visitorInfo, name: e.target.value });
if (formErrors.name) setFormErrors({ ...formErrors, name: '' });
}}
className={`w-full px-4 py-2.5 border-2 rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/30 transition-all duration-200 font-light ${
formErrors.name ? 'border-red-500 focus:border-red-500' : 'border-slate-200 focus:border-[var(--luxury-gold)]'
}`}
placeholder="Your full name"
/>
{formErrors.name && (
<p className="text-xs text-red-500 mt-1">{formErrors.name}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Email <span className="text-red-500">*</span>
</label>
<input
type="email"
value={visitorInfo.email}
onChange={(e) => {
setVisitorInfo({ ...visitorInfo, email: e.target.value });
if (formErrors.email) setFormErrors({ ...formErrors, email: '' });
}}
className={`w-full px-4 py-2.5 border-2 rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/30 transition-all duration-200 font-light ${
formErrors.email ? 'border-red-500 focus:border-red-500' : 'border-slate-200 focus:border-[var(--luxury-gold)]'
}`}
placeholder="your.email@example.com"
/>
{formErrors.email && (
<p className="text-xs text-red-500 mt-1">{formErrors.email}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">
Phone <span className="text-red-500">*</span>
</label>
<input
type="tel"
value={visitorInfo.phone}
onChange={(e) => {
setVisitorInfo({ ...visitorInfo, phone: e.target.value });
if (formErrors.phone) setFormErrors({ ...formErrors, phone: '' });
}}
className={`w-full px-4 py-2.5 border-2 rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/30 transition-all duration-200 font-light ${
formErrors.phone ? 'border-red-500 focus:border-red-500' : 'border-slate-200 focus:border-[var(--luxury-gold)]'
}`}
placeholder="+1 (555) 123-4567"
/>
{formErrors.phone && (
<p className="text-xs text-red-500 mt-1">{formErrors.phone}</p>
)}
</div>
<button
onClick={createChat}
disabled={loading}
className="w-full bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-slate-900 px-4 py-3 rounded-xl hover:from-[var(--luxury-gold-dark)] hover:to-[var(--luxury-gold)] disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-all duration-300 font-semibold shadow-lg shadow-[var(--luxury-gold)]/30 hover:shadow-xl disabled:shadow-none"
>
{loading ? 'Starting chat...' : 'Start Chat'}
</button>
</div>
</div>
</div>
)}
{}
{!showVisitorForm && isWithinBusinessHours && (
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-slate-50/50 to-white">
{/* Working Hours Info Banner */}
{settings.chat_working_hours_start !== undefined && settings.chat_working_hours_end !== undefined && (
<div className="mb-4 p-3 bg-gradient-to-r from-[var(--luxury-gold)]/5 to-[var(--luxury-gold-dark)]/5 rounded-lg border border-[var(--luxury-gold)]/20">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-[var(--luxury-gold)] flex-shrink-0" />
<p className="text-xs text-slate-600 font-light">
<span className="font-medium text-slate-700">Chat Support Hours:</span>{' '}
{formatWorkingHours(settings.chat_working_hours_start, settings.chat_working_hours_end)}
</p>
</div>
</div>
)}
{loading && !chat ? (
<div className="text-center text-slate-500 py-8 font-light">
Starting chat...
</div>
) : (chat && chat.status === 'closed') || (!chat && !isAuthenticated && visitorInfo.name && visitorInfo.email && visitorInfo.phone) ? (
<div className="flex flex-col items-center justify-center py-12 px-4">
<div className="text-center mb-6">
<p className="text-sm text-slate-600 mb-2 font-light">
{chat && chat.status === 'closed' ? 'This chat has ended.' : 'Ready to start a new chat.'}
</p>
{!isAuthenticated && visitorInfo.name && visitorInfo.email && visitorInfo.phone && (
<p className="text-xs text-slate-500 mb-4 font-light">
Start a new chat with {visitorInfo.name}
</p>
)}
</div>
<button
onClick={() => {
setChat(null);
setMessages([]);
setNewMessage('');
// If visitor info exists, start chat directly, otherwise show form
if (!isAuthenticated && visitorInfo.name && visitorInfo.email && visitorInfo.phone) {
setShowVisitorForm(false);
createChat();
} else if (!isAuthenticated) {
setShowVisitorForm(true);
} else {
createChat();
}
}}
disabled={loading}
className="bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-slate-900 px-8 py-3 rounded-xl hover:from-[var(--luxury-gold-dark)] hover:to-[var(--luxury-gold)] transition-all duration-300 font-semibold shadow-lg shadow-[var(--luxury-gold)]/30 hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Starting...' : 'Start New Chat'}
</button>
</div>
) : messages.length === 0 ? (
<div className="text-center text-slate-500 py-8 font-light">
No messages yet. Start the conversation!
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.sender_type === 'visitor' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[80%] rounded-xl p-4 shadow-lg ${
message.sender_type === 'visitor'
? 'bg-gradient-to-br from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-slate-900 border border-[var(--luxury-gold)]/30'
: 'bg-white text-slate-800 border border-slate-200/60 shadow-sm'
}`}
>
{message.sender_type === 'staff' && (
<div className="text-xs font-semibold mb-1.5 text-[var(--luxury-gold)] tracking-wide">
{message.sender_name || 'Staff'}
</div>
)}
<p className="text-sm whitespace-pre-wrap break-words leading-relaxed">
{message.message}
</p>
<p
className={`text-xs mt-2 ${
message.sender_type === 'visitor'
? 'text-slate-700/70'
: 'text-slate-500'
}`}
>
{new Date(message.created_at).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
})}
</p>
</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
)}
{}
{!showVisitorForm && chat && chat.status !== 'closed' && (
<div className="p-4 border-t border-[var(--luxury-gold)]/20 bg-white/90 backdrop-blur-sm rounded-b-2xl">
<div className="flex gap-3">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Type your message..."
className="flex-1 px-4 py-3 border-2 border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--luxury-gold)]/30 focus:border-[var(--luxury-gold)] transition-all duration-200 font-light"
/>
<button
onClick={handleSend}
disabled={!newMessage.trim()}
className="bg-gradient-to-r from-[var(--luxury-gold)] to-[var(--luxury-gold-dark)] text-slate-900 px-5 py-3 rounded-xl hover:from-[var(--luxury-gold-dark)] hover:to-[var(--luxury-gold)] disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-all duration-300 flex items-center gap-2 font-semibold shadow-lg shadow-[var(--luxury-gold)]/30 hover:shadow-xl disabled:shadow-none"
>
<Send className="w-4 h-4" />
</button>
</div>
{chat?.status === 'pending' && (
<p className="text-xs text-slate-500 mt-2 font-light">
Your message will be delivered. A staff member will respond soon.
</p>
)}
</div>
)}
</>
)}
</div>
)}
</div>
<ConfirmationDialog
isOpen={showEndChatDialog}
onClose={() => setShowEndChatDialog(false)}
onConfirm={confirmEndChat}
title="End Chat"
message="Are you sure you want to end this chat? You can start a new chat anytime."
confirmText="End Chat"
cancelText="Cancel"
variant="warning"
/>
</>
);
};
export default ChatWidget;