insignia
back to lab

File Tree

A recursive file tree component with collapsible folders, file type icons, and interactive schema input. Inspired by interview qn

Use 2 spaces or tabs for indentation to create nested folders.

Preview
src
app
layout.tsx
page.tsx
globals.css
components
package.json
tsconfig.json
README.md

Source Code

import { useState } from "react";

// Types
interface FileTreeNode {
  name: string;
  type: "file" | "folder";
  children?: FileTreeNode[];
}

// Returns emoji icon based on file extension
function getFileIcon(filename: string): string {
  const ext = filename.split(".").pop()?.toLowerCase();
  const iconMap: Record<string, string> = {
    json: "📋", ts: "🔷", tsx: "🔷", js: "🟨", jsx: "🟨", 
    css: "🎨", scss: "🎨", md: "📝", mdx: "📝",
    png: "🖼️", jpg: "🖼️", svg: "🖼️", html: "🌐",
    env: "⚙️", config: "⚙️",
  };
  return iconMap[ext || ""] || "📄";
}

// Parses indentation-based text to tree structure
function parseSchemaToTree(schema: string): FileTreeNode[] {
  const lines = schema.split("\n").filter((line) => line.trim());
  const root: FileTreeNode[] = [];
  const stack: { node: FileTreeNode; indent: number }[] = [];

  for (const line of lines) {
    const match = line.match(/^(\s*)/);
    const indent = match ? match[1].replace(/\t/g, "  ").length : 0;
    const name = line.trim();
    if (!name) continue;

    const newNode: FileTreeNode = { name, type: "file" };

    while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
      stack.pop();
    }

    if (stack.length === 0) {
      root.push(newNode);
    } else {
      const parent = stack[stack.length - 1].node;
      parent.type = "folder";
      if (!parent.children) parent.children = [];
      parent.children.push(newNode);
    }

    stack.push({ node: newNode, indent });
  }
  return root;
}

// Recursive component - renders itself for children
function FileTreeNode({ node }: { node: FileTreeNode }) {
  const [isOpen, setIsOpen] = useState(false);
  const isFolder = node.type === "folder";

  return (
    <div style={{ marginLeft: 20 }}>
      <div
        onClick={() => isFolder && setIsOpen(!isOpen)}
        style={{ 
          cursor: isFolder ? "pointer" : "default",
          padding: "4px 8px",
          display: "flex",
          alignItems: "center",
          gap: "6px",
        }}
      >
        {isFolder ? (isOpen ? "📂" : "📁") : getFileIcon(node.name)}
        <span>{node.name}</span>
      </div>
      
      {isFolder && isOpen && node.children?.map((child, i) => (
        <FileTreeNode key={`${child.name}-${i}`} node={child} />
      ))}
    </div>
  );
}

// Main component
export default function FileTree({ data }: { data: FileTreeNode[] }) {
  return (
    <div>
      {data.map((node, i) => (
        <FileTreeNode key={`${node.name}-${i}`} node={node} />
      ))}
    </div>
  );
}

export { parseSchemaToTree, type FileTreeNode };