import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react'; import { useEditorStore } from '../../store/editorStore'; import { useCollaborationStore } from '../../store/collaborationStore'; import { ProjectFile } from '../../types/blocks'; import { FrameworkType } from '../../types'; import { File, Folder, FolderOpen, FileCode, FileJson, FileType, Trash2, Edit3, ChevronRight, ChevronDown, FilePlus, FolderPlus, Link, Unlink, Link2, Globe, X, Copy, PanelRight, Play } from 'lucide-react'; import { v4 as uuidv4 } from 'uuid'; import { getColor } from '../Collaboration/CollaboratorAvatars'; import { getSocket } from '../../hooks/useWebSocket'; interface FileTreeProps { files: ProjectFile[]; activeFileId: string | null; onFileSelect: (id: string) => void; framework: FrameworkType; expanded?: boolean; } const FILE_ICONS: Record = { html: Globe, css: FileType, js: FileCode, ts: FileCode, typescript: FileCode, json: File, xaml: FileType, csharp: FileCode, env: FileType, jsx: FileCode, tsx: FileCode, }; function getFileIcon(type: string) { return FILE_ICONS[type] || File; } function flattenFiles(files: ProjectFile[]): ProjectFile[] { const result: ProjectFile[] = []; for (const f of files) { result.push(f); if (f.children) result.push(...flattenFiles(f.children)); } return result; } function findFile(files: ProjectFile[], id: string): ProjectFile | null { for (const f of files) { if (f.id === id) return f; if (f.children) { const found = findFile(f.children, id); if (found) return found; } } return null; } // ===== LINKED FILES MODAL ===== function ManageLinksModal({ file, allFiles, onClose, onToggleLink }: { file: ProjectFile; allFiles: ProjectFile[]; onClose: () => void; onToggleLink: (fileId: string, linkedId: string) => void; }) { const flat = flattenFiles(allFiles).filter(f => f.type !== 'folder' && f.id !== file.id); const linkedSet = new Set(file.linkedFiles || []); const linked = flat.filter(f => linkedSet.has(f.id)); const unlinked = flat.filter(f => !linkedSet.has(f.id) && (f.type === 'css' || f.type === 'js')); const [tab, setTab] = useState<'linked' | 'add'>('linked'); return (
e.stopPropagation()}>

Linked Scripts & Styles

Manage files linked to {file.name}

{tab === 'linked' ? ( linked.length === 0 ? (

No linked files

Switch to "Add Files" to link CSS/JS

) : (
{linked.map(lf => (
{lf.type === 'css' ? : } {lf.name} {lf.type.toUpperCase()}
))}
) ) : ( unlinked.length === 0 ? (

No files available to link

Create CSS or JS files first

) : (
{unlinked.map(uf => (
onToggleLink(file.id, uf.id)}> {uf.type === 'css' ? : } {uf.name} {uf.type.toUpperCase()}
))}
) )}
); } // ===== LINKED FILE ROW ===== function LinkedFileRow({ file, activeFileId, onSelect, type }: { file: ProjectFile; activeFileId: string | null; onSelect: (id: string) => void; type: string }) { return (
onSelect(file.id)} > {type === 'css' ? : } {file.name} {type === 'css' ? 'CSS' : 'JS'}
); } // ===== FILE ITEM ===== interface FileItemProps { file: ProjectFile; depth: number; activeFileId: string | null; onSelect: (id: string) => void; onRename: (id: string, name: string) => void; onDelete: (id: string) => void; onAddChild: (parentId: string, isFolder: boolean) => void; allFiles: ProjectFile[]; onOpenLinksModal: (file: ProjectFile) => void; } function FileItem({ file, depth, activeFileId, onSelect, onRename, onDelete, onAddChild, allFiles, onOpenLinksModal }: FileItemProps) { const [expanded, setExpanded] = useState(true); const [editing, setEditing] = useState(false); const [editName, setEditName] = useState(file.name); const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null); const cmRef = useRef(null); const Icon = file.type === 'folder' ? (expanded ? FolderOpen : Folder) : getFileIcon(file.type); const isActive = activeFileId === file.id; const isFile = file.type !== 'folder'; const linkedIds = file.linkedFiles || []; const collaborators = useCollaborationStore((s) => s.collaborators); const connected = useCollaborationStore((s) => s.connected); const collabOnFile = useMemo(() => collaborators.filter((c) => c.activeFileId === file.id), [collaborators, file.id]); // Get linked files const linked = linkedIds.map(id => findFile(allFiles, id)).filter(Boolean) as ProjectFile[]; const css = linked.filter(f => f.type === 'css'); const js = linked.filter(f => f.type === 'js'); const hasLinks = css.length + js.length > 0; useEffect(() => { const handler = (e: MouseEvent) => { if (cmRef.current && !cmRef.current.contains(e.target as Node)) setContextMenu(null); }; if (contextMenu) document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [contextMenu]); const handleRename = () => { if (editName.trim() && editName !== file.name) onRename(file.id, editName.trim()); setEditing(false); }; const handleContextMenu = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setContextMenu({ x: e.clientX, y: e.clientY }); }; const isHtmlFile = file.type === 'html'; const isJsOrCss = file.type === 'js' || file.type === 'css'; return (
{ if (file.type === 'folder') setExpanded(!expanded); else onSelect(file.id); }} onContextMenu={handleContextMenu} > {file.type === 'folder' && ( {expanded ? : } )} {editing ? ( setEditName(e.target.value)} onBlur={handleRename} onKeyDown={(e) => { if (e.key === 'Enter') handleRename(); if (e.key === 'Escape') setEditing(false); }} onClick={(e) => e.stopPropagation()} autoFocus /> ) : ( {file.name} )} {hasLinks && isFile && ( {linkedIds.length} )} {connected && collabOnFile.length > 0 && (
{collabOnFile.slice(0, 3).map((c) => (
{c.username.charAt(0).toUpperCase()}
))}
)}
{file.type === 'folder' && ( <> )} {isFile && ( )}
{/* Linked files shown as nested indented children */} {hasLinks && isFile && (
{css.map(lf => ( ))} {js.map(lf => ( ))}
)} {/* Folder children */} {file.type === 'folder' && expanded && file.children && (
{file.children.map((child: ProjectFile) => ( ))}
)} {/* Context Menu */} {contextMenu && (
{isHtmlFile && ( <>
)} {isJsOrCss && ( <>
)}
)}
); } // ===== MAIN COMPONENT ===== export default function FileTreePanel({ files, activeFileId, onFileSelect, framework }: FileTreeProps) { const { addFile, removeFile, updateFile } = useEditorStore(); const [linksModalFile, setLinksModalFile] = useState(null); const wsEmit = useCallback((event: string, data: any) => { const socket = getSocket(); if (socket?.connected) { socket.emit(event, data); } }, []); const handleAddFile = useCallback((parentId?: string) => { const file: ProjectFile = { id: uuidv4(), name: `new-file.${framework === 'maui' ? 'cs' : 'js'}`, type: framework === 'maui' ? 'csharp' : 'js', content: '', }; addFile(file, parentId); wsEmit('file_added', { file, parentId }); }, [addFile, framework, wsEmit]); const handleAddFolder = useCallback((parentId?: string) => { const folder: ProjectFile = { id: uuidv4(), name: 'new-folder', type: 'folder', children: [] }; addFile(folder, parentId); wsEmit('file_added', { file: folder, parentId }); }, [addFile, wsEmit]); const handleRename = useCallback((id: string, name: string) => { updateFile(id, { name }); wsEmit('file_renamed', { fileId: id, name }); }, [updateFile, wsEmit]); const handleDelete = useCallback((id: string) => { removeFile(id); wsEmit('file_deleted', { fileId: id }); }, [removeFile, wsEmit]); const handleToggleLink = useCallback((fileId: string, linkedId: string) => { const file = findFile(files, fileId); if (!file) return; const currentLinks = file.linkedFiles || []; const newLinks = currentLinks.includes(linkedId) ? currentLinks.filter(id => id !== linkedId) : [...currentLinks, linkedId]; updateFile(fileId, { linkedFiles: newLinks }); setLinksModalFile(prev => prev ? { ...prev, linkedFiles: newLinks } : null); }, [files, updateFile]); return (

Files

{files.map((file) => ( { if (isFolder) handleAddFolder(parentId); else handleAddFile(parentId); }} allFiles={files} onOpenLinksModal={setLinksModalFile} /> ))}
{linksModalFile && ( setLinksModalFile(null)} onToggleLink={handleToggleLink} /> )}
); }