feat: add WeeklyCalendar, QuickActions, OpenSessions, RecentActivity dashboard components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-03 08:12:38 -05:00
parent 1397254dfd
commit 396453c18a
4 changed files with 255 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import { Link } from 'react-router-dom'
import { getTreeNavigatePath } from '@/lib/routing'
interface OpenSession {
id: string
treeName: string
treeId: string
treeType?: string
stepNumber?: number
totalSteps?: number
timeAgo: string
}
interface OpenSessionsProps {
sessions: OpenSession[]
}
export function OpenSessions({ sessions }: OpenSessionsProps) {
return (
<div className="glass-card-static flex flex-col h-full">
<div className="flex items-center justify-between px-5 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
<h3 className="font-heading text-sm font-bold text-foreground">My Open Sessions</h3>
<Link to="/sessions" className="text-[0.6875rem] text-muted-foreground hover:text-foreground transition-colors">
View All
</Link>
</div>
<div className="flex-1 flex flex-col">
{sessions.length === 0 ? (
<div className="flex-1 flex items-center justify-center">
<p className="text-sm text-muted-foreground">No open sessions</p>
</div>
) : (
sessions.map((session, i) => (
<div
key={session.id}
className="flex items-center gap-3 px-5 py-3"
style={{
borderBottom: i < sessions.length - 1 ? '1px solid var(--glass-border)' : undefined,
}}
>
<span className="h-2 w-2 shrink-0 rounded-full bg-amber-400" />
<div className="flex-1 min-w-0">
<div className="text-sm text-foreground truncate">{session.treeName}</div>
<div className="text-[0.6875rem] text-muted-foreground">
{session.stepNumber && session.totalSteps
? `Step ${session.stepNumber} of ${session.totalSteps}`
: 'In progress'}
<span className="mx-1.5 text-[hsl(var(--text-dimmed))]">&middot;</span>
<span className="font-label text-[0.625rem]">{session.timeAgo}</span>
</div>
</div>
<Link
to={getTreeNavigatePath(session.treeId, session.treeType)}
state={{ sessionId: session.id }}
className="shrink-0 rounded-lg bg-gradient-brand px-3 py-1 text-[0.6875rem] font-medium text-primary-foreground hover:opacity-90 transition-opacity"
>
Resume
</Link>
</div>
))
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,41 @@
import { useNavigate } from 'react-router-dom'
import { Plus, Play, BookOpen, UserPlus } from 'lucide-react'
const ACTIONS = [
{ icon: Plus, label: 'New Flow', description: 'Create a new flow', href: '/trees/new', color: '#06b6d4' },
{ icon: Play, label: 'Resume Session', description: 'Continue where you left off', href: '/sessions', color: '#34d399' },
{ icon: BookOpen, label: 'Browse Library', description: 'Explore step library', href: '/step-library', color: '#fbbf24' },
{ icon: UserPlus, label: 'Invite Team', description: 'Add team members', href: '/account', color: '#818cf8' },
] as const
export function QuickActions() {
const navigate = useNavigate()
return (
<div className="glass-card-static flex flex-col h-full">
<div className="px-5 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
<h3 className="font-heading text-sm font-bold text-foreground">Quick Actions</h3>
</div>
<div className="flex-1 flex flex-col justify-between p-3 gap-2">
{ACTIONS.map(({ icon: Icon, label, description, href, color }) => (
<button
key={label}
onClick={() => navigate(href)}
className="glass-card flex items-center gap-3 px-4 py-3 text-left"
>
<span
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
style={{ background: `${color}15` }}
>
<Icon size={18} style={{ color }} />
</span>
<div className="min-w-0">
<div className="text-sm font-medium text-foreground">{label}</div>
<div className="text-[0.6875rem] text-muted-foreground truncate">{description}</div>
</div>
</button>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,58 @@
import type { LucideIcon } from 'lucide-react'
import { GitBranch, Play, CheckCircle, FileText, Edit } from 'lucide-react'
interface ActivityItem {
id: string
icon: LucideIcon
iconColor: string
iconBg: string
description: string
timestamp: string
}
interface RecentActivityProps {
activities?: ActivityItem[]
}
const DEFAULT_ACTIVITIES: ActivityItem[] = [
{ id: '1', icon: Play, iconColor: '#34d399', iconBg: 'rgba(52, 211, 153, 0.1)', description: 'Started VPN Connectivity Triage session', timestamp: '2 min ago' },
{ id: '2', icon: CheckCircle, iconColor: '#06b6d4', iconBg: 'rgba(6, 182, 212, 0.1)', description: 'Completed M365 License Provisioning', timestamp: '15 min ago' },
{ id: '3', icon: Edit, iconColor: '#fbbf24', iconBg: 'rgba(251, 191, 36, 0.1)', description: 'Updated Printer Troubleshooting flow', timestamp: '1 hr ago' },
{ id: '4', icon: GitBranch, iconColor: '#818cf8', iconBg: 'rgba(129, 140, 248, 0.1)', description: 'Created new DNS Resolution flow', timestamp: '3 hr ago' },
{ id: '5', icon: FileText, iconColor: '#8891a0', iconBg: 'rgba(136, 145, 160, 0.1)', description: 'Exported session report #TK-4821', timestamp: 'Yesterday' },
]
export function RecentActivity({ activities = DEFAULT_ACTIVITIES }: RecentActivityProps) {
return (
<div className="glass-card-static">
<div className="px-5 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
<h3 className="font-heading text-sm font-bold text-foreground">Recent Activity</h3>
</div>
<div>
{activities.map((item, i) => (
<div
key={item.id}
className="flex items-start gap-3 px-5 py-3 fade-in"
style={{
animationDelay: `${750 + i * 40}ms`,
borderBottom: i < activities.length - 1 ? '1px solid var(--glass-border)' : undefined,
}}
>
<span
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[10px]"
style={{ background: item.iconBg }}
>
<item.icon size={16} style={{ color: item.iconColor }} />
</span>
<div className="flex-1 min-w-0 pt-0.5">
<p className="text-sm text-foreground">{item.description}</p>
</div>
<span className="shrink-0 font-label text-[0.625rem] text-muted-foreground pt-1">
{item.timestamp}
</span>
</div>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,91 @@
import { useMemo } from 'react'
import { Calendar } from 'lucide-react'
interface CalendarEvent {
id: string
title: string
time: string
type: 'default' | 'maintenance'
}
interface WeeklyCalendarProps {
events?: Record<string, CalendarEvent[]>
}
const DAY_NAMES = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
function getWeekDays(): { label: string; date: Date; dateStr: string; isToday: boolean }[] {
const now = new Date()
const day = now.getDay()
const mondayOffset = day === 0 ? 6 : day - 1
const monday = new Date(now)
monday.setDate(now.getDate() - mondayOffset)
return DAY_NAMES.map((label, i) => {
const d = new Date(monday)
d.setDate(monday.getDate() + i)
const dateStr = d.toISOString().split('T')[0]
const isToday = d.toDateString() === now.toDateString()
return { label, date: d, dateStr, isToday }
})
}
export function WeeklyCalendar({ events = {} }: WeeklyCalendarProps) {
const days = useMemo(() => getWeekDays(), [])
return (
<div className="glass-card-static flex flex-col h-full">
<div className="flex items-center gap-2 px-5 py-3" style={{ borderBottom: '1px solid var(--glass-border)' }}>
<Calendar size={16} className="text-muted-foreground" />
<h3 className="font-heading text-sm font-bold text-foreground">This Week</h3>
</div>
<div className="flex flex-1 min-h-0">
{days.map((day, i) => {
const dayEvents = events[day.dateStr] || []
return (
<div
key={day.dateStr}
className="flex-1 flex flex-col min-h-0"
style={{
borderRight: i < 4 ? '1px solid var(--glass-border)' : undefined,
}}
>
<div
className="px-2 py-2 text-center"
style={{
borderBottom: day.isToday ? '2px solid #06b6d4' : '1px solid var(--glass-border)',
}}
>
<span className={`font-label text-[0.625rem] uppercase tracking-[0.1em] ${day.isToday ? 'text-cyan-400' : 'text-muted-foreground'}`}>
{day.label}
</span>
<div className={`text-sm font-heading font-bold ${day.isToday ? 'text-foreground' : 'text-muted-foreground'}`}>
{day.date.getDate()}
</div>
</div>
<div className="flex-1 overflow-y-auto p-1.5 space-y-1">
{dayEvents.length === 0 ? (
<p className="text-[0.625rem] text-[hsl(var(--text-dimmed))] text-center py-2">No events</p>
) : (
dayEvents.map(event => (
<div
key={event.id}
className="rounded-md px-2 py-1.5 text-[0.6875rem] cursor-pointer hover:bg-accent/30 transition-colors"
style={{
borderLeft: `3px solid ${event.type === 'maintenance' ? '#fbbf24' : '#06b6d4'}`,
background: 'rgba(255, 255, 255, 0.02)',
}}
>
<div className="font-medium text-foreground truncate">{event.title}</div>
<div className="font-label text-[0.5625rem] text-muted-foreground">{event.time}</div>
</div>
))
)}
</div>
</div>
)
})}
</div>
</div>
)
}