feat: add onboarding checklist widget to dashboard
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
160
frontend/src/components/dashboard/OnboardingChecklist.tsx
Normal file
160
frontend/src/components/dashboard/OnboardingChecklist.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Check, X, ChevronRight } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getOnboardingStatus, dismissOnboarding } from '@/api/onboarding'
|
||||
import type { OnboardingStatus } from '@/api/onboarding'
|
||||
|
||||
interface ChecklistItem {
|
||||
key: keyof OnboardingStatus
|
||||
label: string
|
||||
path: string
|
||||
}
|
||||
|
||||
const SOLO_ITEMS: ChecklistItem[] = [
|
||||
{ key: 'created_flow', label: 'Create your first flow', path: '/trees' },
|
||||
{ key: 'ran_session', label: 'Run your first session', path: '/trees' },
|
||||
{ key: 'exported_session', label: 'Export a session', path: '/sessions' },
|
||||
{ key: 'tried_ai_assistant', label: 'Try the AI assistant', path: '/assistant' },
|
||||
]
|
||||
|
||||
const TEAM_ITEMS: ChecklistItem[] = [
|
||||
{ key: 'created_flow', label: 'Create your first flow', path: '/trees' },
|
||||
{ key: 'invited_teammate', label: 'Invite a team member', path: '/account' },
|
||||
{ key: 'ran_session', label: 'Run your first session', path: '/trees' },
|
||||
{ key: 'connected_psa', label: 'Connect a PSA integration', path: '/account/integrations' },
|
||||
{ key: 'exported_session', label: 'Export a session', path: '/sessions' },
|
||||
]
|
||||
|
||||
export function OnboardingChecklist() {
|
||||
const navigate = useNavigate()
|
||||
const [status, setStatus] = useState<OnboardingStatus | null>(null)
|
||||
const [dismissed, setDismissed] = useState(false)
|
||||
const [allComplete, setAllComplete] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
getOnboardingStatus()
|
||||
.then(setStatus)
|
||||
.catch(() => {
|
||||
// Silently fail — don't show checklist if endpoint unavailable
|
||||
})
|
||||
}, [])
|
||||
|
||||
const items = status?.is_team_user ? TEAM_ITEMS : SOLO_ITEMS
|
||||
const completedCount = status
|
||||
? items.filter((item) => status[item.key]).length
|
||||
: 0
|
||||
const totalCount = items.length
|
||||
const isAllDone = completedCount === totalCount && status !== null
|
||||
|
||||
useEffect(() => {
|
||||
if (isAllDone) {
|
||||
const timer = setTimeout(() => setAllComplete(true), 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [isAllDone])
|
||||
|
||||
// Don't render if dismissed, fully complete, or not loaded yet
|
||||
if (!status || status.dismissed || dismissed || allComplete) return null
|
||||
|
||||
const progressPercent = totalCount > 0 ? (completedCount / totalCount) * 100 : 0
|
||||
|
||||
const handleDismiss = async () => {
|
||||
setDismissed(true)
|
||||
try {
|
||||
await dismissOnboarding()
|
||||
} catch {
|
||||
// Already hidden locally
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="glass-card overflow-hidden fade-in" style={{ animationDelay: '150ms' }}>
|
||||
{/* Progress bar */}
|
||||
<div className="h-1 w-full bg-[rgba(255,255,255,0.04)]">
|
||||
<div
|
||||
className="h-full bg-gradient-brand transition-all duration-500 ease-out"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<p className="font-label text-[0.625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||
Getting Started
|
||||
</p>
|
||||
<p className="text-sm text-foreground mt-0.5">
|
||||
{isAllDone ? (
|
||||
<span className="text-gradient-brand font-semibold">You're all set!</span>
|
||||
) : (
|
||||
<span>
|
||||
<span className="text-gradient-brand font-semibold">{completedCount}</span>
|
||||
{' '}of {totalCount} complete
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="rounded-md p-1 text-muted-foreground hover:text-foreground hover:bg-[rgba(255,255,255,0.04)] transition-colors"
|
||||
aria-label="Dismiss onboarding checklist"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Checklist items */}
|
||||
<ul className="space-y-1">
|
||||
{items.map((item) => {
|
||||
const done = status[item.key]
|
||||
return (
|
||||
<li key={item.key}>
|
||||
<button
|
||||
onClick={() => !done && navigate(item.path)}
|
||||
disabled={done}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors text-left',
|
||||
done
|
||||
? 'cursor-default'
|
||||
: 'hover:bg-[rgba(255,255,255,0.04)]'
|
||||
)}
|
||||
>
|
||||
{/* Checkbox */}
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-5 w-5 shrink-0 items-center justify-center rounded-md border transition-colors',
|
||||
done
|
||||
? 'bg-gradient-brand border-transparent'
|
||||
: 'border-border'
|
||||
)}
|
||||
>
|
||||
{done && <Check size={12} className="text-[#101114]" />}
|
||||
</span>
|
||||
|
||||
{/* Label */}
|
||||
<span
|
||||
className={cn(
|
||||
'flex-1',
|
||||
done
|
||||
? 'text-muted-foreground line-through'
|
||||
: 'text-foreground'
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
|
||||
{/* Arrow for incomplete items */}
|
||||
{!done && (
|
||||
<ChevronRight size={14} className="text-muted-foreground shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user