- Remove GreetingStatStrip (duplicated PerformanceCards data) - Strip left-border accent from stat cards (AI slop pattern) - Redesign KnowledgeBaseCards: icon grid → compact row list with icon badges - Redesign TeamSummary: distinct inline-row layout, no longer identical twin - Differentiate hover: stat cards use subtle border-hover, sessions keep springy lift - Add loading skeletons to PerformanceCards, KnowledgeBaseCards, TeamSummary - Add error state to PerformanceCards - Extract timeAgo() to shared lib/timeAgo.ts (replaced 4 duplicates) - Fix Skeleton bg-brand-border (undefined CSS var) → border-default - Fix double text-xs text-[0.5625rem] class conflicts across dashboard Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { CheckCircle, Clock, TrendingUp, Timer } from 'lucide-react'
|
|
import type { LucideIcon } from 'lucide-react'
|
|
import { sidebarApi } from '@/api'
|
|
import { cn } from '@/lib/utils'
|
|
import { Skeleton } from '@/components/ui/Skeleton'
|
|
|
|
interface StatCard {
|
|
label: string
|
|
value: string | number
|
|
icon: LucideIcon
|
|
iconColor: string
|
|
href: string
|
|
highlight?: boolean
|
|
}
|
|
|
|
export function PerformanceCards() {
|
|
const navigate = useNavigate()
|
|
const [resolved, setResolved] = useState(0)
|
|
const [active, setActive] = useState(0)
|
|
const [totalMinutes, setTotalMinutes] = useState(0)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState(false)
|
|
|
|
useEffect(() => {
|
|
sidebarApi.getStats()
|
|
.then((stats) => {
|
|
setResolved(stats.resolved_today)
|
|
setActive(stats.active_count)
|
|
setTotalMinutes(stats.total_session_minutes_today)
|
|
})
|
|
.catch(() => setError(true))
|
|
.finally(() => setLoading(false))
|
|
}, [])
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
{Array.from({ length: 4 }).map((_, i) => (
|
|
<div key={i} className="card-flat p-4 space-y-2">
|
|
<Skeleton className="h-3 w-2/3" />
|
|
<Skeleton className="h-7 w-1/2" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="card-flat p-4 text-center">
|
|
<p className="text-sm text-muted-foreground">Unable to load performance data</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const avgMttr = resolved > 0 ? Math.round(totalMinutes / resolved) : 0
|
|
|
|
const cards: StatCard[] = [
|
|
{
|
|
label: 'Resolved Today',
|
|
value: resolved,
|
|
icon: CheckCircle,
|
|
iconColor: '#34d399',
|
|
href: '/analytics',
|
|
highlight: true,
|
|
},
|
|
{
|
|
label: 'Avg Resolution',
|
|
value: avgMttr > 0 ? `${avgMttr}m` : '\u2014',
|
|
icon: Clock,
|
|
iconColor: '#60a5fa',
|
|
href: '/analytics',
|
|
},
|
|
{
|
|
label: 'Active Now',
|
|
value: active,
|
|
icon: TrendingUp,
|
|
iconColor: '#848b9b',
|
|
href: '/sessions?filter=active',
|
|
},
|
|
{
|
|
label: 'Session Time',
|
|
value: totalMinutes > 0 ? `${totalMinutes}m` : '\u2014',
|
|
icon: Timer,
|
|
iconColor: '#fbbf24',
|
|
href: '/analytics',
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
{cards.map((card, i) => (
|
|
<button
|
|
key={card.label}
|
|
onClick={() => navigate(card.href)}
|
|
className="card-flat p-4 text-left fade-in hover:border-[var(--color-border-hover)] transition-colors cursor-pointer"
|
|
style={{
|
|
animationDelay: `${400 + i * 60}ms`,
|
|
}}
|
|
>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<p className="font-sans text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
|
{card.label}
|
|
</p>
|
|
<card.icon size={14} style={{ color: card.iconColor }} />
|
|
</div>
|
|
<p className={cn(
|
|
'font-heading text-2xl font-extrabold tracking-tight',
|
|
card.highlight ? 'text-accent-text' : 'text-foreground'
|
|
)}>
|
|
{card.value}
|
|
</p>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|