"use client"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; import { PlusIcon, Trash2 } from "lucide-react"; import BoardTask from "./Task"; import { TaskViewProps } from "../TaskView"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { useState, useEffect, useRef, useCallback } from "react"; export default function BoardView({ tags, tasks, setTasks, createTask, statuses, }: TaskViewProps) { const [selectedIds, setSelectedIds] = useState>(new Set()); const [isDraggingSelection, setIsDraggingSelection] = useState(false); const [selectionBox, setSelectionBox] = useState<{ startX: number; startY: number; endX: number; endY: number; } | null>(null); const boardRef = useRef(null); const taskCardRefs = useRef>({}); const mouseDownPos = useRef<{ x: number; y: number } | null>(null); function onDragEnd(result: any) { const { draggableId, source, destination } = result; if (!destination) return; const task = Object.values(tasks).find((t) => t.id === draggableId); if (!task) return; setTasks({ ...tasks, [task.id]: { ...task, statusId: destination.droppableId, updatedAt: Date.now(), }, }); } const deleteSelected = useCallback(() => { if (selectedIds.size === 0) return; const updated = { ...tasks }; selectedIds.forEach((id) => delete updated[id]); setTasks(updated); setSelectedIds(new Set()); }, [selectedIds, tasks, setTasks]); // Ctrl+A to select all, Backspace/Delete to delete selected useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if (e.key === "a" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); setSelectedIds(new Set(Object.keys(tasks))); } if (e.key === "Backspace" || e.key === "Delete") { // Don't fire if user is typing in an input/textarea const target = e.target as HTMLElement; if ( target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable ) return; deleteSelected(); } } window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [tasks, deleteSelected]); // Mouse drag selection useEffect(() => { function handleMouseDown(e: MouseEvent) { // Only start selection if clicking on the board background (not a card) const target = e.target as HTMLElement; const isCard = target.closest("[data-task-card]"); if (isCard) return; if (e.button !== 0) return; mouseDownPos.current = { x: e.clientX, y: e.clientY }; } function handleMouseMove(e: MouseEvent) { if (!mouseDownPos.current) return; const dx = Math.abs(e.clientX - mouseDownPos.current.x); const dy = Math.abs(e.clientY - mouseDownPos.current.y); if (dx > 4 || dy > 4) { setIsDraggingSelection(true); setSelectionBox({ startX: mouseDownPos.current.x, startY: mouseDownPos.current.y, endX: e.clientX, endY: e.clientY, }); } } function handleMouseUp(e: MouseEvent) { if (isDraggingSelection && selectionBox) { // Find all task cards that intersect with the selection box const minX = Math.min(selectionBox.startX, selectionBox.endX); const maxX = Math.max(selectionBox.startX, selectionBox.endX); const minY = Math.min(selectionBox.startY, selectionBox.endY); const maxY = Math.max(selectionBox.startY, selectionBox.endY); const newSelected = new Set(); Object.entries(taskCardRefs.current).forEach(([id, el]) => { if (!el) return; const rect = el.getBoundingClientRect(); if ( rect.left < maxX && rect.right > minX && rect.top < maxY && rect.bottom > minY ) { newSelected.add(id); } }); if (e.shiftKey) { setSelectedIds((prev) => new Set([...prev, ...newSelected])); } else { setSelectedIds(newSelected); } } else if (!isDraggingSelection) { // Click on empty area deselects const target = e.target as HTMLElement; const isCard = target.closest("[data-task-card]"); if (!isCard) { setSelectedIds(new Set()); } } mouseDownPos.current = null; setIsDraggingSelection(false); setSelectionBox(null); } window.addEventListener("mousedown", handleMouseDown); window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); return () => { window.removeEventListener("mousedown", handleMouseDown); window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; }, [isDraggingSelection, selectionBox]); function handleTaskClick(e: React.MouseEvent, taskId: string) { if (e.ctrlKey || e.metaKey) { setSelectedIds((prev) => { const next = new Set(prev); if (next.has(taskId)) next.delete(taskId); else next.add(taskId); return next; }); } else if (e.shiftKey) { setSelectedIds((prev) => new Set([...prev, taskId])); } // Normal click handled by SheetTrigger (task edit) if not multi-select } const tasksArray = Object.values(tasks); const tasksByStatus = statuses.map((status) => ({ status, tasks: tasksArray .filter((t) => t.statusId === status.id) .sort((a, b) => a.createdAt - b.createdAt), })); const selRect = selectionBox ? { left: Math.min(selectionBox.startX, selectionBox.endX), top: Math.min(selectionBox.startY, selectionBox.endY), width: Math.abs(selectionBox.endX - selectionBox.startX), height: Math.abs(selectionBox.endY - selectionBox.startY), } : null; return (
{/* Selection box overlay */} {isDraggingSelection && selRect && (
)} {/* Delete bar when items selected */} {selectedIds.size > 0 && (
{selectedIds.size} task{selectedIds.size !== 1 ? "s" : ""} selected — press Backspace to delete
)}
{tasksByStatus.map(({ status, tasks: statusTasks }) => ( {(provided) => ( {status.name}
{statusTasks.map((task, index) => ( {(provided) => (
{ provided.innerRef(el); taskCardRefs.current[task.id] = el; }} {...provided.draggableProps} {...provided.dragHandleProps} data-task-card={task.id} onClick={(e) => handleTaskClick(e, task.id)} > setTasks({ ...tasks, [t.id]: t, }) } provided={provided} task={task} tags={tags} isSelected={selectedIds.has(task.id)} onDelete={(id) => { const updated = { ...tasks }; delete updated[id]; setTasks(updated); setSelectedIds((prev) => { const next = new Set(prev); next.delete(id); return next; }); }} />
{ const updated = { ...tasks }; delete updated[task.id]; setTasks(updated); setSelectedIds((prev) => { const next = new Set(prev); next.delete(task.id); return next; }); }} > Delete
)}
))} {provided.placeholder} {statusTasks.length === 0 && (

No tasks

)}
)}
))}
); }