kalhdrawi's picture
ุฃูˆู„ ุฑูุน ู„ู„ู…ู„ูุงุช ุฅู„ู‰ ุงู„ุณุจูŠุณ kalhdrawi/omnidev
1cf8f01
raw
history blame
11.5 kB
"use client";
import React, { useState, useEffect, useCallback } from 'react';
import { FileDocument, FolderDocument, FileType } from '@/types';
import { FileTree } from './file-tree';
// import { FileSearch } from './file-search';
// import { FileActions } from './file-actions';
import { CreateFileDialog } from './create-file-dialog';
import { CreateFolderDialog } from './create-folder-dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
// import { Separator } from '@/components/ui/separator';
import { Badge } from '@/components/ui/badge';
import {
FolderPlus,
FilePlus,
Search,
Filter,
RefreshCw,
// Settings,
ChevronDown,
ChevronRight
} from 'lucide-react';
import { api } from '@/lib/api';
import { toast } from 'sonner';
export interface FileExplorerProps {
projectId: string;
onFileSelect: (file: FileDocument) => void;
onFileCreate: (file: FileDocument) => void;
onFileUpdate: (file: FileDocument) => void;
onFileDelete: (fileId: string) => void;
onFolderCreate: (folder: FolderDocument) => void;
selectedFileId?: string;
className?: string;
}
export function FileExplorer({
projectId,
onFileSelect,
onFileCreate,
// onFileUpdate,
onFileDelete,
onFolderCreate,
selectedFileId,
className = ""
}: FileExplorerProps) {
const [files, setFiles] = useState<FileDocument[]>([]);
const [folders, setFolders] = useState<FolderDocument[]>([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
const [filterType, setFilterType] = useState<FileType | 'all'>('all');
const [isCreateFileOpen, setIsCreateFileOpen] = useState(false);
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
const [isCollapsed, setIsCollapsed] = useState(false);
// Load project files and folders
const loadProjectData = useCallback(async () => {
try {
setLoading(true);
// Load folders
const foldersResponse = await api.get(`/projects/${projectId}/folders?tree=true`);
if (foldersResponse.data.ok) {
setFolders(foldersResponse.data.tree || []);
}
// Load files
const filesResponse = await api.get(`/projects/${projectId}/files`);
if (filesResponse.data.ok) {
setFiles(filesResponse.data.files || []);
}
} catch (error) {
console.error('Error loading project data:', error);
toast.error('Failed to load project files');
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => {
loadProjectData();
}, [loadProjectData]);
// Filter files based on search and type
const filteredFiles = files.filter(file => {
const matchesSearch = !searchQuery ||
file.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
file.content.toLowerCase().includes(searchQuery.toLowerCase());
const matchesType = filterType === 'all' || file.type === filterType;
const matchesFolder = !selectedFolder || file.parentFolderId === selectedFolder;
return matchesSearch && matchesType && matchesFolder;
});
// Handle file creation
const handleFileCreate = async (fileData: {
name: string;
content: string;
type: FileType;
parentFolderId?: string;
}) => {
try {
const response = await api.post(`/projects/${projectId}/files`, {
...fileData,
parentFolderId: selectedFolder || fileData.parentFolderId
});
if (response.data.ok) {
const newFile = response.data.file;
setFiles(prev => [...prev, newFile]);
onFileCreate(newFile);
toast.success(`File "${newFile.name}" created successfully`);
setIsCreateFileOpen(false);
}
} catch (error: unknown) {
console.error('Error creating file:', error);
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Failed to create file';
toast.error(message);
}
};
// Handle folder creation
const handleFolderCreate = async (folderData: {
name: string;
parentFolderId?: string;
}) => {
try {
const response = await api.post(`/projects/${projectId}/folders`, {
...folderData,
parentFolderId: selectedFolder || folderData.parentFolderId
});
if (response.data.ok) {
const newFolder = response.data.folder;
setFolders(prev => [...prev, newFolder]);
onFolderCreate(newFolder);
toast.success(`Folder "${newFolder.name}" created successfully`);
setIsCreateFolderOpen(false);
// Expand parent folder
if (newFolder.parentFolderId) {
setExpandedFolders(prev => new Set([...prev, newFolder.parentFolderId!]));
}
}
} catch (error: unknown) {
console.error('Error creating folder:', error);
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Failed to create folder';
toast.error(message);
}
};
// Handle file deletion
const handleFileDelete = async (fileId: string) => {
try {
const response = await api.delete(`/projects/${projectId}/files/${fileId}`);
if (response.data.ok) {
setFiles(prev => prev.filter(f => f._id !== fileId));
onFileDelete(fileId);
toast.success('File deleted successfully');
}
} catch (error: unknown) {
console.error('Error deleting file:', error);
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Failed to delete file';
toast.error(message);
}
};
// Handle folder selection
const handleFolderSelect = (folderId: string | null) => {
setSelectedFolder(folderId);
};
// Toggle folder expansion
const toggleFolderExpansion = (folderId: string) => {
setExpandedFolders(prev => {
const newSet = new Set(prev);
if (newSet.has(folderId)) {
newSet.delete(folderId);
} else {
newSet.add(folderId);
}
return newSet;
});
};
// Get file type options for filter
const fileTypes = Array.from(new Set(files.map(f => f.type)));
if (isCollapsed) {
return (
<div className={`w-12 bg-neutral-900 border-r border-neutral-800 flex flex-col items-center py-4 ${className}`}>
<Button
variant="ghost"
size="sm"
onClick={() => setIsCollapsed(false)}
className="p-2"
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
);
}
return (
<div className={`w-80 bg-neutral-900 border-r border-neutral-800 flex flex-col ${className}`}>
{/* Header */}
<div className="p-4 border-b border-neutral-800">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-white">Files</h2>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={loadProjectData}
disabled={loading}
className="p-2"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setIsCollapsed(true)}
className="p-2"
>
<ChevronDown className="h-4 w-4" />
</Button>
</div>
</div>
{/* Search */}
<div className="relative mb-3">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-neutral-400" />
<Input
placeholder="Search files..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-neutral-800 border-neutral-700 text-white"
/>
</div>
{/* Actions */}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setIsCreateFileOpen(true)}
className="flex-1 bg-neutral-800 border-neutral-700 hover:bg-neutral-700"
>
<FilePlus className="h-4 w-4 mr-2" />
File
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setIsCreateFolderOpen(true)}
className="flex-1 bg-neutral-800 border-neutral-700 hover:bg-neutral-700"
>
<FolderPlus className="h-4 w-4 mr-2" />
Folder
</Button>
</div>
</div>
{/* Filter */}
<div className="p-4 border-b border-neutral-800">
<div className="flex items-center gap-2 mb-2">
<Filter className="h-4 w-4 text-neutral-400" />
<span className="text-sm text-neutral-400">Filter by type</span>
</div>
<div className="flex flex-wrap gap-1">
<Badge
variant={filterType === 'all' ? 'default' : 'secondary'}
className="cursor-pointer text-xs"
onClick={() => setFilterType('all')}
>
All ({files.length})
</Badge>
{fileTypes.map(type => {
const count = files.filter(f => f.type === type).length;
return (
<Badge
key={type}
variant={filterType === type ? 'default' : 'secondary'}
className="cursor-pointer text-xs"
onClick={() => setFilterType(type)}
>
{type} ({count})
</Badge>
);
})}
</div>
</div>
{/* File Tree */}
<ScrollArea className="flex-1">
<div className="p-4">
{loading ? (
<div className="text-center text-neutral-400 py-8">
<RefreshCw className="h-6 w-6 animate-spin mx-auto mb-2" />
Loading files...
</div>
) : (
<FileTree
files={filteredFiles}
folders={folders}
selectedFileId={selectedFileId}
selectedFolderId={selectedFolder}
expandedFolders={expandedFolders}
onFileSelect={onFileSelect}
onFolderSelect={handleFolderSelect}
onFolderToggle={toggleFolderExpansion}
onFileDelete={handleFileDelete}
onFileRename={(fileId, newName) => {
// Handle file rename
console.log('Rename file:', fileId, newName);
}}
onFileDuplicate={(fileId) => {
// Handle file duplicate
console.log('Duplicate file:', fileId);
}}
/>
)}
</div>
</ScrollArea>
{/* Dialogs */}
<CreateFileDialog
open={isCreateFileOpen}
onOpenChange={setIsCreateFileOpen}
onFileCreate={handleFileCreate}
parentFolderId={selectedFolder}
/>
<CreateFolderDialog
open={isCreateFolderOpen}
onOpenChange={setIsCreateFolderOpen}
onFolderCreate={handleFolderCreate}
parentFolderId={selectedFolder}
/>
</div>
);
}