import { useState, useRef, useEffect, useCallback } from "react"; import { Plus } from "lucide-react"; import { DndContext, DragOverlay, closestCorners, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragStartEvent, type DragOverEvent, type DragEndEvent, } from "@dnd-kit/core"; import { SortableContext, horizontalListSortingStrategy, } from "@dnd-kit/sortable"; import { Button } from "@/components/ui/button"; import { useBoardStore } from "@/stores/board-store"; import { KanbanColumn } from "@/components/board/KanbanColumn"; import { CardOverlay, ColumnOverlay, } from "@/components/board/DragOverlayContent"; import { CardDetailModal } from "@/components/card-detail/CardDetailModal"; import type { Board } from "@/types/board"; function findColumnByCardId(board: Board, cardId: string) { return board.columns.find((col) => col.cardIds.includes(cardId)); } export function BoardView() { const board = useBoardStore((s) => s.board); const addColumn = useBoardStore((s) => s.addColumn); const moveCard = useBoardStore((s) => s.moveCard); const moveColumn = useBoardStore((s) => s.moveColumn); const [selectedCardId, setSelectedCardId] = useState(null); const [addingColumn, setAddingColumn] = useState(false); const [newColumnTitle, setNewColumnTitle] = useState(""); const inputRef = useRef(null); // Listen for custom event to open card detail from command palette useEffect(() => { function handleOpenCard(e: Event) { const detail = (e as CustomEvent<{ cardId: string }>).detail; if (detail?.cardId) { setSelectedCardId(detail.cardId); } } document.addEventListener("open-card-detail", handleOpenCard); return () => { document.removeEventListener("open-card-detail", handleOpenCard); }; }, []); // Drag state const [activeId, setActiveId] = useState(null); const [activeType, setActiveType] = useState<"card" | "column" | null>(null); // Sensors: PointerSensor with distance of 5px prevents accidental drags on click const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5 }, }), useSensor(KeyboardSensor) ); useEffect(() => { if (addingColumn && inputRef.current) { inputRef.current.focus(); } }, [addingColumn]); function handleAddColumn() { const trimmed = newColumnTitle.trim(); if (trimmed) { addColumn(trimmed); setNewColumnTitle(""); inputRef.current?.focus(); } } function handleKeyDown(e: React.KeyboardEvent) { if (e.key === "Enter") { handleAddColumn(); } else if (e.key === "Escape") { setAddingColumn(false); setNewColumnTitle(""); } } // --- Drag handlers --- const handleDragStart = useCallback((event: DragStartEvent) => { const { active } = event; const type = active.data.current?.type as "card" | "column" | undefined; setActiveId(active.id as string); setActiveType(type ?? null); }, []); const handleDragOver = useCallback( (event: DragOverEvent) => { const { active, over } = event; if (!over || !board) return; const activeType = active.data.current?.type; if (activeType !== "card") return; // Only handle card cross-column moves here const activeCardId = active.id as string; const overId = over.id as string; // Determine the source column const activeColumn = findColumnByCardId(board, activeCardId); if (!activeColumn) return; // Determine the target column let overColumn: ReturnType; let overIndex: number; // Check if we're hovering over a card const overType = over.data.current?.type; if (overType === "card") { overColumn = findColumnByCardId(board, overId); if (!overColumn) return; overIndex = overColumn.cardIds.indexOf(overId); } else if (overType === "column") { // Hovering over the droppable area of a column const columnId = over.data.current?.columnId as string | undefined; if (columnId) { overColumn = board.columns.find((c) => c.id === columnId); } else { overColumn = board.columns.find((c) => c.id === overId); } if (!overColumn) return; overIndex = overColumn.cardIds.length; // Append to end } else { return; } // Only move if we're going to a different column or different position if (activeColumn.id === overColumn.id) return; moveCard(activeCardId, activeColumn.id, overColumn.id, overIndex); }, [board, moveCard] ); const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; if (!over || !board) { setActiveId(null); setActiveType(null); return; } const type = active.data.current?.type; if (type === "column") { // Column reordering const activeColumnId = active.id as string; const overColumnId = over.id as string; if (activeColumnId !== overColumnId) { const fromIndex = board.columns.findIndex( (c) => c.id === activeColumnId ); const toIndex = board.columns.findIndex( (c) => c.id === overColumnId ); if (fromIndex !== -1 && toIndex !== -1) { moveColumn(fromIndex, toIndex); } } } else if (type === "card") { // Card reordering within same column (cross-column already handled in onDragOver) const activeCardId = active.id as string; const overId = over.id as string; const activeColumn = findColumnByCardId(board, activeCardId); if (!activeColumn) { setActiveId(null); setActiveType(null); return; } const overType = over.data.current?.type; if (overType === "card") { const overColumn = findColumnByCardId(board, overId); if (!overColumn) { setActiveId(null); setActiveType(null); return; } if (activeColumn.id === overColumn.id) { // Within same column, reorder const oldIndex = activeColumn.cardIds.indexOf(activeCardId); const newIndex = activeColumn.cardIds.indexOf(overId); if (oldIndex !== newIndex) { moveCard(activeCardId, activeColumn.id, activeColumn.id, newIndex); } } } else if (overType === "column") { // Dropped on an empty column droppable const columnId = over.data.current?.columnId as string | undefined; const targetColumnId = columnId ?? (over.id as string); const targetColumn = board.columns.find( (c) => c.id === targetColumnId ); if (targetColumn && activeColumn.id !== targetColumn.id) { moveCard( activeCardId, activeColumn.id, targetColumn.id, targetColumn.cardIds.length ); } } } setActiveId(null); setActiveType(null); }, [board, moveCard, moveColumn] ); const [announcement, setAnnouncement] = useState(""); const handleDragEndWithAnnouncement = useCallback( (event: DragEndEvent) => { handleDragEnd(event); const { active, over } = event; if (over && board) { const type = active.data.current?.type; if (type === "card") { const card = board.cards[active.id as string]; const targetCol = over.data.current?.type === "column" ? board.columns.find((c) => c.id === (over.data.current?.columnId ?? over.id)) : findColumnByCardId(board, over.id as string); if (card && targetCol) { setAnnouncement(`Moved card "${card.title}" to ${targetCol.title}`); } } else if (type === "column") { const col = board.columns.find((c) => c.id === (active.id as string)); if (col) { setAnnouncement(`Reordered column "${col.title}"`); } } } }, [handleDragEnd, board] ); if (!board) { return (
Loading board...
); } // Get the active item for the drag overlay const activeCard = activeType === "card" && activeId ? board.cards[activeId] : null; const activeColumn = activeType === "column" && activeId ? board.columns.find((c) => c.id === activeId) : null; const columnIds = board.columns.map((c) => c.id); return ( <> {/* Visually hidden live region for drag-and-drop announcements */}
{announcement}
{board.columns.map((column) => ( ))} {/* Add column button / inline input */}
{addingColumn ? (
setNewColumnTitle(e.target.value)} onKeyDown={handleKeyDown} onBlur={() => { if (!newColumnTitle.trim()) { setAddingColumn(false); } }} placeholder="Column title..." className="h-8 rounded-md bg-pylon-surface px-3 text-sm text-pylon-text outline-none placeholder:text-pylon-text-secondary focus:ring-1 focus:ring-pylon-accent" />
) : ( )}
{/* Drag overlay - renders a styled copy of the dragged item */} {activeCard ? ( ) : activeColumn ? ( ) : null}
setSelectedCardId(null)} /> ); }