Files
resolutionflow/frontend/src/hooks/useTreeValidation.ts
Michael Chihlas 996b664ca9 feat: implement My Trees, admin UI, rating modal, and bundle optimization (Issues #15, #18, #19, #31)
Frontend features:
- My Trees personal dashboard with fork tracking (Issue #15)
- Tree sharing UI with token generation and copy (Issue #16)
- Draft tree badges and validation UI (Issue #25)
- Save session as tree modal (Issue #17)
- Rate/review modal with localStorage tracking (Issue #19)
- Admin category management with drag-and-drop (Issue #18)
- Bundle size optimization with code splitting (Issue #31)

Components created:
- MyTreesPage: Personal tree organization
- AdminCategoriesPage: Category CRUD with @dnd-kit
- ShareTreeModal: Tree sharing interface
- SaveSessionAsTreeModal: Session conversion UI
- StepRatingModal: Post-session rating with stars
- StarRating: Reusable rating component
- PageLoader: Loading fallback for lazy routes
- CreateCategoryModal, EditCategoryModal: Admin modals

Bundle optimization:
- Reduced from 892 KB to 221 KB (75% reduction)
- Dynamic imports for 9 heavy pages
- Vendor chunk splitting for optimal caching
- 6 separate vendor chunks (react, markdown, utils, dnd, icons, state)

Dependencies added:
- @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities

API clients:
- stepCategories: Full CRUD for admin
- Enhanced sessions: saveAsTree endpoint
- Enhanced trees: share, fork, canPublish endpoints

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-07 23:06:46 -05:00

178 lines
5.4 KiB
TypeScript

import { useMemo } from 'react'
import type { TreeStructure, ValidationError } from '@/types'
interface ValidationResult {
canPublish: boolean
errors: ValidationError[]
warnings: ValidationError[]
}
/**
* Client-side tree validation hook
* Validates tree structure before allowing publish
*/
export function useTreeValidation(
name: string,
description: string | null,
treeStructure: TreeStructure | null
): ValidationResult {
return useMemo(() => {
const errors: ValidationError[] = []
const warnings: ValidationError[] = []
// Validate name
if (!name || name.trim().length === 0) {
errors.push({
field: 'name',
message: 'Tree name is required',
})
} else if (name.length > 255) {
errors.push({
field: 'name',
message: 'Tree name must be 255 characters or less',
})
}
// Validate tree structure exists
if (!treeStructure) {
errors.push({
field: 'tree_structure',
message: 'Tree structure is required',
})
// Can't validate further without a tree structure
return {
canPublish: errors.length === 0,
errors,
warnings,
}
}
// Validate root node
if (!treeStructure.type) {
errors.push({
field: 'tree_structure.type',
message: 'Root node must have a type',
})
}
// Validate root node content based on type
if (treeStructure.type === 'decision') {
if (!treeStructure.question || treeStructure.question.trim().length === 0) {
errors.push({
field: 'tree_structure.question',
message: 'Decision node must have a question',
})
}
if (!treeStructure.options || treeStructure.options.length === 0) {
errors.push({
field: 'tree_structure.options',
message: 'Decision node must have at least one option',
})
} else {
// Validate each option
treeStructure.options.forEach((option, index) => {
if (!option.label || option.label.trim().length === 0) {
errors.push({
field: `tree_structure.options[${index}].label`,
message: `Option ${index + 1} must have a label`,
})
}
})
}
} else if (treeStructure.type === 'action') {
if (!treeStructure.title || treeStructure.title.trim().length === 0) {
errors.push({
field: 'tree_structure.title',
message: 'Action node must have a title',
})
}
if (!treeStructure.description || treeStructure.description.trim().length === 0) {
errors.push({
field: 'tree_structure.description',
message: 'Action node must have a description',
})
}
} else if (treeStructure.type === 'solution') {
if (!treeStructure.title || treeStructure.title.trim().length === 0) {
errors.push({
field: 'tree_structure.title',
message: 'Solution node must have a title',
})
}
if (!treeStructure.description || treeStructure.description.trim().length === 0) {
errors.push({
field: 'tree_structure.description',
message: 'Solution node must have a description',
})
}
}
// Validate children recursively (basic check)
const validateChildren = (node: TreeStructure, path: string = 'tree_structure') => {
if (node.children && node.children.length > 0) {
node.children.forEach((child, index) => {
const childPath = `${path}.children[${index}]`
if (!child.type) {
errors.push({
field: `${childPath}.type`,
message: 'Child node must have a type',
})
}
// Recursively validate
if (child.type === 'decision' && (!child.question || child.question.trim().length === 0)) {
errors.push({
field: `${childPath}.question`,
message: 'Decision node must have a question',
})
}
if ((child.type === 'action' || child.type === 'solution') &&
(!child.title || child.title.trim().length === 0)) {
errors.push({
field: `${childPath}.title`,
message: `${child.type} node must have a title`,
})
}
if (child.children) {
validateChildren(child, childPath)
}
})
} else if (node.type === 'decision' && (!node.options || node.options.length === 0)) {
// Decision nodes without children should have had options validation above
// This is just a warning for decision nodes that might be incomplete
warnings.push({
field: path,
message: 'Decision node has no children (paths)',
})
}
}
validateChildren(treeStructure)
// Warnings
if (!description || description.trim().length === 0) {
warnings.push({
field: 'description',
message: 'Adding a description helps users understand the tree purpose',
})
}
if (treeStructure.type === 'decision' &&
treeStructure.children &&
treeStructure.children.length < 2) {
warnings.push({
field: 'tree_structure',
message: 'Tree has very few paths - consider adding more troubleshooting options',
})
}
return {
canPublish: errors.length === 0,
errors,
warnings,
}
}, [name, description, treeStructure])
}