← back to Letsbegin

components/DirectoryTree.tsx

121 lines

'use client'

import { useState } from 'react'

interface DirectoryNode {
  name: string
  path: string
  type: 'file' | 'directory'
  children?: DirectoryNode[]
}

interface Props {
  node: DirectoryNode
  depth: number
  selectedPath: string
  onSelect?: (path: string) => void
}

export default function DirectoryTree({ node, depth, selectedPath, onSelect }: Props) {
  const [isExpanded, setIsExpanded] = useState(depth < 2)

  const isDirectory = node.type === 'directory'
  const hasChildren = isDirectory && node.children && node.children.length > 0
  const isSelected = node.path === selectedPath

  const fileIcon = getFileIcon(node.name, isDirectory)

  return (
    <div>
      <div
        className={`tree-item ${isSelected ? 'selected' : ''}`}
        style={{ paddingLeft: `${depth * 16 + 8}px` }}
        onClick={() => {
          if (isDirectory) {
            setIsExpanded(!isExpanded)
          }
          onSelect?.(node.path)
        }}
      >
        {/* Chevron */}
        <div className={`tree-chevron ${isExpanded && hasChildren ? 'expanded' : ''}`}>
          {hasChildren ? (
            <svg className="w-4 h-4 text-[var(--foreground-tertiary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
            </svg>
          ) : (
            <span className="w-4" />
          )}
        </div>

        {/* Icon */}
        <span className="text-sm">{fileIcon}</span>

        {/* Name */}
        <span className="text-sm truncate">{node.name}</span>
      </div>

      {/* Children */}
      {isExpanded && hasChildren && (
        <div>
          {node.children!
            .sort((a, b) => {
              // Directories first, then files
              if (a.type !== b.type) {
                return a.type === 'directory' ? -1 : 1
              }
              return a.name.localeCompare(b.name)
            })
            .map((child) => (
              <DirectoryTree
                key={child.path}
                node={child}
                depth={depth + 1}
                selectedPath={selectedPath}
                onSelect={onSelect}
              />
            ))}
        </div>
      )}
    </div>
  )
}

function getFileIcon(name: string, isDirectory: boolean): string {
  if (isDirectory) {
    const specialFolders: Record<string, string> = {
      'node_modules': '📦',
      '.git': '🔧',
      'app': '📱',
      'components': '🧩',
      'lib': '📚',
      'api': '🔌',
      'public': '🌐',
      'styles': '🎨',
      'tests': '🧪',
      '__tests__': '🧪',
    }
    return specialFolders[name] || '📁'
  }

  const ext = name.split('.').pop()?.toLowerCase() || ''
  const icons: Record<string, string> = {
    'ts': '📘',
    'tsx': '⚛️',
    'js': '📒',
    'jsx': '⚛️',
    'json': '📋',
    'md': '📝',
    'css': '🎨',
    'scss': '🎨',
    'html': '🌐',
    'env': '🔐',
    'gitignore': '🔧',
    'lock': '🔒',
    'png': '🖼️',
    'jpg': '🖼️',
    'svg': '🎭',
  }
  return icons[ext] || '📄'
}