Added in Logo's and worked on Avatars and Chats
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
"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)
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user