feat: add onboarding checklist widget to dashboard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-17 01:40:32 -04:00
parent dfdc6cae9c
commit b72eb56b7f
3 changed files with 185 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
import { apiClient } from './client'
export interface OnboardingStatus {
created_flow: boolean
ran_session: boolean
exported_session: boolean
tried_ai_assistant: boolean
invited_teammate: boolean
connected_psa: boolean
is_team_user: boolean
dismissed: boolean
}
export async function getOnboardingStatus(): Promise<OnboardingStatus> {
const response = await apiClient.get('/users/onboarding-status')
return response.data
}
export async function dismissOnboarding(): Promise<void> {
await apiClient.post('/users/onboarding-status/dismiss')
}

View 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>
)
}

View File

@@ -26,6 +26,7 @@ import { QuickActions } from '@/components/dashboard/QuickActions'
import { OpenSessions } from '@/components/dashboard/OpenSessions'
import { RecentActivity } from '@/components/dashboard/RecentActivity'
import { PreparedSessions } from '@/components/dashboard/PreparedSessions'
import { OnboardingChecklist } from '@/components/dashboard/OnboardingChecklist'
function timeAgo(dateStr: string): string {
const now = Date.now()
@@ -278,6 +279,9 @@ export function QuickStartPage() {
</p>
</div>
{/* Onboarding Checklist */}
<OnboardingChecklist />
{/* Row 1: Calendar + Quick Actions */}
<div className="flex gap-4" style={{ alignItems: 'stretch' }}>
<div className="flex-1 min-w-0">