/** * SiteHierarchySection Component * * Tree view visualization of analysed pages showing parent-child relationships * and site structure hierarchy. * * Features: * - Expandable tree nodes * - Visual depth indicators * - Analysis status per node * - Element counts * * @component * @layer Presentation */ import { useState, useMemo } from 'react'; import { ChevronRight, ChevronDown, Globe, FileText, ExternalLink } from 'lucide-react'; import type { DiscoveredPage } from '@domain/entities'; import { AnalysisStatus } from '@domain/entities'; import { ANALYSIS_STATUS_CONFIG } from '@domain/constants'; interface SiteHierarchySectionProps { pages: DiscoveredPage[]; } /** * Build tree structure from flat page list */ function buildTree(pages: DiscoveredPage[]): TreeNode[] { // Create map of pages by ID const pageMap = new Map(); pages.forEach((page) => { pageMap.set(page.id, page); }); // Find root pages (no parent) const rootPages = pages.filter((page) => !page.parentPageId); // Build tree recursively function buildNode(page: DiscoveredPage): TreeNode { const children = pages .filter((p) => p.parentPageId === page.id) .map((child) => buildNode(child)) .sort((a, b) => a.page.url.localeCompare(b.page.url)); return { page, children, }; } return rootPages.map((page) => buildNode(page)).sort((a, b) => a.page.depth - b.page.depth); } /** * Tree Node Interface */ interface TreeNode { page: DiscoveredPage; children: TreeNode[]; } /** * Analysis Status Badge */ function AnalysisStatusBadge({ status }: { status: AnalysisStatus }) { const config = ANALYSIS_STATUS_CONFIG[status]; const colorClasses: Record = { green: 'bg-green-100 text-green-800 border-green-200', blue: 'bg-blue-100 text-blue-800 border-blue-200', yellow: 'bg-yellow-100 text-yellow-800 border-yellow-200', orange: 'bg-orange-100 text-orange-800 border-orange-200', red: 'bg-red-100 text-red-800 border-red-200', gray: 'bg-gray-100 text-gray-700 border-gray-200', }; // Extract color name from config.color (e.g., "text-green-600" -> "green") const colorName = config.color.split('-')[1] || 'gray'; return ( {config.label} ); } /** * TreeNode Component (Recursive) */ function TreeNodeComponent({ node, depth = 0 }: { node: TreeNode; depth?: number }) { const [isExpanded, setIsExpanded] = useState(depth < 2); // Auto-expand first 2 levels const hasChildren = node.children.length > 0; // Extract path from URL const urlPath = node.page.url.replace(/^https?:\/\/[^/]+/, '') || '/'; return (
hasChildren && setIsExpanded(!isExpanded)} > {/* Expand/Collapse Icon */}
{hasChildren ? ( isExpanded ? ( ) : ( ) ) : null}
{/* Page Icon */}
{depth === 0 ? ( ) : ( )}
{/* URL Path */} e.stopPropagation()} > {urlPath} {/* Element Count */} {node.page.elementCount !== undefined && ( {node.page.elementCount} elements )} {/* Analysis Status */} {/* Depth Badge */} L{node.page.depth}
{/* Children */} {hasChildren && isExpanded && (
{node.children.map((child) => ( ))}
)}
); } /** * SiteHierarchySection Component */ export function SiteHierarchySection({ pages }: SiteHierarchySectionProps) { // Build tree structure const tree = useMemo(() => buildTree(pages), [pages]); // Calculate stats const stats = useMemo(() => { const depthCounts: Record = {}; let maxDepth = 0; pages.forEach((page) => { depthCounts[page.depth] = (depthCounts[page.depth] || 0) + 1; maxDepth = Math.max(maxDepth, page.depth); }); return { depthCounts, maxDepth }; }, [pages]); return (
{/* Header with Stats */}

Site Hierarchy

Tree view of analysed pages (max depth: {stats.maxDepth})

{/* Depth Distribution */}
{Object.entries(stats.depthCounts) .sort(([a], [b]) => Number(a) - Number(b)) .map(([depth, count]) => (
L{depth}:{' '} {count}
))}
{/* Tree View */}
{tree.length === 0 ? (

No pages to display

) : (
{tree.map((node) => ( ))}
)}
{/* Legend */}
Legend:
Root page
Child page
L0, L1, L2... Depth level
); }