178 lines
5.0 KiB
TypeScript
178 lines
5.0 KiB
TypeScript
|
|
'use client'
|
||
|
|
|
||
|
|
import { useState, useRef, useEffect } from 'react'
|
||
|
|
import { cn } from '@/lib/utils'
|
||
|
|
import { Button } from '@/components/ui/button'
|
||
|
|
import { Textarea } from '@/components/ui/textarea'
|
||
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
||
|
|
import { Send, MessageSquare } from 'lucide-react'
|
||
|
|
|
||
|
|
interface Message {
|
||
|
|
id: string
|
||
|
|
message: string
|
||
|
|
createdAt: Date | string
|
||
|
|
isRead: boolean
|
||
|
|
sender: {
|
||
|
|
id: string
|
||
|
|
name: string | null
|
||
|
|
email: string
|
||
|
|
role?: string
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
interface MentorChatProps {
|
||
|
|
messages: Message[]
|
||
|
|
currentUserId: string
|
||
|
|
onSendMessage: (message: string) => Promise<void>
|
||
|
|
isLoading?: boolean
|
||
|
|
isSending?: boolean
|
||
|
|
className?: string
|
||
|
|
}
|
||
|
|
|
||
|
|
export function MentorChat({
|
||
|
|
messages,
|
||
|
|
currentUserId,
|
||
|
|
onSendMessage,
|
||
|
|
isLoading,
|
||
|
|
isSending,
|
||
|
|
className,
|
||
|
|
}: MentorChatProps) {
|
||
|
|
const [newMessage, setNewMessage] = useState('')
|
||
|
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||
|
|
|
||
|
|
const scrollToBottom = () => {
|
||
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||
|
|
}
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
scrollToBottom()
|
||
|
|
}, [messages])
|
||
|
|
|
||
|
|
const handleSend = async () => {
|
||
|
|
const text = newMessage.trim()
|
||
|
|
if (!text || isSending) return
|
||
|
|
setNewMessage('')
|
||
|
|
await onSendMessage(text)
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||
|
|
e.preventDefault()
|
||
|
|
handleSend()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const formatTime = (date: Date | string) => {
|
||
|
|
const d = typeof date === 'string' ? new Date(date) : date
|
||
|
|
return d.toLocaleString('en-US', {
|
||
|
|
month: 'short',
|
||
|
|
day: 'numeric',
|
||
|
|
hour: 'numeric',
|
||
|
|
minute: '2-digit',
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isLoading) {
|
||
|
|
return (
|
||
|
|
<div className={cn('flex flex-col gap-3', className)}>
|
||
|
|
<Skeleton className="h-16 w-3/4" />
|
||
|
|
<Skeleton className="h-16 w-3/4 ml-auto" />
|
||
|
|
<Skeleton className="h-16 w-3/4" />
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={cn('flex flex-col', className)}>
|
||
|
|
{/* Messages */}
|
||
|
|
<div className="flex-1 overflow-y-auto max-h-[400px] space-y-3 p-4">
|
||
|
|
{messages.length === 0 ? (
|
||
|
|
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||
|
|
<MessageSquare className="h-10 w-10 mb-3 opacity-50" />
|
||
|
|
<p className="text-sm font-medium">No messages yet</p>
|
||
|
|
<p className="text-xs mt-1">Send a message to start the conversation</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
messages.map((msg) => {
|
||
|
|
const isOwn = msg.sender.id === currentUserId
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
key={msg.id}
|
||
|
|
className={cn(
|
||
|
|
'flex',
|
||
|
|
isOwn ? 'justify-end' : 'justify-start'
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
'max-w-[80%] rounded-lg px-4 py-2.5',
|
||
|
|
isOwn
|
||
|
|
? 'bg-primary text-primary-foreground'
|
||
|
|
: 'bg-muted'
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{!isOwn && (
|
||
|
|
<p className={cn(
|
||
|
|
'text-xs font-medium mb-1',
|
||
|
|
isOwn ? 'text-primary-foreground/70' : 'text-foreground/70'
|
||
|
|
)}>
|
||
|
|
{msg.sender.name || msg.sender.email}
|
||
|
|
{msg.sender.role === 'MENTOR' && (
|
||
|
|
<span className="ml-1.5 text-[10px] font-normal opacity-70">
|
||
|
|
Mentor
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
<p className="text-sm whitespace-pre-wrap break-words">
|
||
|
|
{msg.message}
|
||
|
|
</p>
|
||
|
|
<p
|
||
|
|
className={cn(
|
||
|
|
'text-[10px] mt-1',
|
||
|
|
isOwn
|
||
|
|
? 'text-primary-foreground/60'
|
||
|
|
: 'text-muted-foreground'
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{formatTime(msg.createdAt)}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
})
|
||
|
|
)}
|
||
|
|
<div ref={messagesEndRef} />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Input */}
|
||
|
|
<div className="border-t p-3">
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Textarea
|
||
|
|
ref={textareaRef}
|
||
|
|
value={newMessage}
|
||
|
|
onChange={(e) => setNewMessage(e.target.value)}
|
||
|
|
onKeyDown={handleKeyDown}
|
||
|
|
placeholder="Type a message..."
|
||
|
|
className="min-h-[40px] max-h-[120px] resize-none"
|
||
|
|
rows={1}
|
||
|
|
disabled={isSending}
|
||
|
|
/>
|
||
|
|
<Button
|
||
|
|
size="icon"
|
||
|
|
onClick={handleSend}
|
||
|
|
disabled={!newMessage.trim() || isSending}
|
||
|
|
className="shrink-0"
|
||
|
|
>
|
||
|
|
<Send className="h-4 w-4" />
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
<p className="text-[10px] text-muted-foreground mt-1.5">
|
||
|
|
Press Enter to send, Shift+Enter for new line
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|