feat: add onboarding checklist widget to dashboard
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
21
frontend/src/api/onboarding.ts
Normal file
21
frontend/src/api/onboarding.ts
Normal 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')
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user