Add modular dashboard widgets and layout editor
Some checks failed
Build Frontend / build (push) Failing after 7s
Some checks failed
Build Frontend / build (push) Failing after 7s
Introduces a modular dashboard system with draggable, configurable widgets including revenue, low stock, recent customers, and pending chats. Adds a dashboard editor for layout customization, widget visibility, and settings. Refactors dashboard content to use the new widget system and improves UI consistency and interactivity.
This commit is contained in:
172
components/dashboard/pending-chats-widget.tsx
Normal file
172
components/dashboard/pending-chats-widget.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { MessageSquare, MessageCircle, ArrowRight, Clock } from "lucide-react"
|
||||
import { clientFetch, getCookie } from "@/lib/api"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import Link from "next/link"
|
||||
import { RelativeTime } from "@/components/ui/relative-time"
|
||||
|
||||
interface PendingChatsWidgetProps {
|
||||
settings?: {
|
||||
showPreview?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
interface Chat {
|
||||
id: string
|
||||
buyerId: string
|
||||
telegramUsername?: string
|
||||
lastUpdated: string
|
||||
unreadCount: number
|
||||
}
|
||||
|
||||
export default function PendingChatsWidget({ settings }: PendingChatsWidgetProps) {
|
||||
const showPreview = settings?.showPreview !== false
|
||||
const [chats, setChats] = useState<Chat[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const getVendorIdFromToken = () => {
|
||||
const authToken = getCookie("Authorization") || ""
|
||||
if (!authToken) return null
|
||||
try {
|
||||
const payload = JSON.parse(atob(authToken.split(".")[1]))
|
||||
return payload.id
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchChats = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const vendorId = getVendorIdFromToken()
|
||||
if (!vendorId) {
|
||||
setError("Please login to view chats")
|
||||
return
|
||||
}
|
||||
|
||||
const response = await clientFetch(`/chats/vendor/${vendorId}/batch?page=1&limit=5`)
|
||||
|
||||
const chatCounts = response.unreadCounts?.chatCounts || {}
|
||||
const pendingChats = (response.chats || [])
|
||||
.filter((c: any) => chatCounts[c._id] > 0)
|
||||
.map((c: any) => ({
|
||||
id: c._id,
|
||||
buyerId: c.buyerId,
|
||||
telegramUsername: c.telegramUsername,
|
||||
lastUpdated: c.lastUpdated,
|
||||
unreadCount: chatCounts[c._id] || 0
|
||||
}))
|
||||
|
||||
setChats(pendingChats)
|
||||
} catch (err) {
|
||||
console.error("Error fetching chats:", err)
|
||||
setError("Failed to load chats")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchChats()
|
||||
}, [])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="border-border/40 bg-background/50 backdrop-blur-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<Skeleton className="h-5 w-32 mb-1" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="flex items-center gap-4">
|
||||
<Skeleton className="h-10 w-10 rounded-full" />
|
||||
<div className="space-y-2 flex-1">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-3 w-1/4" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-border/40 bg-background/50 backdrop-blur-sm overflow-hidden flex flex-col h-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<MessageSquare className="h-5 w-5 text-emerald-500" />
|
||||
Pending Chats
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Unanswered customer messages
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Link href="/dashboard/chats">
|
||||
<Button variant="ghost" size="sm" className="h-8 gap-1.5 text-xs">
|
||||
Inbox
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
</Button>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 flex-grow">
|
||||
{error ? (
|
||||
<div className="flex flex-col items-center justify-center py-10 text-center">
|
||||
<MessageCircle className="h-10 w-10 text-muted-foreground/20 mb-3" />
|
||||
<p className="text-sm text-muted-foreground">{error}</p>
|
||||
</div>
|
||||
) : chats.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-emerald-500/10 flex items-center justify-center mb-4">
|
||||
<MessageCircle className="h-6 w-6 text-emerald-500" />
|
||||
</div>
|
||||
<h3 className="font-medium">All caught up!</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1 max-w-xs">
|
||||
No pending customer chats that require your attention.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{chats.map((chat) => (
|
||||
<Link
|
||||
key={chat.id}
|
||||
href={`/dashboard/chats/${chat.id}`}
|
||||
className="flex items-center gap-4 p-3 rounded-xl hover:bg-muted/50 transition-colors group"
|
||||
>
|
||||
<div className="relative">
|
||||
<Avatar className="h-10 w-10 border shadow-sm group-hover:scale-105 transition-transform">
|
||||
<AvatarFallback className="bg-emerald-500/10 text-emerald-600 text-xs font-bold">
|
||||
{(chat.telegramUsername || chat.buyerId).slice(0, 2).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="absolute -top-0.5 -right-0.5 h-3 w-3 bg-emerald-500 rounded-full ring-2 ring-background border border-background shadow-sm" />
|
||||
</div>
|
||||
<div className="flex-grow min-w-0">
|
||||
<h4 className="font-semibold text-sm truncate group-hover:text-primary transition-colors">
|
||||
{chat.telegramUsername ? `@${chat.telegramUsername}` : `Customer ${chat.buyerId.slice(-6)}`}
|
||||
</h4>
|
||||
<div className="flex items-center gap-2 mt-0.5 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3" />
|
||||
<RelativeTime date={new Date(chat.lastUpdated)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-emerald-500 text-emerald-foreground text-[10px] font-bold px-1.5 py-0.5 rounded-full shadow-sm ring-2 ring-emerald-500/10">
|
||||
{chat.unreadCount}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user