kalhdrawi's picture
ุฃูˆู„ ุฑูุน ู„ู„ู…ู„ูุงุช ุฅู„ู‰ ุงู„ุณุจูŠุณ kalhdrawi/omnidev
1cf8f01
raw
history blame
7.37 kB
"use client";
import React, { useState } from 'react';
import { FileDocument, FolderDocument } from '@/types';
import { FileTreeItem } from './file-tree-item';
import { FolderTreeItem } from './folder-tree-item';
import {
DndContext,
DragEndEvent,
DragOverlay,
DragStartEvent,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { FileTypeDetector } from '@/lib/file-type-detector';
export interface FileTreeProps {
files: FileDocument[];
folders: FolderDocument[];
selectedFileId?: string;
selectedFolderId?: string | null;
expandedFolders: Set<string>;
onFileSelect: (file: FileDocument) => void;
onFolderSelect: (folderId: string | null) => void;
onFolderToggle: (folderId: string) => void;
onFileDelete: (fileId: string) => void;
onFileRename: (fileId: string, newName: string) => void;
onFileDuplicate: (fileId: string) => void;
onFileDrop?: (fileId: string, targetFolderId: string | null) => void;
onFolderDrop?: (folderId: string, targetFolderId: string | null) => void;
}
interface DragItem {
id: string;
type: 'file' | 'folder';
name: string;
}
export function FileTree({
files,
folders,
selectedFileId,
selectedFolderId,
expandedFolders,
onFileSelect,
onFolderSelect,
onFolderToggle,
onFileDelete,
onFileRename,
onFileDuplicate,
onFileDrop,
onFolderDrop
}: FileTreeProps) {
const [activeItem, setActiveItem] = useState<DragItem | null>(null);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
type TreeNode = { type: 'file' | 'folder'; item: FileDocument | FolderDocument; children?: TreeNode[] };
// Build tree structure
const buildTree = () => {
const tree: TreeNode[] = [];
// Add root folders first
const rootFolders = folders.filter(folder => !folder.parentFolderId);
const addFolderToTree = (folder: FolderDocument, parentArray: TreeNode[]) => {
const folderNode = {
type: 'folder' as const,
item: folder,
children: [] as TreeNode[]
};
parentArray.push(folderNode);
// Add subfolders
const subfolders = folders.filter(f => f.parentFolderId === folder._id);
subfolders.forEach(subfolder => addFolderToTree(subfolder, folderNode.children));
// Add files in this folder
const folderFiles = files.filter(f => f.parentFolderId === folder._id);
folderFiles.forEach(file => {
folderNode.children.push({
type: 'file' as const,
item: file
});
});
};
// Add root folders and their contents
rootFolders.forEach(folder => addFolderToTree(folder, tree));
// Add root files (files without parent folder)
const rootFiles = files.filter(file => !file.parentFolderId);
rootFiles.forEach(file => {
tree.push({
type: 'file' as const,
item: file
});
});
return tree;
};
const tree = buildTree();
const handleDragStart = (event: DragStartEvent) => {
const { active } = event;
const [type, id] = (active.id as string).split(':');
let name = '';
if (type === 'file') {
const file = files.find(f => f._id === id);
name = file?.name || '';
} else if (type === 'folder') {
const folder = folders.find(f => f._id === id);
name = folder?.name || '';
}
setActiveItem({
id,
type: type as 'file' | 'folder',
name
});
};
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
setActiveItem(null);
if (!over) return;
const [activeType, activeId] = (active.id as string).split(':');
const [overType, overId] = (over.id as string).split(':');
// Don't allow dropping on self
if (activeId === overId) return;
// Handle file drop
if (activeType === 'file') {
const targetFolderId = overType === 'folder' ? overId : null;
onFileDrop?.(activeId, targetFolderId);
}
// Handle folder drop
if (activeType === 'folder') {
const targetFolderId = overType === 'folder' ? overId : null;
onFolderDrop?.(activeId, targetFolderId);
}
};
const renderTreeNode = (node: TreeNode, level: number = 0) => {
const { type, item, children } = node;
if (type === 'folder') {
const folder = item as FolderDocument;
const isExpanded = expandedFolders.has(folder._id);
const isSelected = selectedFolderId === folder._id;
return (
<div key={folder._id}>
<FolderTreeItem
folder={folder}
level={level}
isExpanded={isExpanded}
isSelected={isSelected}
onToggle={() => onFolderToggle(folder._id)}
onSelect={() => onFolderSelect(folder._id)}
isDragOver={false} // TODO: implement drag over state
/>
{isExpanded && children && children.length > 0 && (
<div className="ml-4">
{children.map((child: TreeNode) => renderTreeNode(child, level + 1))}
</div>
)}
</div>
);
} else {
const file = item as FileDocument;
const isSelected = selectedFileId === file._id;
return (
<FileTreeItem
key={file._id}
file={file}
level={level}
isSelected={isSelected}
onSelect={() => onFileSelect(file)}
onDelete={() => onFileDelete(file._id)}
onRename={(newName) => onFileRename(file._id, newName)}
onDuplicate={() => onFileDuplicate(file._id)}
/>
);
}
};
const allItems = [
...folders.map(f => `folder:${f._id}`),
...files.map(f => `file:${f._id}`)
];
if (tree.length === 0) {
return (
<div className="text-center text-neutral-400 py-8">
<div className="text-sm">No files or folders</div>
<div className="text-xs mt-1">Create your first file or folder to get started</div>
</div>
);
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<SortableContext items={allItems} strategy={verticalListSortingStrategy}>
<div className="space-y-1">
{tree.map(node => renderTreeNode(node))}
</div>
</SortableContext>
<DragOverlay>
{activeItem ? (
<div className="bg-neutral-800 border border-neutral-600 rounded px-2 py-1 text-sm text-white shadow-lg">
<div className="flex items-center gap-2">
<span>
{activeItem.type === 'file'
? FileTypeDetector.getFileIcon(
files.find(f => f._id === activeItem.id)?.type || 'txt'
)
: '๐Ÿ“'
}
</span>
<span>{activeItem.name}</span>
</div>
</div>
) : null}
</DragOverlay>
</DndContext>
);
}