diff --git a/client/components/page-components/chat/ChatSection.tsx b/client/components/page-components/chat/ChatSection.tsx index fa0dc553..8157d8ca 100644 --- a/client/components/page-components/chat/ChatSection.tsx +++ b/client/components/page-components/chat/ChatSection.tsx @@ -1,38 +1,85 @@ -'use client' +"use client" +import { useRef, useEffect, useState } from "react" import { ScrollArea } from "@/components/ui/scroll-area" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" -import { ArrowLeft, Send, AlertCircle } from "lucide-react" +import { ArrowLeft, Send, AlertCircle, Smile } from "lucide-react" import { IChat, IMessage } from "@/types" -import { useState } from "react" import { useRouter } from "next/navigation" import { Spinner } from "@/components/skeletons/spinner" import { getSenderData } from "./getSenderData" +import { format } from "date-fns" +import dynamic from 'next/dynamic' +import { EmojiClickData, Theme } from 'emoji-picker-react' + +const EmojiPicker = dynamic(() => import('emoji-picker-react'), { ssr: false }) interface ChatSectionProps { - sender:"doctor"|"patient"; - messages: IMessage[]; - onSendMessage: (message: string) => void; - isError: boolean; - isPending:boolean; - isLoading: boolean; - chat:IChat; - error?: string; + sender: "doctor" | "patient" + messages: IMessage[] + onSendMessage: (message: string) => void + isError: boolean + isPending: boolean + isLoading: boolean + chat: IChat + error?: string } -const ChatSection = ({ messages, onSendMessage, sender, error, isError, isLoading, chat, isPending }: ChatSectionProps) => { - const [message, setMessage] = useState(""); +export default function ChatSection({ + messages, + onSendMessage, + sender, + error, + isError, + isLoading, + chat, + isPending, +}: ChatSectionProps) { + const [message, setMessage] = useState("") + const [showEmojiPicker, setShowEmojiPicker] = useState(false) const router = useRouter() + const scrollAreaRef = useRef(null) + const emojiButtonRef = useRef(null) + const emojiPickerRef = useRef(null) const handleSendMessage = () => { if (message.trim()) { onSendMessage(message) setMessage("") + setShowEmojiPicker(false) } } + const handleEmojiClick = (emojiData: EmojiClickData) => { + setMessage((prevMessage) => prevMessage + emojiData.emoji) + } + + useEffect(() => { + if (scrollAreaRef.current) { + scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight + } + }, [messages]) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + emojiButtonRef.current && + !emojiButtonRef.current.contains(event.target as Node) && + emojiPickerRef.current && + !emojiPickerRef.current.contains(event.target as Node) + ) { + setShowEmojiPicker(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + if (isLoading) { return (
@@ -55,50 +102,97 @@ const ChatSection = ({ messages, onSendMessage, sender, error, isError, isLoadin return (
-
+
- - - {sender} + + {sender[0].toUpperCase()} -

{getSenderData(sender,chat.doctorName!,chat.patientName!)}

+
+

{getSenderData(sender, chat.doctorName!, chat.patientName!)}

+

{sender === "doctor" ? "Patient" : "Doctor"}

+
- +
- {messages.map(({ _id, message, isReceived }) => ( + {messages.map(({ _id, message, isReceived, createdAt }) => (
-
-

{message}

+ {isReceived && ( + + + {(sender === "doctor" ? "P" : "D")} + + )} +
+
+

{message}

+
+
+ {format(new Date(createdAt!), "HH:mm")} + {!isReceived && } +
+ {!isReceived && ( + + + {sender[0].toUpperCase()} + + )}
))}
-
+
setMessage(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()} + onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} /> - + {showEmojiPicker && ( +
+ +
+ )} +
+
) -} - -export default ChatSection \ No newline at end of file +} \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 9aea9ed1..27836918 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -33,6 +33,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "emoji-picker-react": "^4.12.0", "firebase": "^10.13.1", "framer-motion": "^11.3.28", "input-otp": "^1.2.4", @@ -3830,6 +3831,21 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/emoji-picker-react": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.0.tgz", + "integrity": "sha512-q2c8UcZH0eRIMj41bj0k1akTjk69tsu+E7EzkW7giN66iltF6H9LQvQvw6ugscsxdC+1lmt3WZpQkkY65J95tg==", + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -4653,6 +4669,12 @@ "@firebase/vertexai-preview": "0.0.3" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "license": "MIT" + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", diff --git a/client/package.json b/client/package.json index bde2bb98..3d8e08f5 100644 --- a/client/package.json +++ b/client/package.json @@ -36,6 +36,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", + "emoji-picker-react": "^4.12.0", "firebase": "^10.13.1", "framer-motion": "^11.3.28", "input-otp": "^1.2.4", diff --git a/server/src/infrastructure/repositories/MessageRepository.ts b/server/src/infrastructure/repositories/MessageRepository.ts index 3f1af4fb..b2b1caa1 100644 --- a/server/src/infrastructure/repositories/MessageRepository.ts +++ b/server/src/infrastructure/repositories/MessageRepository.ts @@ -14,7 +14,10 @@ export default class MessageRepository implements IMessageRepository { } async findByChatId(chatId: string, limit: number, offset: number): Promise> { const totalItems = await this.model.countDocuments({ chatId }); - const items = await this.model.find({ chatId }).limit(limit); + const items = await this.model.find({ chatId }) + .sort({ createdAt: 1 }) + .limit(limit) + .skip(offset); return getPaginatedResult(totalItems, offset, limit, items); } async markAsRead(messageId: string): Promise {