This commit is contained in:
NotII
2025-03-09 03:33:58 +00:00
parent 20807da752
commit b45a4e2e01
4 changed files with 132 additions and 16 deletions

View File

@@ -11,11 +11,9 @@ export const metadata: Metadata = {
export default function ChatDetailPage({ params }: { params: { id: string } }) { export default function ChatDetailPage({ params }: { params: { id: string } }) {
return ( return (
<Dashboard> <Dashboard>
<div className="container mx-auto py-6 space-y-6"> <div className="h-full w-full">
<div className="grid grid-cols-1 gap-6">
<ChatDetail chatId={params.id} /> <ChatDetail chatId={params.id} />
</div> </div>
</div>
</Dashboard> </Dashboard>
); );
} }

View File

@@ -10,7 +10,7 @@ import { cn } from "@/lib/utils";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import axios from "axios"; import axios from "axios";
import { toast } from "sonner"; import { toast } from "sonner";
import { ArrowLeft, Send, RefreshCw } from "lucide-react"; import { ArrowLeft, Send, RefreshCw, FileText, Image as ImageIcon, Download, File } from "lucide-react";
import { getCookie } from "@/lib/client-utils"; import { getCookie } from "@/lib/client-utils";
interface Message { interface Message {
@@ -34,6 +34,50 @@ interface Chat {
orderId?: string; orderId?: string;
} }
// Helper function to extract filename from URL
const getFileNameFromUrl = (url: string): string => {
// Try to extract filename from the URL path
const pathParts = url.split('/');
const lastPart = pathParts[pathParts.length - 1];
// Remove query parameters if any
const fileNameParts = lastPart.split('?');
let fileName = fileNameParts[0];
// If filename is too long or not found, create a generic name
if (!fileName || fileName.length > 30) {
return 'attachment';
}
// URL decode the filename (handle spaces and special characters)
try {
fileName = decodeURIComponent(fileName);
} catch (e) {
// If decoding fails, use the original
}
return fileName;
};
// Helper function to get file icon based on extension or URL pattern
const getFileIcon = (url: string): React.ReactNode => {
const fileName = url.toLowerCase();
// Image files
if (/\.(jpg|jpeg|png|gif|webp|svg|bmp|tiff)($|\?)/i.test(fileName) ||
url.includes('/photos/') || url.includes('/photo/')) {
return <ImageIcon className="h-5 w-5" />;
}
// Document files
if (/\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|rtf|csv)($|\?)/i.test(fileName)) {
return <FileText className="h-5 w-5" />;
}
// Default file icon
return <File className="h-5 w-5" />;
};
export default function ChatDetail({ chatId }: { chatId: string }) { export default function ChatDetail({ chatId }: { chatId: string }) {
const router = useRouter(); const router = useRouter();
const [chat, setChat] = useState<Chat | null>(null); const [chat, setChat] = useState<Chat | null>(null);
@@ -248,8 +292,8 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
if (loading) { if (loading) {
return ( return (
<Card className="w-full h-[80vh] flex flex-col"> <Card className="w-full h-[calc(100vh-80px)] flex flex-col">
<CardHeader> <CardHeader className="border-b py-2 px-4">
<CardTitle className="flex items-center space-x-2"> <CardTitle className="flex items-center space-x-2">
<Button variant="ghost" size="icon" onClick={handleBackClick}> <Button variant="ghost" size="icon" onClick={handleBackClick}>
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
@@ -266,8 +310,8 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
if (!chat) { if (!chat) {
return ( return (
<Card className="w-full h-[80vh] flex flex-col"> <Card className="w-full h-[calc(100vh-80px)] flex flex-col">
<CardHeader> <CardHeader className="border-b py-2 px-4">
<CardTitle className="flex items-center space-x-2"> <CardTitle className="flex items-center space-x-2">
<Button variant="ghost" size="icon" onClick={handleBackClick}> <Button variant="ghost" size="icon" onClick={handleBackClick}>
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
@@ -286,8 +330,8 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
} }
return ( return (
<Card className="w-full h-[80vh] flex flex-col"> <Card className="w-full h-[calc(100vh-80px)] flex flex-col">
<CardHeader className="border-b"> <CardHeader className="border-b py-2 px-4">
<CardTitle className="flex items-center space-x-2"> <CardTitle className="flex items-center space-x-2">
<Button variant="ghost" size="icon" onClick={handleBackClick}> <Button variant="ghost" size="icon" onClick={handleBackClick}>
<ArrowLeft className="h-5 w-5" /> <ArrowLeft className="h-5 w-5" />
@@ -296,7 +340,7 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex-1 overflow-y-auto p-4 space-y-4"> <CardContent className="flex-1 overflow-y-auto p-2 space-y-2">
{chat.messages.length === 0 ? ( {chat.messages.length === 0 ? (
<div className="h-full flex items-center justify-center"> <div className="h-full flex items-center justify-center">
<p className="text-muted-foreground">No messages yet. Send one to start the conversation.</p> <p className="text-muted-foreground">No messages yet. Send one to start the conversation.</p>
@@ -312,7 +356,7 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
> >
<div <div
className={cn( className={cn(
"max-w-[80%] rounded-lg p-3", "max-w-[90%] rounded-lg p-3",
msg.sender === "vendor" msg.sender === "vendor"
? "bg-primary text-primary-foreground" ? "bg-primary text-primary-foreground"
: "bg-muted" : "bg-muted"
@@ -331,7 +375,73 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
</span> </span>
</div> </div>
<p className="whitespace-pre-wrap break-words">{msg.content}</p> <p className="whitespace-pre-wrap break-words">{msg.content}</p>
{/* Show attachments if any (future enhancement) */} {/* Show attachments if any */}
{msg.attachments && msg.attachments.length > 0 && (
<div className="mt-2 space-y-2">
{msg.attachments.map((attachment, idx) => {
// Check if attachment is an image by looking at the URL extension or paths
const isImage = /\.(jpg|jpeg|png|gif|webp)($|\?)/i.test(attachment) ||
attachment.includes('/photos/') ||
attachment.includes('/photo/');
const fileName = getFileNameFromUrl(attachment);
return isImage ? (
// Render image attachment
<div key={`attachment-${idx}`} className="rounded-md overflow-hidden bg-background/20 p-1">
<div className="flex justify-between items-center mb-1">
<span className="text-xs opacity-70 flex items-center">
<ImageIcon className="h-3 w-3 mr-1" />
{fileName}
</span>
<a
href={attachment}
download={fileName}
target="_blank"
rel="noopener noreferrer"
className="text-xs opacity-70 hover:opacity-100"
onClick={(e) => e.stopPropagation()}
>
<Download className="h-3 w-3" />
</a>
</div>
<a href={attachment} target="_blank" rel="noopener noreferrer">
<img
src={attachment}
alt={fileName}
className="max-w-full max-h-60 object-contain cursor-pointer hover:opacity-90 transition-opacity rounded"
onError={(e) => {
// Fallback for broken images
(e.target as HTMLImageElement).src = "/placeholder-image.svg";
}}
/>
</a>
</div>
) : (
// Render file attachment
<div key={`attachment-${idx}`} className="flex items-center bg-background/20 rounded-md p-2 hover:bg-background/30 transition-colors">
<div className="mr-2">
{getFileIcon(attachment)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm truncate font-medium">
{fileName}
</div>
</div>
<a
href={attachment}
download={fileName}
target="_blank"
rel="noopener noreferrer"
className="ml-2 p-1 rounded-sm hover:bg-background/50"
>
<Download className="h-4 w-4" />
</a>
</div>
);
})}
</div>
)}
</div> </div>
</div> </div>
)) ))
@@ -339,7 +449,7 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />
</CardContent> </CardContent>
<div className="p-4 border-t"> <div className="p-2 border-t">
<form onSubmit={handleSendMessage} className="flex space-x-2"> <form onSubmit={handleSendMessage} className="flex space-x-2">
<Input <Input
value={message} value={message}

View File

@@ -2,6 +2,7 @@
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { usePathname } from "next/navigation"
import Sidebar from "./sidebar" import Sidebar from "./sidebar"
import UnifiedNotifications from "@/components/notifications/UnifiedNotifications" import UnifiedNotifications from "@/components/notifications/UnifiedNotifications"
import type React from "react" import type React from "react"
@@ -13,6 +14,10 @@ interface LayoutProps {
export default function Layout({ children }: LayoutProps) { export default function Layout({ children }: LayoutProps) {
const { theme } = useTheme() const { theme } = useTheme()
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
const pathname = usePathname()
// Check if we're in a chat detail page
const isChatDetailPage = pathname?.includes('/dashboard/chats/') && !pathname?.endsWith('/chats') && !pathname?.endsWith('/new')
useEffect(() => setMounted(true), []) useEffect(() => setMounted(true), [])
@@ -27,7 +32,9 @@ export default function Layout({ children }: LayoutProps) {
<UnifiedNotifications /> <UnifiedNotifications />
</div> </div>
</header> </header>
<main className="flex-1 overflow-auto p-6 dark:bg-[#0F0F12]">{children}</main> <main className={`flex-1 overflow-auto ${isChatDetailPage ? 'p-0' : 'p-6'} dark:bg-[#0F0F12]`}>
{children}
</main>
</div> </div>
</div> </div>
) )

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="150" viewBox="0 0 200 150"><rect width="200" height="150" fill="#f0f0f0"/><path d="M100,56 L124,90 H76 Z" fill="#d0d0d0"/><circle cx="124" cy="56" r="8" fill="#d0d0d0"/><text x="100" y="120" font-family="Arial" font-size="12" text-anchor="middle" fill="#888">Image unavailable</text></svg>

After

Width:  |  Height:  |  Size: 350 B