"use client" import { useState } from "react" import { ChevronRight, FileText, Folder, Plus, Trash2, GripVertical } from "lucide-react" import { toast } from "sonner" import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent } from "@dnd-kit/core" import { SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from "@dnd-kit/sortable" import { CSS } from "@dnd-kit/utilities" import { Chapter } from "../types" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/shared/components/ui/collapsible" import { Button } from "@/shared/components/ui/button" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/shared/components/ui/alert-dialog" import { cn } from "@/shared/lib/utils" import { CreateChapterDialog } from "./create-chapter-dialog" import { deleteChapterAction, reorderChaptersAction } from "../actions" interface SortableChapterItemProps { chapter: Chapter level: number selectedId?: string onSelect: (chapter: Chapter) => void textbookId: string onDelete: (chapter: Chapter) => void onCreateSub: (parentId: string) => void canEdit?: boolean } function SortableChapterItem({ chapter, level, selectedId, onSelect, textbookId, onDelete, onCreateSub, canEdit = true }: SortableChapterItemProps) { const [isOpen, setIsOpen] = useState(level === 0) const hasChildren = chapter.children && chapter.children.length > 0 const isSelected = chapter.id === selectedId const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: chapter.id, disabled: !canEdit }) const style = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 10 : 1, position: "relative" as const, } return (
0 && "ml-2 border-l border-muted/30 pl-2")}>
{canEdit && (
)} {hasChildren ? ( ) : (
)}
onSelect(chapter)} > {hasChildren ? ( ) : ( )} {chapter.title}
{hasChildren && ( )}
) } function RecursiveSortableList({ items, level, selectedId, onSelect, textbookId, onDelete, onCreateSub, canEdit = true }: { items: Chapter[], level: number, selectedId?: string, onSelect: (c: Chapter) => void, textbookId: string, onDelete: (c: Chapter) => void, onCreateSub: (pid: string) => void, canEdit?: boolean }) { return ( i.id)} strategy={verticalListSortingStrategy}> {items.map((chapter) => ( ))} ) } interface ChapterSidebarListProps { chapters: Chapter[] selectedChapterId?: string onSelectChapter: (chapter: Chapter) => void textbookId: string canEdit?: boolean } export function ChapterSidebarList({ chapters, selectedChapterId, onSelectChapter, textbookId, canEdit = true }: ChapterSidebarListProps) { const [showCreateDialog, setShowCreateDialog] = useState(false) const [createParentId, setCreateParentId] = useState(undefined) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [deleteTarget, setDeleteTarget] = useState(null) const [isDeleting, setIsDeleting] = useState(false) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ) const handleDragEnd = async (event: DragEndEvent) => { const { active, over } = event if (!over || active.id === over.id) return // Find which list the items belong to // Since we only support sibling reordering for now, we assume active and over are in the same list // We need a helper to find the parent of an item in the tree const findParent = (items: Chapter[], id: string): Chapter | null => { for (const item of items) { if (item.children?.some(c => c.id === id)) return item if (item.children) { const found = findParent(item.children, id) if (found) return found } } return null } const activeParent = findParent(chapters, active.id as string) const overParent = findParent(chapters, over.id as string) // If parents don't match (and neither is root), we can't reorder easily in this simplified version // But actually, we need to check if they are in the same list. // If both are root items (activeParent is null), they are siblings. const getSiblings = (parentId: string | null) => { if (!parentId) return chapters const parent = chapters.find(c => c.id === parentId) // This only finds root parents, we need recursive find const findNode = (nodes: Chapter[], id: string): Chapter | null => { for (const node of nodes) { if (node.id === id) return node if (node.children) { const found = findNode(node.children, id) if (found) return found } } return null } return findNode(chapters, parentId)?.children || [] } // Simplified logic: We trust dnd-kit's SortableContext to only allow valid drops if we restricted it? // No, dnd-kit allows dropping anywhere by default unless restricted. // We need to find the list that contains the 'active' item // And the list that contains the 'over' item. // If they are the same list, we reorder. let activeList: Chapter[] = chapters let activeParentId: string | null = null if (activeParent) { activeList = activeParent.children || [] activeParentId = activeParent.id } else { // Check if active is in root if (!chapters.some(c => c.id === active.id)) { // Should not happen if tree is consistent return } } // Check if over is in the same list if (activeList.some(c => c.id === over.id)) { const oldIndex = activeList.findIndex((item) => item.id === active.id) const newIndex = activeList.findIndex((item) => item.id === over.id) await reorderChaptersAction(active.id as string, newIndex, activeParentId, textbookId) toast.success("Order updated") } } const handleDelete = async () => { if (!deleteTarget) return setIsDeleting(true) const res = await deleteChapterAction(deleteTarget.id, textbookId) setIsDeleting(false) if (res.success) { toast.success(res.message) setShowDeleteDialog(false) setDeleteTarget(null) } else { toast.error(res.message) } } const handleDeleteRequest = (chapter: Chapter) => { if (chapter.children && chapter.children.length > 0) { toast.error("Cannot delete chapter with subchapters") return } setDeleteTarget(chapter) setShowDeleteDialog(true) } const handleCreateSubRequest = (parentId: string) => { setCreateParentId(parentId) setShowCreateDialog(true) } // If not editable, we can skip dnd logic if (!canEdit) { return ( ) } return ( { setShowCreateDialog(open) if (!open) setCreateParentId(undefined) }} /> Delete Chapter? This will permanently delete {deleteTarget?.title}. This action cannot be undone. Cancel {isDeleting ? "Deleting..." : "Delete"} ) }