Complete Phase 2: Frontend implementation with React + TypeScript
Frontend Features: - React 18 + Vite + TypeScript + Tailwind CSS + Zustand - JWT authentication with automatic token refresh - Tree library with search and category filtering - Full tree navigation (decision/action/solution nodes) - Session management with notes and completion - Session history with export (Markdown/Text/HTML) - ErrorBoundary for graceful error handling Backend Fixes: - CORS: Added port 5174 to allowed origins - Sessions: Fixed JSONB datetime serialization (mode='json') Documentation: - Updated PROGRESS.md with Phase 2 completion - Updated 03-DEVELOPMENT-ROADMAP.md with checked items - Added PHASE-2.5-PERSONAL-BRANCHING.md spec Seed Data: - Added backend/scripts/seed_data.py with Password Reset tree Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
168
frontend/src/pages/TreeLibraryPage.tsx
Normal file
168
frontend/src/pages/TreeLibraryPage.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { treesApi } from '@/api'
|
||||
import type { TreeListItem } from '@/types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export function TreeLibraryPage() {
|
||||
const navigate = useNavigate()
|
||||
const [trees, setTrees] = useState<TreeListItem[]>([])
|
||||
const [categories, setCategories] = useState<string[]>([])
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [selectedCategory])
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const [treesData, categoriesData] = await Promise.all([
|
||||
treesApi.list({ category: selectedCategory || undefined }),
|
||||
treesApi.categories(),
|
||||
])
|
||||
setTrees(treesData)
|
||||
setCategories(categoriesData)
|
||||
} catch (err) {
|
||||
setError('Failed to load trees')
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.trim()) {
|
||||
loadData()
|
||||
return
|
||||
}
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const results = await treesApi.search(searchQuery, selectedCategory || undefined)
|
||||
setTrees(results)
|
||||
} catch (err) {
|
||||
setError('Search failed')
|
||||
console.error(err)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStartSession = (treeId: string) => {
|
||||
navigate(`/trees/${treeId}/navigate`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground">Decision Trees</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Select a troubleshooting tree to start a new session
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search and Filter */}
|
||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row">
|
||||
<div className="flex flex-1 gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search trees..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className={cn(
|
||||
'flex-1 rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground placeholder:text-muted-foreground',
|
||||
'focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
)}
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className={cn(
|
||||
'rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary'
|
||||
)}
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<div className="mb-6 rounded-md bg-destructive/10 p-4 text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
</div>
|
||||
) : trees.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
No trees found. {searchQuery && 'Try adjusting your search.'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{trees.map((tree) => (
|
||||
<div
|
||||
key={tree.id}
|
||||
className="rounded-lg border border-border bg-card p-6 shadow-sm transition-shadow hover:shadow-md"
|
||||
>
|
||||
<div className="mb-2 flex items-start justify-between">
|
||||
<h3 className="font-semibold text-card-foreground">{tree.name}</h3>
|
||||
{tree.category && (
|
||||
<span className="rounded-full bg-secondary px-2 py-0.5 text-xs text-secondary-foreground">
|
||||
{tree.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-muted-foreground line-clamp-2">
|
||||
{tree.description || 'No description available'}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
v{tree.version} · {tree.usage_count} uses
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleStartSession(tree.id)}
|
||||
className={cn(
|
||||
'rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground',
|
||||
'hover:bg-primary/90'
|
||||
)}
|
||||
>
|
||||
Start Session
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TreeLibraryPage
|
||||
Reference in New Issue
Block a user