Files
NewStrcuture_Backup/src/app/(dashboard)/chats/page.tsx
T

403 lines
16 KiB
TypeScript

"use client"
import { useState, useRef, useCallback, useEffect } from "react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { conversations as conversationsData } from "@/data/chats"
import {
Search, Send, Phone, Video, MoreHorizontal, Paperclip,
Smile, Flag, Ban, Trash2, Image, File, X,
} from "lucide-react"
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
DialogFooter, DialogClose,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { useTheme } from "next-themes"
import { useUser } from "@/providers/user-provider"
import { toast } from "sonner"
import data from "@emoji-mart/data"
import Picker from "@emoji-mart/react"
export default function ChatsPage() {
const { theme } = useTheme()
const { user } = useUser()
const [activeChat, setActiveChat] = useState(conversationsData[0]?.id ?? null)
const [messageInput, setMessageInput] = useState("")
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [attachments, setAttachments] = useState<File[]>([])
const [panelWidth, setPanelWidth] = useState(320)
const [isResizing, setIsResizing] = useState(false)
const [reportDialogOpen, setReportDialogOpen] = useState(false)
const [reportReason, setReportReason] = useState("")
const fileInputRef = useRef<HTMLInputElement>(null)
const emojiPickerRef = useRef<HTMLDivElement>(null)
const resizeStartRef = useRef({ x: 0, width: 0 })
const [conversations, setConversations] = useState(conversationsData)
if (!user) return null
const conversation = conversations.find((c) => c.id === activeChat)
const otherParticipant = (conv: typeof conversationsData[0]) =>
conv.participants.find((p) => p.id !== "user1") ?? conv.participants[0]
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (emojiPickerRef.current && !emojiPickerRef.current.contains(e.target as Node)) {
setShowEmojiPicker(false)
}
}
document.addEventListener("mousedown", handleClickOutside)
return () => document.removeEventListener("mousedown", handleClickOutside)
}, [])
const handleResizeStart = useCallback((e: React.MouseEvent) => {
e.preventDefault()
setIsResizing(true)
resizeStartRef.current = { x: e.clientX, width: panelWidth }
}, [panelWidth])
useEffect(() => {
if (!isResizing) return
const handleMouseMove = (e: MouseEvent) => {
const delta = e.clientX - resizeStartRef.current.x
const newWidth = Math.max(240, Math.min(560, resizeStartRef.current.width + delta))
setPanelWidth(newWidth)
}
const handleMouseUp = () => setIsResizing(false)
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
return () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
}, [isResizing])
const handleEmojiSelect = (emoji: { native: string }) => {
setMessageInput((prev) => prev + emoji.native)
setShowEmojiPicker(false)
}
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? [])
setAttachments((prev) => [...prev, ...files])
if (e.target) e.target.value = ""
}
const removeAttachment = (index: number) => {
setAttachments((prev) => prev.filter((_, i) => i !== index))
}
const handleSend = (e: React.FormEvent) => {
e.preventDefault()
if (!messageInput.trim() && attachments.length === 0) return
const newMessage = {
id: crypto.randomUUID(),
conversationId: activeChat!,
senderId: "user1",
senderName: "Sarah Chen",
senderAvatar: "SC",
content: messageInput.trim(),
timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
}
setConversations((prev) =>
prev.map((conv) =>
conv.id === activeChat
? {
...conv,
messages: [...conv.messages, newMessage],
lastMessage: newMessage.content,
lastMessageTime: "Just now",
unread: 0,
}
: conv
)
)
setMessageInput("")
setAttachments([])
}
const formatFileSize = (bytes: number) => {
if (bytes < 1024) return bytes + " B"
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
}
const isImageFile = (file: File) => file.type.startsWith("image/")
return (
<div className="flex h-[calc(100vh-8rem)] -m-4 lg:-m-6 rounded-lg border bg-card overflow-hidden">
{/* Conversations list - left panel */}
<div
className="flex flex-col border-r shrink-0 overflow-hidden"
style={{ width: panelWidth }}
>
<div className="p-4 border-b space-y-3">
<h2 className="text-lg font-semibold">Chats</h2>
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input placeholder="Search conversations..." className="h-9 pl-9" />
</div>
</div>
<ScrollArea className="flex-1">
{conversations.map((conv) => {
const person = otherParticipant(conv)
const isActive = conv.id === activeChat
return (
<button
key={conv.id}
onClick={() => setActiveChat(conv.id)}
className={cn(
"w-full flex items-start gap-3 p-4 text-left transition-colors hover:bg-muted/50",
isActive && "bg-muted"
)}
>
<Avatar className="h-10 w-10 shrink-0">
<AvatarFallback>{person.avatar}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium truncate">{person.name}</span>
<span className="text-xs text-muted-foreground shrink-0">{conv.lastMessageTime}</span>
</div>
<p className="text-xs text-muted-foreground truncate mt-0.5">{conv.lastMessage}</p>
</div>
{conv.unread > 0 && (
<span className="shrink-0 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-[10px] font-medium text-primary-foreground">
{conv.unread}
</span>
)}
</button>
)
})}
</ScrollArea>
</div>
{/* Resize handle */}
<div
className="w-1.5 cursor-col-resize shrink-0 relative group hover:bg-primary/20 transition-colors"
onMouseDown={handleResizeStart}
>
<div className="absolute inset-y-0 -left-1 -right-1" />
</div>
{/* Chat area - right panel */}
{conversation ? (
<div className="flex-1 flex flex-col min-w-0">
{/* Chat header */}
<div className="flex items-center justify-between gap-4 px-6 h-16 border-b shrink-0">
<div className="flex items-center gap-3 min-w-0">
<Avatar className="h-9 w-9 shrink-0">
<AvatarFallback>{otherParticipant(conversation).avatar}</AvatarFallback>
</Avatar>
<div className="min-w-0">
<p className="text-sm font-medium truncate">{otherParticipant(conversation).name}</p>
<p className="text-xs text-muted-foreground truncate">{otherParticipant(conversation).role}</p>
</div>
</div>
<div className="flex items-center gap-1 shrink-0">
<Button
variant="ghost" size="icon" className="h-8 w-8"
onClick={() => toast.info("Voice calling coming soon")}
>
<Phone className="h-4 w-4" />
</Button>
<Button
variant="ghost" size="icon" className="h-8 w-8"
onClick={() => toast.info("Video calling coming soon")}
>
<Video className="h-4 w-4" />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setReportDialogOpen(true)}>
<Flag className="mr-2 h-4 w-4" /> Report
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toast.info("Blocked")}>
<Ban className="mr-2 h-4 w-4" /> Block
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={() => toast.info("Conversation deleted")}
>
<Trash2 className="mr-2 h-4 w-4" /> Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Messages */}
<ScrollArea className="flex-1 p-6">
<div className="space-y-4">
{conversation.messages.map((msg) => {
const isMe = msg.senderId === "user1"
return (
<div key={msg.id} className={cn("flex gap-3", isMe && "flex-row-reverse")}>
<Avatar className="h-8 w-8 mt-0.5 shrink-0">
{isMe ? <AvatarImage src={user.avatar} /> : null}
<AvatarFallback className={cn("text-xs", isMe && "bg-primary text-primary-foreground")}>
{msg.senderAvatar}
</AvatarFallback>
</Avatar>
<div className={cn("max-w-[70%]", isMe && "items-end flex flex-col")}>
<div
className={cn(
"rounded-2xl px-4 py-2.5 text-sm",
isMe ? "bg-primary text-primary-foreground rounded-tr-sm" : "bg-muted rounded-tl-sm"
)}
>
{msg.content}
</div>
<span className="text-[10px] text-muted-foreground mt-1 px-1">{msg.timestamp}</span>
</div>
</div>
)
})}
</div>
</ScrollArea>
{/* Input */}
<div className="p-4 border-t shrink-0 space-y-2">
{attachments.length > 0 && (
<div className="flex flex-wrap gap-2">
{attachments.map((file, i) => (
<div
key={i}
className="flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-1.5 text-sm max-w-[200px]"
>
{isImageFile(file) ? (
<Image className="h-4 w-4 shrink-0 text-muted-foreground" />
) : (
<File className="h-4 w-4 shrink-0 text-muted-foreground" />
)}
<span className="truncate">{file.name}</span>
<span className="text-xs text-muted-foreground shrink-0">{formatFileSize(file.size)}</span>
<Button
variant="ghost" size="icon" className="h-5 w-5 -mr-1 shrink-0"
onClick={() => removeAttachment(i)}
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
)}
<form onSubmit={handleSend} className="flex items-center gap-2">
<input
ref={fileInputRef}
type="file"
multiple
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt"
className="hidden"
onChange={handleFileSelect}
/>
<Button
type="button" variant="ghost" size="icon" className="h-9 w-9 shrink-0"
onClick={() => fileInputRef.current?.click()}
>
<Paperclip className="h-4 w-4 text-muted-foreground" />
</Button>
<div className="relative flex-1">
<Input
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
placeholder="Type a message..."
className="h-9 w-full pr-9"
/>
<div className="absolute right-0 top-0 bottom-0 flex items-center pr-1" ref={emojiPickerRef}>
<Button
type="button" variant="ghost" size="icon" className="h-7 w-7 shrink-0"
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
>
<Smile className="h-4 w-4 text-muted-foreground" />
</Button>
{showEmojiPicker && (
<div className="absolute bottom-full right-0 mb-2 z-50">
<Picker
data={data}
onEmojiSelect={handleEmojiSelect}
theme={theme === "dark" ? "dark" : "light"}
previewPosition="none"
skinTonePosition="none"
set="native"
maxFrequentRows={2}
/>
</div>
)}
</div>
</div>
<Button type="submit" size="icon" className="h-9 w-9 shrink-0" disabled={!messageInput.trim() && attachments.length === 0}>
<Send className="h-4 w-4" />
</Button>
</form>
</div>
</div>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<p>Select a conversation to start chatting</p>
</div>
)}
{/* Resize overlay */}
{isResizing && <div className="fixed inset-0 z-50 cursor-col-resize" />}
{/* Report dialog */}
<Dialog open={reportDialogOpen} onOpenChange={setReportDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Report {conversation ? otherParticipant(conversation).name : "User"}</DialogTitle>
<DialogDescription>
Let us know why you're reporting this conversation.
</DialogDescription>
</DialogHeader>
<div className="space-y-3 py-2">
<Label htmlFor="reason">Reason</Label>
<Textarea
id="reason"
placeholder="Describe the issue..."
value={reportReason}
onChange={(e) => setReportReason(e.target.value)}
rows={4}
/>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button
onClick={() => {
toast.success("Report submitted")
setReportReason("")
setReportDialogOpen(false)
}}
disabled={!reportReason.trim()}
>
Submit Report
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}