400 lines
16 KiB
TypeScript
400 lines
16 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { useChat } from '@/hooks/useChat';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { toast } from 'sonner';
|
|
import {
|
|
MessageCircle,
|
|
Car,
|
|
UtensilsCrossed,
|
|
Hotel as HotelIcon,
|
|
Shield,
|
|
MapPin,
|
|
Send,
|
|
Search,
|
|
Plus,
|
|
Phone,
|
|
Video,
|
|
Paperclip,
|
|
Smile,
|
|
Circle,
|
|
HeadphonesIcon
|
|
} from 'lucide-react';
|
|
|
|
type ServiceType = 'all' | 'taxi' | 'restaurant' | 'hotel' | 'guide' | 'politur' | 'support';
|
|
|
|
const serviceIcons = {
|
|
taxi: Car,
|
|
restaurant: UtensilsCrossed,
|
|
hotel: HotelIcon,
|
|
guide: MapPin,
|
|
politur: Shield,
|
|
support: HeadphonesIcon
|
|
};
|
|
|
|
const serviceColors = {
|
|
taxi: 'bg-yellow-500/10 text-yellow-700 border-yellow-200',
|
|
restaurant: 'bg-orange-500/10 text-orange-700 border-orange-200',
|
|
hotel: 'bg-blue-500/10 text-blue-700 border-blue-200',
|
|
guide: 'bg-green-500/10 text-green-700 border-green-200',
|
|
politur: 'bg-red-500/10 text-red-700 border-red-200',
|
|
support: 'bg-purple-500/10 text-purple-700 border-purple-200'
|
|
};
|
|
|
|
const Messages = () => {
|
|
const { user } = useAuth();
|
|
const [selectedChatId, setSelectedChatId] = useState<string | null>(null);
|
|
const [messageText, setMessageText] = useState('');
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [serviceFilter, setServiceFilter] = useState<ServiceType>('all');
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
const {
|
|
chats,
|
|
messages,
|
|
onlineUsers,
|
|
loading,
|
|
error,
|
|
loadMessages,
|
|
sendMessage,
|
|
searchUsers,
|
|
createChat,
|
|
getChatById,
|
|
clearError
|
|
} = useChat();
|
|
|
|
const selectedChat = selectedChatId ? getChatById(selectedChatId) : null;
|
|
|
|
// Filter chats by service type
|
|
const filteredChats = chats.filter(chat => {
|
|
const matchesService = serviceFilter === 'all' || chat.serviceType === serviceFilter;
|
|
const matchesSearch = !searchQuery ||
|
|
chat.name.toLowerCase().includes(searchQuery.toLowerCase());
|
|
return matchesService && matchesSearch;
|
|
});
|
|
|
|
// Auto-scroll to bottom of messages
|
|
const scrollToBottom = () => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
};
|
|
|
|
useEffect(() => {
|
|
scrollToBottom();
|
|
}, [messages]);
|
|
|
|
// Load messages when chat is selected
|
|
useEffect(() => {
|
|
if (selectedChatId) {
|
|
loadMessages(selectedChatId);
|
|
}
|
|
}, [selectedChatId, loadMessages]);
|
|
|
|
// Auto-select first chat
|
|
useEffect(() => {
|
|
if (chats.length > 0 && !selectedChatId) {
|
|
setSelectedChatId(chats[0].id);
|
|
}
|
|
}, [chats, selectedChatId]);
|
|
|
|
// Handle send message
|
|
const handleSendMessage = async () => {
|
|
if (!messageText.trim() || !selectedChatId) return;
|
|
|
|
try {
|
|
await sendMessage(selectedChatId, messageText);
|
|
setMessageText('');
|
|
} catch (err) {
|
|
toast.error('No se pudo enviar el mensaje');
|
|
}
|
|
};
|
|
|
|
// Format time
|
|
const formatTime = (timestamp: string) => {
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
|
|
|
if (diffInHours < 24) {
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
} else if (diffInHours < 168) {
|
|
return date.toLocaleDateString([], { weekday: 'short' });
|
|
} else {
|
|
return date.toLocaleDateString();
|
|
}
|
|
};
|
|
|
|
const getServiceIcon = (serviceType?: string) => {
|
|
if (!serviceType) return MessageCircle;
|
|
return serviceIcons[serviceType as keyof typeof serviceIcons] || MessageCircle;
|
|
};
|
|
|
|
const getServiceBadgeClass = (serviceType?: string) => {
|
|
if (!serviceType) return 'bg-muted';
|
|
return serviceColors[serviceType as keyof typeof serviceColors] || 'bg-muted';
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-screen bg-background overflow-hidden">
|
|
<div className="chat-container m-0 overflow-hidden relative rounded-lg flex w-full">
|
|
{/* Chat List Sidebar */}
|
|
<div className="chat-list__sidebar p-0 w-80 bg-background border-r border-border flex flex-col">
|
|
{/* Header */}
|
|
<div className="p-4 border-b border-border">
|
|
<h2 className="text-xl font-bold mb-2">Mensajes</h2>
|
|
<p className="text-sm text-muted-foreground">Comunícate con servicios turísticos</p>
|
|
</div>
|
|
|
|
{/* Search */}
|
|
<div className="p-4 border-b border-border">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
type="search"
|
|
className="pl-9"
|
|
placeholder="Buscar conversaciones..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Service Filter Tabs */}
|
|
<div className="px-4 py-3 border-b border-border">
|
|
<Tabs value={serviceFilter} onValueChange={(v) => setServiceFilter(v as ServiceType)}>
|
|
<TabsList className="w-full grid grid-cols-4 h-auto gap-1">
|
|
<TabsTrigger value="all" className="text-xs py-1">
|
|
Todos
|
|
</TabsTrigger>
|
|
<TabsTrigger value="taxi" className="text-xs py-1">
|
|
<Car className="h-3 w-3" />
|
|
</TabsTrigger>
|
|
<TabsTrigger value="restaurant" className="text-xs py-1">
|
|
<UtensilsCrossed className="h-3 w-3" />
|
|
</TabsTrigger>
|
|
<TabsTrigger value="hotel" className="text-xs py-1">
|
|
<HotelIcon className="h-3 w-3" />
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
<TabsList className="w-full grid grid-cols-3 h-auto gap-1 mt-1">
|
|
<TabsTrigger value="guide" className="text-xs py-1">
|
|
<MapPin className="h-3 w-3" />
|
|
</TabsTrigger>
|
|
<TabsTrigger value="politur" className="text-xs py-1">
|
|
<Shield className="h-3 w-3" />
|
|
</TabsTrigger>
|
|
<TabsTrigger value="support" className="text-xs py-1">
|
|
<HeadphonesIcon className="h-3 w-3" />
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</Tabs>
|
|
</div>
|
|
|
|
{/* Chat List */}
|
|
<ScrollArea className="flex-1">
|
|
<div className="p-2 space-y-1">
|
|
{loading && (
|
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
|
Cargando conversaciones...
|
|
</div>
|
|
)}
|
|
{!loading && filteredChats.length === 0 && (
|
|
<div className="p-4 text-center text-sm text-muted-foreground">
|
|
No hay conversaciones
|
|
</div>
|
|
)}
|
|
{filteredChats.map((chat) => {
|
|
const ServiceIcon = getServiceIcon(chat.serviceType);
|
|
return (
|
|
<div
|
|
key={chat.id}
|
|
className={`flex items-start p-3 rounded-lg cursor-pointer transition-colors ${
|
|
selectedChatId === chat.id
|
|
? 'bg-primary/10 border border-primary/20'
|
|
: 'hover:bg-accent'
|
|
}`}
|
|
onClick={() => setSelectedChatId(chat.id)}
|
|
>
|
|
<div className="relative mr-3">
|
|
<Avatar className="h-12 w-12">
|
|
<AvatarImage src={chat.avatar} />
|
|
<AvatarFallback>
|
|
<ServiceIcon className="h-6 w-6" />
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
{chat.online && (
|
|
<div className="absolute bottom-0 right-0 h-3 w-3 bg-green-500 rounded-full border-2 border-background" />
|
|
)}
|
|
{chat.unreadCount > 0 && (
|
|
<div className="absolute -top-1 -left-1 h-5 w-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
|
|
{chat.unreadCount}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<h5 className="font-semibold text-sm truncate">{chat.name}</h5>
|
|
<span className="text-xs text-muted-foreground">
|
|
{chat.lastMessage ? formatTime(chat.lastMessage.timestamp) : ''}
|
|
</span>
|
|
</div>
|
|
{chat.serviceType && (
|
|
<Badge variant="outline" className={`mb-1 text-xs ${getServiceBadgeClass(chat.serviceType)}`}>
|
|
<ServiceIcon className="h-3 w-3 mr-1" />
|
|
{chat.serviceType.charAt(0).toUpperCase() + chat.serviceType.slice(1)}
|
|
</Badge>
|
|
)}
|
|
<p className="text-xs text-muted-foreground truncate">
|
|
{chat.lastMessage?.content || 'No hay mensajes'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
|
|
{/* Chat Area */}
|
|
<div className="flex-1 flex flex-col">
|
|
{selectedChat ? (
|
|
<>
|
|
{/* Chat Header */}
|
|
<div className="p-4 border-b border-border flex items-center justify-between bg-card">
|
|
<div className="flex items-center">
|
|
<Avatar className="h-10 w-10 mr-3">
|
|
<AvatarImage src={selectedChat.avatar} />
|
|
<AvatarFallback>
|
|
{React.createElement(getServiceIcon(selectedChat.serviceType), { className: 'h-5 w-5' })}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<h2 className="font-semibold flex items-center gap-2">
|
|
{selectedChat.name}
|
|
{selectedChat.serviceType && (
|
|
<Badge variant="outline" className={`text-xs ${getServiceBadgeClass(selectedChat.serviceType)}`}>
|
|
{selectedChat.serviceType}
|
|
</Badge>
|
|
)}
|
|
</h2>
|
|
<div className="flex items-center text-sm text-muted-foreground">
|
|
<Circle className={`h-2 w-2 mr-1 ${selectedChat.online ? 'fill-green-500 text-green-500' : 'fill-gray-400 text-gray-400'}`} />
|
|
{selectedChat.online ? 'En línea' : 'Desconectado'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Button size="sm" variant="outline" title="Llamar">
|
|
<Phone className="h-4 w-4" />
|
|
</Button>
|
|
<Button size="sm" variant="outline" title="Video llamada">
|
|
<Video className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Messages */}
|
|
<ScrollArea className="flex-1 p-4">
|
|
<div className="space-y-4">
|
|
{messages.length === 0 ? (
|
|
<div className="flex flex-col items-center justify-center h-full text-center p-8">
|
|
<div className="w-24 h-24 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
|
<MessageCircle className="h-12 w-12 text-primary" />
|
|
</div>
|
|
<h3 className="text-lg font-medium mb-2">Inicia la conversación</h3>
|
|
<p className="text-muted-foreground text-sm">
|
|
Envía el primer mensaje a {selectedChat.name}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
messages.map((message) => (
|
|
<div
|
|
key={message.id}
|
|
className={`flex items-start ${message.isOwn ? 'justify-end' : 'justify-start'}`}
|
|
>
|
|
{!message.isOwn && (
|
|
<Avatar className="h-8 w-8 mr-2">
|
|
<AvatarImage src={message.senderAvatar} />
|
|
<AvatarFallback>{message.senderName?.charAt(0)}</AvatarFallback>
|
|
</Avatar>
|
|
)}
|
|
<div className={`flex flex-col ${message.isOwn ? 'items-end' : 'items-start'} max-w-md`}>
|
|
{!message.isOwn && (
|
|
<span className="text-xs text-muted-foreground mb-1">{message.senderName}</span>
|
|
)}
|
|
<div
|
|
className={`px-4 py-2 rounded-lg ${
|
|
message.isOwn
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-muted'
|
|
}`}
|
|
>
|
|
<p className="text-sm">{message.content}</p>
|
|
</div>
|
|
<span className="text-xs text-muted-foreground mt-1">{message.timestamp}</span>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{/* Message Input */}
|
|
<div className="p-4 border-t border-border bg-card">
|
|
<div className="flex items-center space-x-2">
|
|
<Button size="sm" variant="outline" title="Adjuntar archivo">
|
|
<Paperclip className="h-4 w-4" />
|
|
</Button>
|
|
<div className="flex-1">
|
|
<Input
|
|
placeholder="Escribe un mensaje..."
|
|
value={messageText}
|
|
onChange={(e) => setMessageText(e.target.value)}
|
|
onKeyPress={(e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
handleSendMessage();
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
<Button size="sm" variant="outline" title="Emoji">
|
|
<Smile className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={handleSendMessage}
|
|
disabled={!messageText.trim() || loading}
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className="flex-1 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="w-32 h-32 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<MessageCircle className="h-16 w-16 text-primary" />
|
|
</div>
|
|
<h3 className="text-lg font-medium mb-2">Selecciona una conversación</h3>
|
|
<p className="text-muted-foreground text-sm max-w-sm">
|
|
Elige un servicio para comunicarte: taxis, restaurantes, hoteles, guías turísticos o POLITUR
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Messages;
|