Spaces:
Sleeping
Sleeping
| import React, { useState } from "react"; | |
| import Layout from "@/components/layout/layout"; | |
| import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | |
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import { Send, Search, Phone, Video, MessageSquare, Inbox, Paperclip, MoreHorizontal, Plus, Users } from "lucide-react"; | |
| interface Message { | |
| id: string; | |
| sender: string; | |
| content: string; | |
| timestamp: string; | |
| isRead: boolean; | |
| isOutgoing: boolean; | |
| avatar?: string; | |
| } | |
| interface Conversation { | |
| id: string; | |
| participant: string; | |
| lastMessage: string; | |
| timestamp: string; | |
| unreadCount: number; | |
| avatar?: string; | |
| isOnline: boolean; | |
| isGroup?: boolean; | |
| members?: string[]; | |
| } | |
| const Messages = () => { | |
| const [conversations, setConversations] = useState<Conversation[]>([ | |
| { | |
| id: "1", | |
| participant: "Team Alpha", | |
| lastMessage: "Let's discuss the project timeline", | |
| timestamp: "10:30 AM", | |
| unreadCount: 3, | |
| isOnline: true, | |
| isGroup: true, | |
| members: ["Jane Smith", "Alex Johnson", "Sarah Williams"] | |
| }, | |
| { | |
| id: "2", | |
| participant: "Jane Smith", | |
| lastMessage: "I've finished the UI designs", | |
| timestamp: "Yesterday", | |
| unreadCount: 0, | |
| isOnline: true | |
| }, | |
| { | |
| id: "3", | |
| participant: "Alex Johnson", | |
| lastMessage: "Can we review the backlog items?", | |
| timestamp: "Yesterday", | |
| unreadCount: 2, | |
| isOnline: false | |
| }, | |
| { | |
| id: "4", | |
| participant: "Sarah Williams", | |
| lastMessage: "The API integration is complete", | |
| timestamp: "Oct 15", | |
| unreadCount: 0, | |
| isOnline: false | |
| }, | |
| { | |
| id: "5", | |
| participant: "Project Leads", | |
| lastMessage: "Meeting scheduled for tomorrow", | |
| timestamp: "Oct 14", | |
| unreadCount: 0, | |
| isOnline: true, | |
| isGroup: true, | |
| members: ["Michael Brown", "Emma Davis", "David Wilson"] | |
| } | |
| ]); | |
| const [activeConversation, setActiveConversation] = useState<string>("1"); | |
| const [messageInput, setMessageInput] = useState(""); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const [messages, setMessages] = useState<Record<string, Message[]>>({ | |
| "1": [ | |
| { | |
| id: "1-1", | |
| sender: "Alex Johnson", | |
| content: "Hey team, we should discuss the project timeline", | |
| timestamp: "10:15 AM", | |
| isRead: true, | |
| isOutgoing: false | |
| }, | |
| { | |
| id: "1-2", | |
| sender: "Sarah Williams", | |
| content: "I agree. I'm available this afternoon", | |
| timestamp: "10:20 AM", | |
| isRead: true, | |
| isOutgoing: false | |
| }, | |
| { | |
| id: "1-3", | |
| sender: "You", | |
| content: "Let's schedule a meeting for 2 PM", | |
| timestamp: "10:25 AM", | |
| isRead: true, | |
| isOutgoing: true | |
| }, | |
| { | |
| id: "1-4", | |
| sender: "Jane Smith", | |
| content: "Works for me. I'll prepare the agenda", | |
| timestamp: "10:30 AM", | |
| isRead: false, | |
| isOutgoing: false | |
| } | |
| ], | |
| "2": [ | |
| { | |
| id: "2-1", | |
| sender: "Jane Smith", | |
| content: "I've been working on the UI designs", | |
| timestamp: "Yesterday, 4:30 PM", | |
| isRead: true, | |
| isOutgoing: false | |
| }, | |
| { | |
| id: "2-2", | |
| sender: "You", | |
| content: "How's it coming along?", | |
| timestamp: "Yesterday, 4:35 PM", | |
| isRead: true, | |
| isOutgoing: true | |
| }, | |
| { | |
| id: "2-3", | |
| sender: "Jane Smith", | |
| content: "Just finished! I'll share the files", | |
| timestamp: "Yesterday, 4:40 PM", | |
| isRead: true, | |
| isOutgoing: false | |
| }, | |
| { | |
| id: "2-4", | |
| sender: "Jane Smith", | |
| content: "I've finished the UI designs and shared them in the design folder", | |
| timestamp: "Yesterday, 4:45 PM", | |
| isRead: true, | |
| isOutgoing: false | |
| } | |
| ] | |
| }); | |
| const filteredConversations = conversations.filter( | |
| (conversation) => | |
| conversation.participant.toLowerCase().includes(searchQuery.toLowerCase()) | |
| ); | |
| const handleSendMessage = () => { | |
| if (messageInput.trim() === "") return; | |
| const newMessage: Message = { | |
| id: `${activeConversation}-${Date.now()}`, | |
| sender: "You", | |
| content: messageInput, | |
| timestamp: "Just now", | |
| isRead: true, | |
| isOutgoing: true | |
| }; | |
| setMessages((prevMessages) => ({ | |
| ...prevMessages, | |
| [activeConversation]: [ | |
| ...(prevMessages[activeConversation] || []), | |
| newMessage | |
| ] | |
| })); | |
| // Update last message in conversation list | |
| setConversations( | |
| conversations.map((conversation) => | |
| conversation.id === activeConversation | |
| ? { | |
| ...conversation, | |
| lastMessage: messageInput, | |
| timestamp: "Just now", | |
| unreadCount: 0 | |
| } | |
| : conversation | |
| ) | |
| ); | |
| setMessageInput(""); | |
| }; | |
| const markAsRead = (conversationId: string) => { | |
| setConversations( | |
| conversations.map((conversation) => | |
| conversation.id === conversationId | |
| ? { ...conversation, unreadCount: 0 } | |
| : conversation | |
| ) | |
| ); | |
| }; | |
| const handleConversationClick = (conversationId: string) => { | |
| setActiveConversation(conversationId); | |
| markAsRead(conversationId); | |
| }; | |
| const getInitials = (name: string) => { | |
| return name | |
| .split(" ") | |
| .map((n) => n[0]) | |
| .join("") | |
| .toUpperCase(); | |
| }; | |
| const currentConversation = conversations.find( | |
| (conversation) => conversation.id === activeConversation | |
| ); | |
| return ( | |
| <Layout> | |
| <div className="container mx-auto py-6"> | |
| <div className="flex h-[calc(100vh-12rem)] overflow-hidden rounded-lg border"> | |
| {/* Sidebar */} | |
| <div className="w-full max-w-xs border-r flex flex-col"> | |
| <div className="p-4 border-b"> | |
| <div className="relative"> | |
| <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> | |
| <Input | |
| placeholder="Search conversations..." | |
| className="pl-8" | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| /> | |
| </div> | |
| </div> | |
| <Tabs defaultValue="all" className="flex-1 flex flex-col"> | |
| <TabsList className="grid grid-cols-3 px-4 py-2"> | |
| <TabsTrigger value="all">All</TabsTrigger> | |
| <TabsTrigger value="unread">Unread</TabsTrigger> | |
| <TabsTrigger value="groups">Groups</TabsTrigger> | |
| </TabsList> | |
| <ScrollArea className="flex-1"> | |
| <TabsContent value="all" className="m-0"> | |
| {filteredConversations.map((conversation) => ( | |
| <div | |
| key={conversation.id} | |
| className={`flex items-center gap-3 p-3 cursor-pointer hover:bg-muted/50 ${ | |
| activeConversation === conversation.id ? "bg-muted" : "" | |
| }`} | |
| onClick={() => handleConversationClick(conversation.id)} | |
| > | |
| <div className="relative"> | |
| <Avatar> | |
| <AvatarImage src={conversation.avatar} /> | |
| <AvatarFallback> | |
| {conversation.isGroup ? ( | |
| <Users className="h-4 w-4" /> | |
| ) : ( | |
| getInitials(conversation.participant) | |
| )} | |
| </AvatarFallback> | |
| </Avatar> | |
| {conversation.isOnline && ( | |
| <span className="absolute bottom-0 right-0 h-3 w-3 rounded-full bg-green-500 border-2 border-background"></span> | |
| )} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex justify-between items-center"> | |
| <span className="font-medium truncate"> | |
| {conversation.participant} | |
| </span> | |
| <span className="text-xs text-muted-foreground"> | |
| {conversation.timestamp} | |
| </span> | |
| </div> | |
| <div className="flex justify-between items-center mt-1"> | |
| <p className="text-sm text-muted-foreground truncate"> | |
| {conversation.lastMessage} | |
| </p> | |
| {conversation.unreadCount > 0 && ( | |
| <Badge variant="default" className="ml-auto"> | |
| {conversation.unreadCount} | |
| </Badge> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </TabsContent> | |
| <TabsContent value="unread" className="m-0"> | |
| {filteredConversations | |
| .filter((c) => c.unreadCount > 0) | |
| .map((conversation) => ( | |
| <div | |
| key={conversation.id} | |
| className={`flex items-center gap-3 p-3 cursor-pointer hover:bg-muted/50 ${ | |
| activeConversation === conversation.id ? "bg-muted" : "" | |
| }`} | |
| onClick={() => handleConversationClick(conversation.id)} | |
| > | |
| <Avatar> | |
| <AvatarImage src={conversation.avatar} /> | |
| <AvatarFallback> | |
| {conversation.isGroup ? ( | |
| <Users className="h-4 w-4" /> | |
| ) : ( | |
| getInitials(conversation.participant) | |
| )} | |
| </AvatarFallback> | |
| </Avatar> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex justify-between items-center"> | |
| <span className="font-medium truncate"> | |
| {conversation.participant} | |
| </span> | |
| <span className="text-xs text-muted-foreground"> | |
| {conversation.timestamp} | |
| </span> | |
| </div> | |
| <div className="flex justify-between items-center mt-1"> | |
| <p className="text-sm text-muted-foreground truncate"> | |
| {conversation.lastMessage} | |
| </p> | |
| <Badge variant="default"> | |
| {conversation.unreadCount} | |
| </Badge> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </TabsContent> | |
| <TabsContent value="groups" className="m-0"> | |
| {filteredConversations | |
| .filter((c) => c.isGroup) | |
| .map((conversation) => ( | |
| <div | |
| key={conversation.id} | |
| className={`flex items-center gap-3 p-3 cursor-pointer hover:bg-muted/50 ${ | |
| activeConversation === conversation.id ? "bg-muted" : "" | |
| }`} | |
| onClick={() => handleConversationClick(conversation.id)} | |
| > | |
| <Avatar> | |
| <AvatarImage src={conversation.avatar} /> | |
| <AvatarFallback> | |
| <Users className="h-4 w-4" /> | |
| </AvatarFallback> | |
| </Avatar> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex justify-between items-center"> | |
| <span className="font-medium truncate"> | |
| {conversation.participant} | |
| </span> | |
| <span className="text-xs text-muted-foreground"> | |
| {conversation.timestamp} | |
| </span> | |
| </div> | |
| <div className="flex justify-between items-center mt-1"> | |
| <p className="text-sm text-muted-foreground truncate"> | |
| {conversation.members?.join(", ")} | |
| </p> | |
| {conversation.unreadCount > 0 && ( | |
| <Badge variant="default"> | |
| {conversation.unreadCount} | |
| </Badge> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </TabsContent> | |
| </ScrollArea> | |
| <div className="p-3 border-t"> | |
| <Button className="w-full"> | |
| <Plus className="h-4 w-4 mr-2" /> | |
| New Message | |
| </Button> | |
| </div> | |
| </Tabs> | |
| </div> | |
| {/* Main Content */} | |
| <div className="flex-1 flex flex-col"> | |
| {currentConversation ? ( | |
| <> | |
| {/* Chat Header */} | |
| <div className="p-4 border-b flex items-center justify-between"> | |
| <div className="flex items-center"> | |
| <Avatar className="h-10 w-10 mr-3"> | |
| <AvatarImage src={currentConversation.avatar} /> | |
| <AvatarFallback> | |
| {currentConversation.isGroup ? ( | |
| <Users className="h-4 w-4" /> | |
| ) : ( | |
| getInitials(currentConversation.participant) | |
| )} | |
| </AvatarFallback> | |
| </Avatar> | |
| <div> | |
| <h2 className="font-semibold flex items-center"> | |
| {currentConversation.participant} | |
| {currentConversation.isOnline && ( | |
| <span className="ml-2 h-2 w-2 rounded-full bg-green-500"></span> | |
| )} | |
| </h2> | |
| {currentConversation.isGroup && ( | |
| <p className="text-xs text-muted-foreground"> | |
| {currentConversation.members?.join(", ")} | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <Button variant="ghost" size="icon"> | |
| <Phone className="h-4 w-4" /> | |
| </Button> | |
| <Button variant="ghost" size="icon"> | |
| <Video className="h-4 w-4" /> | |
| </Button> | |
| <Button variant="ghost" size="icon"> | |
| <MoreHorizontal className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| {/* Messages */} | |
| <ScrollArea className="flex-1 p-4"> | |
| <div className="space-y-4"> | |
| {messages[activeConversation]?.map((message) => ( | |
| <div | |
| key={message.id} | |
| className={`flex ${ | |
| message.isOutgoing ? "justify-end" : "justify-start" | |
| }`} | |
| > | |
| {!message.isOutgoing && ( | |
| <Avatar className="h-8 w-8 mr-2 mt-1"> | |
| <AvatarFallback> | |
| {getInitials(message.sender)} | |
| </AvatarFallback> | |
| </Avatar> | |
| )} | |
| <div | |
| className={`max-w-[70%] ${ | |
| message.isOutgoing | |
| ? "bg-primary text-primary-foreground" | |
| : "bg-muted" | |
| } rounded-lg p-3`} | |
| > | |
| {!message.isOutgoing && ( | |
| <p className="text-xs font-medium mb-1"> | |
| {message.sender} | |
| </p> | |
| )} | |
| <p>{message.content}</p> | |
| <p className="text-xs mt-1 opacity-70 text-right"> | |
| {message.timestamp} | |
| </p> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </ScrollArea> | |
| {/* Message Input */} | |
| <div className="p-4 border-t"> | |
| <div className="flex gap-2"> | |
| <Button variant="outline" size="icon"> | |
| <Paperclip className="h-4 w-4" /> | |
| </Button> | |
| <Input | |
| placeholder="Type a message..." | |
| value={messageInput} | |
| onChange={(e) => setMessageInput(e.target.value)} | |
| onKeyPress={(e) => { | |
| if (e.key === "Enter") handleSendMessage(); | |
| }} | |
| /> | |
| <Button | |
| size="icon" | |
| onClick={handleSendMessage} | |
| disabled={messageInput.trim() === ""} | |
| > | |
| <Send className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| </> | |
| ) : ( | |
| <div className="flex-1 flex items-center justify-center flex-col p-4"> | |
| <MessageSquare className="h-12 w-12 text-muted-foreground mb-4" /> | |
| <h3 className="text-xl font-semibold mb-2">Your Messages</h3> | |
| <p className="text-muted-foreground text-center mb-4"> | |
| Select a conversation or start a new one | |
| </p> | |
| <Button> | |
| <Plus className="h-4 w-4 mr-2" /> | |
| New Message | |
| </Button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </Layout> | |
| ); | |
| }; | |
| export default Messages; | |