diff --git a/client/src/pages/ChatPage.tsx b/client/src/pages/ChatPage.tsx index ed360cd..15c8ecf 100644 --- a/client/src/pages/ChatPage.tsx +++ b/client/src/pages/ChatPage.tsx @@ -1,449 +1,531 @@ import { useState, useEffect, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; import { useAuth } from '@/hooks/useAuth'; -import { Card } from '@/components/ui/card'; +import { Card, CardContent, CardHeader } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { Separator } from '@/components/ui/separator'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; import { PageHeader } from '@/components/ui/page-header'; +import { useToast } from '@/hooks/use-toast'; +import Sidebar from '@/components/layout/Sidebar'; import { MessageCircle, Search, - Phone, - Video, - MoreVertical, Send, - Paperclip, - Mic, - Users, - Settings, UserPlus, - Briefcase + Settings, + Plus, + Users, + Clock } from 'lucide-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { apiRequest } from '@/lib/queryClient'; - -interface Contact { - id: number; - name: string; - avatar?: string; - lastMessage?: string; - lastMessageTime?: string; - unreadCount?: number; - isOnline?: boolean; - isProfessional?: boolean; - professionalType?: string; -} - -interface Message { - id: number; - content: string; - senderId: number; - timestamp: string; - type: 'text' | 'image' | 'document' | 'voice'; - isEdited?: boolean; - status: 'sent' | 'delivered' | 'read'; -} - -interface Chat { - id: number; - name?: string; - type: 'direct' | 'group' | 'professional_service'; - participants: any[]; - messages: Message[]; -} export default function ChatPage() { - const { t } = useTranslation(); const { user } = useAuth(); + const { toast } = useToast(); const queryClient = useQueryClient(); const [selectedChat, setSelectedChat] = useState(null); const [newMessage, setNewMessage] = useState(''); const [searchQuery, setSearchQuery] = useState(''); + const [contactOpen, setContactOpen] = useState(false); + const [chatOpen, setChatOpen] = useState(false); const messagesEndRef = useRef(null); - const [ws, setWs] = useState(null); - // Fetch contacts/chats - const { data: chats = [], isLoading } = useQuery({ + const [newContact, setNewContact] = useState({ + name: '', + email: '', + phone: '', + notes: '' + }); + + const [newChat, setNewChat] = useState({ + name: '', + type: 'group' as 'direct' | 'group', + description: '' + }); + + // Fetch chats + const { data: chats, isLoading } = useQuery({ queryKey: ['/api/chats'], - enabled: !!user, + queryFn: async () => { + const response = await fetch('/api/chats'); + if (!response.ok) throw new Error('Failed to fetch chats'); + return response.json(); + }, + enabled: !!user + }); + + // Fetch contacts + const { data: contacts } = useQuery({ + queryKey: ['/api/contacts'], + queryFn: async () => { + const response = await fetch('/api/contacts'); + if (!response.ok) throw new Error('Failed to fetch contacts'); + return response.json(); + }, + enabled: !!user }); // Fetch messages for selected chat - const { data: messages = [] } = useQuery({ + const { data: messages } = useQuery({ queryKey: ['/api/chats', selectedChat, 'messages'], - enabled: !!selectedChat, + queryFn: async () => { + const response = await fetch(`/api/chats/${selectedChat}/messages`); + if (!response.ok) throw new Error('Failed to fetch messages'); + return response.json(); + }, + enabled: !!selectedChat }); - // Fetch professional services - const { data: professionalServices = [] } = useQuery({ - queryKey: ['/api/professional-services'], + // Create contact mutation + const createContactMutation = useMutation({ + mutationFn: async (contactData: typeof newContact) => { + const response = await fetch('/api/contacts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(contactData) + }); + if (!response.ok) throw new Error('Failed to create contact'); + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/contacts'] }); + setContactOpen(false); + setNewContact({ name: '', email: '', phone: '', notes: '' }); + toast({ title: "Contact added successfully!" }); + }, + onError: () => { + toast({ title: "Failed to add contact", variant: "destructive" }); + } + }); + + // Create chat mutation + const createChatMutation = useMutation({ + mutationFn: async (chatData: typeof newChat) => { + const response = await fetch('/api/chats', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(chatData) + }); + if (!response.ok) throw new Error('Failed to create chat'); + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/chats'] }); + setChatOpen(false); + setNewChat({ name: '', type: 'group', description: '' }); + toast({ title: "Chat created successfully!" }); + }, + onError: () => { + toast({ title: "Failed to create chat", variant: "destructive" }); + } }); // Send message mutation const sendMessageMutation = useMutation({ - mutationFn: async (messageData: { chatId: number; content: string; type: string }) => { - return apiRequest(`/api/chats/${messageData.chatId}/messages`, { + mutationFn: async (messageData: { content: string; type: string }) => { + const response = await fetch(`/api/chats/${selectedChat}/messages`, { method: 'POST', - body: JSON.stringify(messageData), + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(messageData) }); + if (!response.ok) throw new Error('Failed to send message'); + return response.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/chats', selectedChat, 'messages'] }); setNewMessage(''); }, + onError: () => { + toast({ title: "Failed to send message", variant: "destructive" }); + } }); + const handleCreateContact = (e: React.FormEvent) => { + e.preventDefault(); + if (!newContact.name) { + toast({ title: "Please enter a contact name", variant: "destructive" }); + return; + } + createContactMutation.mutate(newContact); + }; + + const handleCreateChat = (e: React.FormEvent) => { + e.preventDefault(); + if (!newChat.name) { + toast({ title: "Please enter a chat name", variant: "destructive" }); + return; + } + createChatMutation.mutate(newChat); + }; + + const handleSendMessage = (e: React.FormEvent) => { + e.preventDefault(); + if (!newMessage.trim() || !selectedChat) return; + + sendMessageMutation.mutate({ + content: newMessage, + type: 'text' + }); + }; + // WebSocket connection for real-time messaging useEffect(() => { if (!user) return; - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/ws`; - const websocket = new WebSocket(wsUrl); + const wsUrl = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const ws = new WebSocket(`${wsUrl}//${window.location.host}/ws`); - websocket.onopen = () => { + ws.onopen = () => { console.log('Connected to chat WebSocket'); - websocket.send(JSON.stringify({ type: 'join', userId: user.id })); }; - websocket.onmessage = (event) => { + ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'new_message') { + queryClient.invalidateQueries({ queryKey: ['/api/chats', data.chatId, 'messages'] }); queryClient.invalidateQueries({ queryKey: ['/api/chats'] }); - if (data.chatId === selectedChat) { - queryClient.invalidateQueries({ queryKey: ['/api/chats', selectedChat, 'messages'] }); - } } }; - websocket.onclose = () => { + ws.onclose = () => { console.log('Chat WebSocket disconnected'); }; - setWs(websocket); - return () => { - websocket.close(); + ws.close(); }; - }, [user, selectedChat, queryClient]); + }, [user, queryClient]); - // Auto-scroll to bottom when new messages arrive + // Auto-scroll to bottom of messages useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); - const handleSendMessage = () => { - if (!newMessage.trim() || !selectedChat) return; - - sendMessageMutation.mutate({ - chatId: selectedChat, - content: newMessage.trim(), - type: 'text' - }); - }; - - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSendMessage(); - } - }; - - const filteredChats = chats.filter((chat: any) => - chat.name?.toLowerCase().includes(searchQuery.toLowerCase()) || - chat.participants?.some((p: any) => - p.name?.toLowerCase().includes(searchQuery.toLowerCase()) - ) - ); - - const formatTime = (timestamp: string) => { - return new Date(timestamp).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit' - }); - }; - - const formatDate = (timestamp: string) => { - const date = new Date(timestamp); - const today = new Date(); - const yesterday = new Date(today); - yesterday.setDate(yesterday.getDate() - 1); - - if (date.toDateString() === today.toDateString()) { - return 'Today'; - } else if (date.toDateString() === yesterday.toDateString()) { - return 'Yesterday'; - } else { - return date.toLocaleDateString(); - } - }; + const filteredChats = chats?.filter((chat: any) => + chat.name?.toLowerCase().includes(searchQuery.toLowerCase()) + ) || []; if (isLoading) { return ( -
-
-
{t('common.loading')}
+
+ +
+
+
+
+
+
+
+
+
); } return ( -
- {/* Sidebar - Chat List */} -
- {/* Header */} -
-
-

- - {t('chat.title')} -

+
+ +
+
+
- - -
-
- - {/* Search */} -
- - setSearchQuery(e.target.value)} - className="pl-10" - /> -
-
- - {/* Professional Services Section */} -
-
- - {t('chat.professionalServices')} -
- -
- {professionalServices.slice(0, 4).map((service: any) => ( - -
- - - {service.name?.[0]} - -
{service.name}
- - {service.type} - -
-
- ))} -
-
-
- - {/* Chat List */} - -
- {filteredChats.map((chat: any) => ( -
setSelectedChat(chat.id)} - className={`p-3 rounded-lg cursor-pointer mb-1 transition-colors ${ - selectedChat === chat.id - ? 'bg-primary/10 border border-primary/20' - : 'hover:bg-accent' - }`} - > -
-
- - - - {chat.name?.[0] || chat.participants?.[0]?.name?.[0]} - - - {chat.isOnline && ( -
- )} -
- -
-
-

- {chat.name || chat.participants?.[0]?.name} - {chat.isProfessional && ( - - {chat.professionalType} - - )} -

- - {formatTime(chat.lastMessageAt)} - + + + + + + + Add New Contact + + Add a new contact to your network. + + +
+
+ + setNewContact({ ...newContact, name: e.target.value })} + required + />
- -
-

- {chat.lastMessage || t('chat.noMessages')} -

- {chat.unreadCount > 0 && ( - - {chat.unreadCount} - - )} +
+ + setNewContact({ ...newContact, email: e.target.value })} + />
-
-
-
- ))} -
- -
+
+ + setNewContact({ ...newContact, phone: e.target.value })} + /> +
+
+ +