diff --git a/src/components/FileTree.jsx b/src/components/FileTree.jsx deleted file mode 100644 index cf3d1e1..0000000 --- a/src/components/FileTree.jsx +++ /dev/null @@ -1,729 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ScrollArea } from './ui/scroll-area'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { - Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye, Search, X, - ChevronRight, - FileJson, FileType, FileSpreadsheet, FileArchive, - Hash, Braces, Terminal, Database, Globe, Palette, Music2, Video, Archive, - Lock, Shield, Settings, Image, BookOpen, Cpu, Box, Gem, Coffee, - Flame, Hexagon, FileCode2, Code2, Cog, FileWarning, Binary, SquareFunction, - Scroll, FlaskConical, NotebookPen, FileCheck, Workflow, Blocks -} from 'lucide-react'; -import { cn } from '../lib/utils'; -import ImageViewer from './ImageViewer'; -import { api } from '../utils/api'; - -// ─── File Icon Registry ────────────────────────────────────────────── -// Maps file extensions (and special filenames) to { icon, colorClass } pairs. -// Uses lucide-react icons mapped semantically to file types. - -const ICON_SIZE = 'w-4 h-4 flex-shrink-0'; - -const FILE_ICON_MAP = { - // ── JavaScript / TypeScript ── - js: { icon: FileCode, color: 'text-yellow-500' }, - jsx: { icon: FileCode, color: 'text-yellow-500' }, - mjs: { icon: FileCode, color: 'text-yellow-500' }, - cjs: { icon: FileCode, color: 'text-yellow-500' }, - ts: { icon: FileCode2, color: 'text-blue-500' }, - tsx: { icon: FileCode2, color: 'text-blue-500' }, - mts: { icon: FileCode2, color: 'text-blue-500' }, - - // ── Python ── - py: { icon: Code2, color: 'text-emerald-500' }, - pyw: { icon: Code2, color: 'text-emerald-500' }, - pyi: { icon: Code2, color: 'text-emerald-400' }, - ipynb:{ icon: NotebookPen, color: 'text-orange-500' }, - - // ── Rust ── - rs: { icon: Cog, color: 'text-orange-600' }, - toml: { icon: Settings, color: 'text-gray-500' }, - - // ── Go ── - go: { icon: Hexagon, color: 'text-cyan-500' }, - - // ── Ruby ── - rb: { icon: Gem, color: 'text-red-500' }, - erb: { icon: Gem, color: 'text-red-400' }, - - // ── PHP ── - php: { icon: Blocks, color: 'text-violet-500' }, - - // ── Java / Kotlin ── - java: { icon: Coffee, color: 'text-red-600' }, - jar: { icon: Coffee, color: 'text-red-500' }, - kt: { icon: Hexagon, color: 'text-violet-500' }, - kts: { icon: Hexagon, color: 'text-violet-400' }, - - // ── C / C++ ── - c: { icon: Cpu, color: 'text-blue-600' }, - h: { icon: Cpu, color: 'text-blue-400' }, - cpp: { icon: Cpu, color: 'text-blue-700' }, - hpp: { icon: Cpu, color: 'text-blue-500' }, - cc: { icon: Cpu, color: 'text-blue-700' }, - - // ── C# ── - cs: { icon: Hexagon, color: 'text-purple-600' }, - - // ── Swift ── - swift:{ icon: Flame, color: 'text-orange-500' }, - - // ── Lua ── - lua: { icon: SquareFunction, color: 'text-blue-500' }, - - // ── R ── - r: { icon: FlaskConical, color: 'text-blue-600' }, - - // ── Web ── - html: { icon: Globe, color: 'text-orange-600' }, - htm: { icon: Globe, color: 'text-orange-600' }, - css: { icon: Hash, color: 'text-blue-500' }, - scss: { icon: Hash, color: 'text-pink-500' }, - sass: { icon: Hash, color: 'text-pink-400' }, - less: { icon: Hash, color: 'text-indigo-500' }, - vue: { icon: FileCode2, color: 'text-emerald-500' }, - svelte:{ icon: FileCode2, color: 'text-orange-500' }, - - // ── Data / Config ── - json: { icon: Braces, color: 'text-yellow-600' }, - jsonc:{ icon: Braces, color: 'text-yellow-500' }, - json5:{ icon: Braces, color: 'text-yellow-500' }, - yaml: { icon: Settings, color: 'text-purple-400' }, - yml: { icon: Settings, color: 'text-purple-400' }, - xml: { icon: FileCode, color: 'text-orange-500' }, - csv: { icon: FileSpreadsheet, color: 'text-green-600' }, - tsv: { icon: FileSpreadsheet, color: 'text-green-500' }, - sql: { icon: Database, color: 'text-blue-500' }, - graphql:{ icon: Workflow, color: 'text-pink-500' }, - gql: { icon: Workflow, color: 'text-pink-500' }, - proto:{ icon: Box, color: 'text-green-500' }, - env: { icon: Shield, color: 'text-yellow-600' }, - - // ── Documents ── - md: { icon: BookOpen, color: 'text-blue-500' }, - mdx: { icon: BookOpen, color: 'text-blue-400' }, - txt: { icon: FileText, color: 'text-gray-500' }, - doc: { icon: FileText, color: 'text-blue-600' }, - docx: { icon: FileText, color: 'text-blue-600' }, - pdf: { icon: FileCheck, color: 'text-red-600' }, - rtf: { icon: FileText, color: 'text-gray-500' }, - tex: { icon: Scroll, color: 'text-teal-600' }, - rst: { icon: FileText, color: 'text-gray-400' }, - - // ── Shell / Scripts ── - sh: { icon: Terminal, color: 'text-green-500' }, - bash: { icon: Terminal, color: 'text-green-500' }, - zsh: { icon: Terminal, color: 'text-green-400' }, - fish: { icon: Terminal, color: 'text-green-400' }, - ps1: { icon: Terminal, color: 'text-blue-400' }, - bat: { icon: Terminal, color: 'text-gray-500' }, - cmd: { icon: Terminal, color: 'text-gray-500' }, - - // ── Images ── - png: { icon: Image, color: 'text-purple-500' }, - jpg: { icon: Image, color: 'text-purple-500' }, - jpeg: { icon: Image, color: 'text-purple-500' }, - gif: { icon: Image, color: 'text-purple-400' }, - webp: { icon: Image, color: 'text-purple-400' }, - ico: { icon: Image, color: 'text-purple-400' }, - bmp: { icon: Image, color: 'text-purple-400' }, - tiff: { icon: Image, color: 'text-purple-400' }, - svg: { icon: Palette, color: 'text-amber-500' }, - - // ── Audio ── - mp3: { icon: Music2, color: 'text-pink-500' }, - wav: { icon: Music2, color: 'text-pink-500' }, - ogg: { icon: Music2, color: 'text-pink-400' }, - flac: { icon: Music2, color: 'text-pink-400' }, - aac: { icon: Music2, color: 'text-pink-400' }, - m4a: { icon: Music2, color: 'text-pink-400' }, - - // ── Video ── - mp4: { icon: Video, color: 'text-rose-500' }, - mov: { icon: Video, color: 'text-rose-500' }, - avi: { icon: Video, color: 'text-rose-500' }, - webm: { icon: Video, color: 'text-rose-400' }, - mkv: { icon: Video, color: 'text-rose-400' }, - - // ── Fonts ── - ttf: { icon: FileType, color: 'text-red-500' }, - otf: { icon: FileType, color: 'text-red-500' }, - woff: { icon: FileType, color: 'text-red-400' }, - woff2:{ icon: FileType, color: 'text-red-400' }, - eot: { icon: FileType, color: 'text-red-400' }, - - // ── Archives ── - zip: { icon: Archive, color: 'text-amber-600' }, - tar: { icon: Archive, color: 'text-amber-600' }, - gz: { icon: Archive, color: 'text-amber-600' }, - bz2: { icon: Archive, color: 'text-amber-600' }, - rar: { icon: Archive, color: 'text-amber-500' }, - '7z': { icon: Archive, color: 'text-amber-500' }, - - // ── Lock files ── - lock: { icon: Lock, color: 'text-gray-500' }, - - // ── Binary / Executable ── - exe: { icon: Binary, color: 'text-gray-500' }, - bin: { icon: Binary, color: 'text-gray-500' }, - dll: { icon: Binary, color: 'text-gray-400' }, - so: { icon: Binary, color: 'text-gray-400' }, - dylib:{ icon: Binary, color: 'text-gray-400' }, - wasm: { icon: Binary, color: 'text-purple-500' }, - - // ── Misc config ── - ini: { icon: Settings, color: 'text-gray-500' }, - cfg: { icon: Settings, color: 'text-gray-500' }, - conf: { icon: Settings, color: 'text-gray-500' }, - log: { icon: Scroll, color: 'text-gray-400' }, - map: { icon: File, color: 'text-gray-400' }, -}; - -// Special full-filename matches (highest priority) -const FILENAME_ICON_MAP = { - 'Dockerfile': { icon: Box, color: 'text-blue-500' }, - 'docker-compose.yml': { icon: Box, color: 'text-blue-500' }, - 'docker-compose.yaml': { icon: Box, color: 'text-blue-500' }, - '.dockerignore': { icon: Box, color: 'text-gray-500' }, - '.gitignore': { icon: Settings, color: 'text-gray-500' }, - '.gitmodules': { icon: Settings, color: 'text-gray-500' }, - '.gitattributes': { icon: Settings, color: 'text-gray-500' }, - '.editorconfig': { icon: Settings, color: 'text-gray-500' }, - '.prettierrc': { icon: Settings, color: 'text-pink-400' }, - '.prettierignore': { icon: Settings, color: 'text-gray-500' }, - '.eslintrc': { icon: Settings, color: 'text-violet-500' }, - '.eslintrc.js': { icon: Settings, color: 'text-violet-500' }, - '.eslintrc.json': { icon: Settings, color: 'text-violet-500' }, - '.eslintrc.cjs': { icon: Settings, color: 'text-violet-500' }, - 'eslint.config.js': { icon: Settings, color: 'text-violet-500' }, - 'eslint.config.mjs':{ icon: Settings, color: 'text-violet-500' }, - '.env': { icon: Shield, color: 'text-yellow-600' }, - '.env.local': { icon: Shield, color: 'text-yellow-600' }, - '.env.development': { icon: Shield, color: 'text-yellow-500' }, - '.env.production': { icon: Shield, color: 'text-yellow-600' }, - '.env.example': { icon: Shield, color: 'text-yellow-400' }, - 'package.json': { icon: Braces, color: 'text-green-500' }, - 'package-lock.json':{ icon: Lock, color: 'text-gray-500' }, - 'yarn.lock': { icon: Lock, color: 'text-blue-400' }, - 'pnpm-lock.yaml': { icon: Lock, color: 'text-orange-400' }, - 'bun.lockb': { icon: Lock, color: 'text-gray-400' }, - 'Cargo.toml': { icon: Cog, color: 'text-orange-600' }, - 'Cargo.lock': { icon: Lock, color: 'text-orange-400' }, - 'Gemfile': { icon: Gem, color: 'text-red-500' }, - 'Gemfile.lock': { icon: Lock, color: 'text-red-400' }, - 'Makefile': { icon: Terminal, color: 'text-gray-500' }, - 'CMakeLists.txt': { icon: Cog, color: 'text-blue-500' }, - 'tsconfig.json': { icon: Braces, color: 'text-blue-500' }, - 'jsconfig.json': { icon: Braces, color: 'text-yellow-500' }, - 'vite.config.ts': { icon: Flame, color: 'text-purple-500' }, - 'vite.config.js': { icon: Flame, color: 'text-purple-500' }, - 'webpack.config.js':{ icon: Cog, color: 'text-blue-500' }, - 'tailwind.config.js':{ icon: Hash, color: 'text-cyan-500' }, - 'tailwind.config.ts':{ icon: Hash, color: 'text-cyan-500' }, - 'postcss.config.js':{ icon: Cog, color: 'text-red-400' }, - 'babel.config.js': { icon: Settings, color: 'text-yellow-500' }, - '.babelrc': { icon: Settings, color: 'text-yellow-500' }, - 'README.md': { icon: BookOpen, color: 'text-blue-500' }, - 'LICENSE': { icon: FileCheck, color: 'text-gray-500' }, - 'LICENSE.md': { icon: FileCheck, color: 'text-gray-500' }, - 'CHANGELOG.md': { icon: Scroll, color: 'text-blue-400' }, - 'requirements.txt': { icon: FileText, color: 'text-emerald-400' }, - 'go.mod': { icon: Hexagon, color: 'text-cyan-500' }, - 'go.sum': { icon: Lock, color: 'text-cyan-400' }, -}; - -function getFileIconData(filename) { - // 1. Exact filename match - if (FILENAME_ICON_MAP[filename]) { - return FILENAME_ICON_MAP[filename]; - } - - // 2. Check for .env prefix pattern - if (filename.startsWith('.env')) { - return { icon: Shield, color: 'text-yellow-600' }; - } - - // 3. Extension-based lookup - const ext = filename.split('.').pop()?.toLowerCase(); - if (ext && FILE_ICON_MAP[ext]) { - return FILE_ICON_MAP[ext]; - } - - // 4. Fallback - return { icon: File, color: 'text-muted-foreground' }; -} - - -// ─── Component ─────────────────────────────────────────────────────── - -function FileTree({ selectedProject, onFileOpen }) { - const { t } = useTranslation(); - const [files, setFiles] = useState([]); - const [loading, setLoading] = useState(false); - const [expandedDirs, setExpandedDirs] = useState(new Set()); - const [selectedImage, setSelectedImage] = useState(null); - const [viewMode, setViewMode] = useState('detailed'); - const [searchQuery, setSearchQuery] = useState(''); - const [filteredFiles, setFilteredFiles] = useState([]); - - useEffect(() => { - if (selectedProject) { - fetchFiles(); - } - }, [selectedProject]); - - useEffect(() => { - const savedViewMode = localStorage.getItem('file-tree-view-mode'); - if (savedViewMode && ['simple', 'detailed', 'compact'].includes(savedViewMode)) { - setViewMode(savedViewMode); - } - }, []); - - useEffect(() => { - if (!searchQuery.trim()) { - setFilteredFiles(files); - } else { - const filtered = filterFiles(files, searchQuery.toLowerCase()); - setFilteredFiles(filtered); - - const expandMatches = (items) => { - items.forEach(item => { - if (item.type === 'directory' && item.children && item.children.length > 0) { - setExpandedDirs(prev => new Set(prev.add(item.path))); - expandMatches(item.children); - } - }); - }; - expandMatches(filtered); - } - }, [files, searchQuery]); - - const filterFiles = (items, query) => { - return items.reduce((filtered, item) => { - const matchesName = item.name.toLowerCase().includes(query); - let filteredChildren = []; - - if (item.type === 'directory' && item.children) { - filteredChildren = filterFiles(item.children, query); - } - - if (matchesName || filteredChildren.length > 0) { - filtered.push({ - ...item, - children: filteredChildren - }); - } - - return filtered; - }, []); - }; - - const fetchFiles = async () => { - setLoading(true); - try { - const response = await api.getFiles(selectedProject.name); - - if (!response.ok) { - const errorText = await response.text(); - console.error('❌ File fetch failed:', response.status, errorText); - setFiles([]); - return; - } - - const data = await response.json(); - setFiles(data); - } catch (error) { - console.error('❌ Error fetching files:', error); - setFiles([]); - } finally { - setLoading(false); - } - }; - - const toggleDirectory = (path) => { - const newExpanded = new Set(expandedDirs); - if (newExpanded.has(path)) { - newExpanded.delete(path); - } else { - newExpanded.add(path); - } - setExpandedDirs(newExpanded); - }; - - const changeViewMode = (mode) => { - setViewMode(mode); - localStorage.setItem('file-tree-view-mode', mode); - }; - - const formatFileSize = (bytes) => { - if (!bytes || bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; - }; - - const formatRelativeTime = (date) => { - if (!date) return '-'; - const now = new Date(); - const past = new Date(date); - const diffInSeconds = Math.floor((now - past) / 1000); - - if (diffInSeconds < 60) return t('fileTree.justNow'); - if (diffInSeconds < 3600) return t('fileTree.minAgo', { count: Math.floor(diffInSeconds / 60) }); - if (diffInSeconds < 86400) return t('fileTree.hoursAgo', { count: Math.floor(diffInSeconds / 3600) }); - if (diffInSeconds < 2592000) return t('fileTree.daysAgo', { count: Math.floor(diffInSeconds / 86400) }); - return past.toLocaleDateString(); - }; - - const isImageFile = (filename) => { - const ext = filename.split('.').pop()?.toLowerCase(); - const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp']; - return imageExtensions.includes(ext); - }; - - const getFileIcon = (filename) => { - const { icon: Icon, color } = getFileIconData(filename); - return ; - }; - - // ── Click handler shared across all view modes ── - const handleItemClick = (item) => { - if (item.type === 'directory') { - toggleDirectory(item.path); - } else if (isImageFile(item.name)) { - setSelectedImage({ - name: item.name, - path: item.path, - projectPath: selectedProject.path, - projectName: selectedProject.name - }); - } else if (onFileOpen) { - onFileOpen(item.path); - } - }; - - // ── Indent guide + folder/file icon rendering ── - const renderIndentGuides = (level) => { - if (level === 0) return null; - return ( -