feat: roll out illustrative empty states across 8 pages with 2 new guide entries

- TreeLibraryPage: split empty state into no-flows (illustration + CTA) vs no-filter-results
- MyAnalyticsPage/TeamAnalyticsPage: add zero-sessions empty state with illustration
- SessionHistoryPage: split into no-sessions (illustration) vs no-filter-results
- StepLibraryBrowser: illustrative empty state when no steps exist
- ScriptTemplateList: replace plain empty state with ScriptIllustration
- MySharesPage: replace icon-based empty state with ShareIllustration
- IntegrationsPage: add IntegrationIllustration above setup form
- Add script-templates and psa-setup guides to guides data
- Add EmptyState vitest tests (7 tests)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-17 01:21:11 -04:00
parent 85d1ed8028
commit dfdc6cae9c
10 changed files with 324 additions and 43 deletions

View File

@@ -12,6 +12,7 @@ import {
} from 'recharts'
import { Spinner } from '@/components/common/Spinner'
import { EmptyState } from '@/components/common/EmptyState'
import { AnalyticsIllustration } from '@/components/common/EmptyStateIllustrations'
import { analyticsApi } from '@/api'
import { usePermissions } from '@/hooks/usePermissions'
import type { PersonalAnalyticsResponse, AnalyticsPeriod } from '@/types'
@@ -57,6 +58,7 @@ export default function MyAnalyticsPage() {
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<EmptyState
illustration={<AnalyticsIllustration />}
title="Analytics unavailable"
description="Failed to load analytics data. Please try again."
/>
@@ -64,6 +66,27 @@ export default function MyAnalyticsPage() {
)
}
if (data.summary.total_sessions === 0) {
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<EmptyState
illustration={<AnalyticsIllustration />}
title="Track your troubleshooting performance"
description="Analytics show resolution times, common paths, and team efficiency. Data appears automatically as you complete sessions."
action={
<Link
to="/trees"
className="inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-5 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all"
>
Run Your First Session
</Link>
}
learnMoreLink="/guides/analytics"
/>
</div>
)
}
const { summary, time_series, top_flows } = data
const outcomeBreakdown = summary.outcome_breakdown

View File

@@ -1,10 +1,11 @@
import { useState, useEffect, useCallback } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Globe, Users, Copy, Check, Link2, ExternalLink, Trash2, ArrowLeft } from 'lucide-react'
import { Globe, Users, Copy, Check, ExternalLink, Trash2, ArrowLeft } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { Button } from '@/components/ui/Button'
import { Spinner } from '@/components/common/Spinner'
import { EmptyState } from '@/components/common/EmptyState'
import { ShareIllustration } from '@/components/common/EmptyStateIllustrations'
import { ConfirmDialog } from '@/components/common/ConfirmDialog'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
@@ -142,18 +143,17 @@ export default function MySharesPage() {
{/* Empty state */}
{shares.length === 0 ? (
<div className="bg-card border border-border rounded-xl">
<EmptyState
icon={<Link2 className="h-12 w-12" />}
title="No shared sessions"
description="Share a session from the session detail page to create a link"
action={
<Button onClick={() => navigate('/sessions')}>
Go to Sessions
</Button>
}
/>
</div>
<EmptyState
illustration={<ShareIllustration />}
title="Share session results with your team"
description="Create shareable links to completed sessions for knowledge sharing and client communication."
action={
<Button onClick={() => navigate('/sessions')}>
View Sessions
</Button>
}
learnMoreLink="/guides/sharing-exports"
/>
) : (
<div className="space-y-4">
{shares.map((share) => {

View File

@@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useCallback } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import { PageMeta } from '@/components/common/PageMeta'
import { sessionsApi } from '@/api/sessions'
import { treesApi } from '@/api/trees'
@@ -9,6 +9,7 @@ import { SessionFilters } from '@/components/session/SessionFilters'
import type { SessionFilterState } from '@/components/session/SessionFilters'
import { Spinner } from '@/components/common/Spinner'
import { EmptyState } from '@/components/common/EmptyState'
import { SessionIllustration } from '@/components/common/EmptyStateIllustrations'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
import { getSessionResumePath } from '@/lib/routing'
@@ -259,19 +260,32 @@ export function SessionHistoryPage() {
<Spinner />
</div>
) : sessions.length === 0 ? (
<EmptyState
title="No sessions found"
description={filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from
? "Try adjusting your filters"
: "Complete a flow to see it here"}
action={
(filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from) ? (
(filters.ticketNumber || filters.clientName || filters.treeName || filters.dateRange?.from) ? (
<EmptyState
title="No sessions match your filters"
description="Try adjusting your search or filters."
action={
<button onClick={handleClearFilters} className="text-foreground hover:underline text-sm">
Clear all filters
</button>
) : undefined
}
/>
}
/>
) : (
<EmptyState
illustration={<SessionIllustration />}
title="Your session history will appear here"
description="Every troubleshooting session is recorded with decisions, timing, and outcomes — ready for export or review."
action={
<Link
to="/trees"
className="inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-5 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all"
>
Start a Session
</Link>
}
learnMoreLink="/guides/sessions"
/>
)
) : (
<>
<div className="space-y-4">

View File

@@ -12,6 +12,7 @@ import {
} from 'recharts'
import { Spinner } from '@/components/common/Spinner'
import { EmptyState } from '@/components/common/EmptyState'
import { AnalyticsIllustration } from '@/components/common/EmptyStateIllustrations'
import { analyticsApi } from '@/api'
import { usePermissions } from '@/hooks/usePermissions'
import { toast } from '@/lib/toast'
@@ -70,6 +71,7 @@ export default function TeamAnalyticsPage() {
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<EmptyState
illustration={<AnalyticsIllustration />}
title="Analytics unavailable"
description="Failed to load analytics data. Please try again."
/>
@@ -77,6 +79,27 @@ export default function TeamAnalyticsPage() {
)
}
if (data.summary.total_sessions === 0) {
return (
<div className="container mx-auto px-4 py-6 sm:px-6 sm:py-8">
<EmptyState
illustration={<AnalyticsIllustration />}
title="Track your troubleshooting performance"
description="Analytics show resolution times, common paths, and team efficiency. Data appears automatically as you complete sessions."
action={
<Link
to="/trees"
className="inline-flex items-center gap-2 rounded-[10px] bg-gradient-brand px-5 py-2.5 text-sm font-semibold text-[#101114] shadow-lg shadow-primary/20 hover:opacity-90 active:scale-[0.97] transition-all"
>
Run Your First Session
</Link>
}
learnMoreLink="/guides/analytics"
/>
</div>
)
}
const { summary, time_series, top_flows, top_engineers } = data
return (

View File

@@ -3,6 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'
import { X, RotateCcw, Play, FileUp } from 'lucide-react'
import { PageMeta } from '@/components/common/PageMeta'
import { Button } from '@/components/ui/Button'
import { FlowIllustration } from '@/components/common/EmptyStateIllustrations'
import { treesApi } from '@/api/trees'
import { categoriesApi } from '@/api/categories'
import { foldersApi } from '@/api/folders'
@@ -496,14 +497,29 @@ export function TreeLibraryPage() {
<Spinner />
</div>
) : trees.length === 0 ? (
<EmptyState
title="No flows found"
description={
(searchQuery || hasActiveFilters)
? 'Try adjusting your filters.'
: 'Create your first flow to get started.'
}
/>
(searchQuery || hasActiveFilters) ? (
<EmptyState
title="No flows match your filters"
description="Try adjusting your search or filters."
action={
<Button variant="secondary" onClick={clearAllFilters}>
Clear Filters
</Button>
}
/>
) : (
<EmptyState
illustration={<FlowIllustration />}
title="Build your first troubleshooting flow"
description="Flows guide your team through proven resolution paths, capturing every decision along the way."
action={
canCreateTrees ? (
<CreateFlowDropdown aiEnabled={aiEnabled} label="Create a Flow" />
) : undefined
}
learnMoreLink="/guides/creating-flows"
/>
)
) : (
<>
{treeLibraryView === 'grid' && (

View File

@@ -1,6 +1,8 @@
import { useEffect, useState } from 'react'
import { Plug, CheckCircle2, AlertCircle, Loader2, Pencil, Trash2, Shield, History, Ticket, Users, Zap, Save } from 'lucide-react'
import { analytics } from '@/lib/analytics'
import { EmptyState } from '@/components/common/EmptyState'
import { IntegrationIllustration } from '@/components/common/EmptyStateIllustrations'
import { PageMeta } from '@/components/common/PageMeta'
import { integrationsApi } from '@/api/integrations'
import type { PsaConnectionResponse, PsaConnectionCreate, PsaConnectionUpdate, PsaConnectionTestResponse } from '@/types'
@@ -254,6 +256,18 @@ export function IntegrationsPage() {
{/* Connection Tab */}
{activeTab === 'connection' && (
<div className="max-w-3xl">
{/* Illustrative empty state when no connection exists */}
{mode === 'setup' && (
<div className="mb-6">
<EmptyState
illustration={<IntegrationIllustration />}
title="Connect your PSA for seamless workflows"
description="Link ConnectWise or other PSA tools to pull ticket context into sessions and push documentation back automatically."
learnMoreLink="/guides/psa-setup"
/>
</div>
)}
{/* Setup / Edit Form */}
{(mode === 'setup' || mode === 'edit') && (
<div className="glass-card-static p-6">