update
This commit is contained in:
719
Frontend/src/features/notifications/components/ChatWidget.tsx
Normal file
719
Frontend/src/features/notifications/components/ChatWidget.tsx
Normal file
@@ -0,0 +1,719 @@
|
||||
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';
|
||||
|
||||
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-[#d4af37] to-[#c9a227] text-slate-900 p-5 rounded-full shadow-2xl shadow-[#d4af37]/40 hover:from-[#c9a227] hover:to-[#d4af37] 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-[#d4af37] to-[#c9a227] 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-[#d4af37]/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-[#d4af37] to-[#c9a227] 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-[#d4af37]/20 to-[#c9a227]/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 9 AM - 5 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-[#d4af37]/10 to-[#c9a227]/10 rounded-xl border border-[#d4af37]/20">
|
||||
<div className="flex items-start gap-3">
|
||||
<Clock className="w-5 h-5 text-[#d4af37] 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 || 9}:00 AM - {settings.chat_working_hours_end || 17}: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-[#d4af37]/30 focus:border-[#d4af37] 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-[#d4af37]/30 focus:border-[#d4af37] 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-[#d4af37] to-[#c9a227] text-slate-900 px-4 py-3 rounded-xl hover:from-[#c9a227] hover:to-[#d4af37] disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-all duration-300 font-semibold shadow-lg shadow-[#d4af37]/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-[#d4af37]/30 transition-all duration-200 font-light ${
|
||||
formErrors.name ? 'border-red-500 focus:border-red-500' : 'border-slate-200 focus:border-[#d4af37]'
|
||||
}`}
|
||||
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-[#d4af37]/30 transition-all duration-200 font-light ${
|
||||
formErrors.email ? 'border-red-500 focus:border-red-500' : 'border-slate-200 focus:border-[#d4af37]'
|
||||
}`}
|
||||
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-[#d4af37]/30 transition-all duration-200 font-light ${
|
||||
formErrors.phone ? 'border-red-500 focus:border-red-500' : 'border-slate-200 focus:border-[#d4af37]'
|
||||
}`}
|
||||
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-[#d4af37] to-[#c9a227] text-slate-900 px-4 py-3 rounded-xl hover:from-[#c9a227] hover:to-[#d4af37] disabled:bg-gray-300 disabled:text-gray-500 disabled:cursor-not-allowed transition-all duration-300 font-semibold shadow-lg shadow-[#d4af37]/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">
|
||||
{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-[#d4af37] to-[#c9a227] text-slate-900 px-8 py-3 rounded-xl hover:from-[#c9a227] hover:to-[#d4af37] transition-all duration-300 font-semibold shadow-lg shadow-[#d4af37]/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-[#d4af37] to-[#c9a227] text-slate-900 border border-[#d4af37]/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-[#d4af37] 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-[#d4af37]/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-[#d4af37]/30 focus:border-[#d4af37] transition-all duration-200 font-light"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!newMessage.trim()}
|
||||
className="bg-gradient-to-r from-[#d4af37] to-[#c9a227] text-slate-900 px-5 py-3 rounded-xl hover:from-[#c9a227] hover:to-[#d4af37] 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-[#d4af37]/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;
|
||||
|
||||
Reference in New Issue
Block a user