Skip to content

Commit

Permalink
chat section message sending emoji completed
Browse files Browse the repository at this point in the history
  • Loading branch information
sinanptm committed Sep 24, 2024
1 parent 78e7a38 commit 8b1a62d
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 32 deletions.
156 changes: 125 additions & 31 deletions client/components/page-components/chat/ChatSection.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(null)
const emojiButtonRef = useRef<HTMLButtonElement>(null)
const emojiPickerRef = useRef<HTMLDivElement>(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 (
<div className="flex items-center justify-center h-full">
Expand All @@ -55,50 +102,97 @@ const ChatSection = ({ messages, onSendMessage, sender, error, isError, isLoadin

return (
<div className="flex flex-col h-full bg-background">
<header className="p-4 border-b border-border flex-shrink-0">
<header className="p-4 border-b border-border flex-shrink-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center space-x-4">
<Button variant="ghost" size="icon" onClick={()=>router.back()} className="sm:hidden">
<Button variant="ghost" size="icon" onClick={() => router.back()} className="sm:hidden">
<ArrowLeft className="h-6 w-6" />
<span className="sr-only">Back to chat list</span>
</Button>
<Avatar className="w-10 h-10">
<AvatarImage src={getSenderData(sender,chat.doctorProfile!,chat.patientProfile!)||`/assets/icons/circle-user.svg`} alt={`${getSenderData(sender,chat.doctorName!,chat.patientName!)}`} />
<AvatarFallback>{sender}</AvatarFallback>
<AvatarImage
src={getSenderData(sender, chat.doctorProfile!, chat.patientProfile!) || `/assets/icons/circle-user.svg`}
alt={`${getSenderData(sender, chat.doctorName!, chat.patientName!)}`}
/>
<AvatarFallback>{sender[0].toUpperCase()}</AvatarFallback>
</Avatar>
<h2 className="text-xl font-semibold">{getSenderData(sender,chat.doctorName!,chat.patientName!)}</h2>
<div>
<h2 className="text-lg font-semibold">{getSenderData(sender, chat.doctorName!, chat.patientName!)}</h2>
<p className="text-sm text-muted-foreground">{sender === "doctor" ? "Patient" : "Doctor"}</p>
</div>
</div>
</header>
<ScrollArea className="flex-grow p-4">
<ScrollArea className="flex-grow p-4" ref={scrollAreaRef}>
<div className="space-y-4">
{messages.map(({ _id, message, isReceived }) => (
{messages.map(({ _id, message, isReceived, createdAt }) => (
<div
key={_id}
className={`flex items-end gap-2 ${isReceived ? "justify-start" : "justify-end"}`}
>
<div className={`rounded-lg p-3 max-w-[70%] ${isReceived ? "bg-accent" : "bg-primary text-primary-foreground"}`}>
<p className="text-sm">{message}</p>
{isReceived && (
<Avatar className="w-8 h-8">
<AvatarImage
src={getSenderData(sender === "doctor" ? "patient" : "doctor", chat.doctorProfile!, chat.patientProfile!) || `/assets/icons/circle-user.svg`}
alt={`${getSenderData(sender === "doctor" ? "patient" : "doctor", chat.doctorName!, chat.patientName!)}`}
/>
<AvatarFallback>{(sender === "doctor" ? "P" : "D")}</AvatarFallback>
</Avatar>
)}
<div className={`flex flex-col ${isReceived ? "items-start" : "items-end"} max-w-[70%]`}>
<div className={`rounded-lg p-3 ${isReceived ? "bg-accent" : "bg-primary text-primary-foreground"}`}>
<p className="text-sm">{message}</p>
</div>
<div className="flex items-center text-xs text-muted-foreground mt-1">
<span className="text-[10px]">{format(new Date(createdAt!), "HH:mm")}</span>
{!isReceived && <span className="text-[10px] ml-1"></span>}
</div>
</div>
{!isReceived && (
<Avatar className="w-8 h-8">
<AvatarImage
src={getSenderData(sender, chat.doctorProfile!, chat.patientProfile!) || `/assets/icons/circle-user.svg`}
alt={`${getSenderData(sender, chat.doctorName!, chat.patientName!)}`}
/>
<AvatarFallback>{sender[0].toUpperCase()}</AvatarFallback>
</Avatar>
)}
</div>
))}
</div>
</ScrollArea>
<footer className="border-t border-border p-4 flex-shrink-0">
<footer className="border-t border-border p-4 flex-shrink-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="flex items-center gap-2">
<Input
className="flex-1"
placeholder={`Message user ${getSenderData(sender,chat.doctorName!,chat.patientName!)}`}
placeholder={`Message ${getSenderData(sender, chat.doctorName!, chat.patientName!)}`}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()}
/>
<Button onClick={handleSendMessage}>
<Send className="h-4 w-4" />
<span className="sr-only">Send message</span>
<div className="relative">
<Button
variant="ghost"
size="icon"
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
ref={emojiButtonRef}
aria-label="Add emoji"
>
<Smile className="h-5 w-5" />
</Button>
{showEmojiPicker && (
<div className="absolute bottom-full right-0 mb-2 z-10" ref={emojiPickerRef}>
<EmojiPicker theme={Theme.AUTO} onEmojiClick={handleEmojiClick} />
</div>
)}
</div>
<Button
onClick={handleSendMessage}
disabled={!message.trim()}
>
<Send className="h-4 w-4 mr-2" />
Send
</Button>
</div>
</footer>
</div>
)
}

export default ChatSection
}
22 changes: 22 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion server/src/infrastructure/repositories/MessageRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export default class MessageRepository implements IMessageRepository {
}
async findByChatId(chatId: string, limit: number, offset: number): Promise<PaginatedResult<IMessage>> {
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<void> {
Expand Down

1 comment on commit 8b1a62d

@vercel
Copy link

@vercel vercel bot commented on 8b1a62d Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.