"use client"; import { ChevronDownIcon, ChevronRightIcon, DocumentIcon, FolderIcon, TrashIcon, } from "@heroicons/react/24/outline"; import { useState } from "react"; import React from "react"; import type { DragEvent, KeyboardEvent } from "react"; // Types export interface FileItem { id: string; name: string; type: "file" | "folder"; parentId: string | null; } export interface TreeNode extends FileItem { children: TreeNode[]; level: number; } export interface DragState { draggedId: string | null; dragOverId: string | null; dropPosition: "before" | "after" | "inside" | null; } export interface EditState { editingId: string | null; setEditingId: (id: string | null) => void; } export interface DragActions { dragState: DragState; setDragState: (state: DragState) => void; } export interface TreeState { expandedFolders: Set; toggleFolder: (id: string) => void; } export interface FileTreeItemProps { node: TreeNode; editState: EditState; dragActions: DragActions; treeState: TreeState; onUpdate: (id: string, updates: Partial) => void; onDelete: (id: string) => void; onMove: ( draggedId: string, targetId: string | null, position: "before" | "after" | "inside" ) => void; } // Constants for drag and drop export const DRAG_STATE_RESET: DragState = { draggedId: null, dragOverId: null, dropPosition: null, }; const DROP_ZONES = { BEFORE_THRESHOLD: 0.25, AFTER_THRESHOLD: 0.75, FILE_MIDDLE_THRESHOLD: 0.5, } as const; const SPACING = { LEVEL_INDENT: 20, BASE_PADDING: 8, } as const; export const FileTreeItem = ({ node, editState, dragActions, treeState, onUpdate, onDelete, onMove, }: FileTreeItemProps) => { const { editingId, setEditingId } = editState; const { dragState, setDragState } = dragActions; const { expandedFolders, toggleFolder } = treeState; const [editName, setEditName] = useState(node.name); const isExpanded = expandedFolders.has(node.id); const hasChildren = node.children.length > 0; const isEditing = editingId === node.id; const isDragging = dragState.draggedId === node.id; const isDragOver = dragState.dragOverId === node.id; const handleSave = () => { if (editName.trim()) { onUpdate(node.id, { name: editName.trim() }); setEditingId(null); } }; const handleCancel = () => { setEditName(node.name); setEditingId(null); }; const handleKeyPress = (e: KeyboardEvent) => { if (e.key === "Enter") handleSave(); else if (e.key === "Escape") handleCancel(); }; const handleDragStart = (e: DragEvent) => { e.dataTransfer.setData("text/plain", node.id); e.dataTransfer.effectAllowed = "move"; setDragState({ ...DRAG_STATE_RESET, draggedId: node.id }); }; const handleDragEnd = () => setDragState(DRAG_STATE_RESET); const getDropPosition = ( e: DragEvent, rect: DOMRect ): "before" | "after" | "inside" => { const relativeY = (e.clientY - rect.top) / rect.height; if (node.type === "folder") { if (relativeY < DROP_ZONES.BEFORE_THRESHOLD) return "before"; if (relativeY > DROP_ZONES.AFTER_THRESHOLD) return "after"; return "inside"; } return relativeY < DROP_ZONES.FILE_MIDDLE_THRESHOLD ? "before" : "after"; }; const handleDragOver = (e: DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (!dragState.draggedId) return; setDragState({ ...dragState, dragOverId: node.id, dropPosition: getDropPosition(e, e.currentTarget.getBoundingClientRect()), }); }; const handleDragLeave = (e: DragEvent) => { // Only clear if we're leaving this element entirely if (!e.currentTarget.contains(e.relatedTarget as Node)) { setDragState({ ...dragState, dragOverId: null, dropPosition: null, }); } }; const handleDrop = (e: DragEvent) => { e.preventDefault(); if (!dragState.draggedId || !dragState.dropPosition) return; onMove(dragState.draggedId, node.id, dragState.dropPosition); setDragState(DRAG_STATE_RESET); }; const getDropIndicatorClass = () => { if (!isDragOver || !dragState.dropPosition) return ""; const indicators = { before: "border-t-2 border-blue-500", after: "border-b-2 border-blue-500", inside: "bg-blue-50 border border-blue-300", }; return indicators[dragState.dropPosition] || ""; }; const leftPadding = node.level * SPACING.LEVEL_INDENT + SPACING.BASE_PADDING; return (
{/* Expand/Collapse Icon */} {node.type === "folder" && hasChildren && ( )} {(node.type === "file" || !hasChildren) && (
)} {/* File/Folder Icon */}
{node.type === "folder" ? ( ) : ( )}
{/* Name */}
{isEditing ? ( setEditName(e.target.value)} onBlur={handleSave} onKeyDown={handleKeyPress} className="w-full px-1 py-0.5 text-sm border border-blue-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500" /> ) : ( { e.stopPropagation(); setEditingId(node.id); }} > {node.name} )}
{/* Actions */}
{/* Children */} {node.type === "folder" && isExpanded && node.children.map((child) => ( ))}
); };