"use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { DndContext, DragOverlay, PointerSensor, TouchSensor, closestCorners, useDraggable, useDroppable, useSensor, useSensors, type DragEndEvent, type DragStartEvent, } from "@dnd-kit/core"; import { restrictToWindowEdges } from "@dnd-kit/modifiers"; import { GitBranch, GripVertical, NotebookPen } from "lucide-react"; import type { ProjectStatus } from "@prisma/client"; import { CiBadge } from "@/components/CiBadge"; import { STATUS_META, STATUS_ORDER, accentOf } from "@/lib/status"; import { cn, truncate } from "@/lib/utils"; import type { ProjectListItem } from "@/types"; /** * Statusspalten mit Ziehen und Ablegen. * * Der Statuswechsel wird sofort angezeigt und erst danach gespeichert; * scheitert das Speichern, springt die Karte zurueck. Die Reihenfolge * innerhalb einer Spalte ist nicht frei sortierbar – sie folgt der im Board * gewaehlten Sortierung, sonst muesste eine Position mitgespeichert werden. */ export function KanbanBoard({ projects, showArchived, }: { projects: ProjectListItem[]; showArchived: boolean; }) { const router = useRouter(); const [overrides, setOverrides] = useState>({}); const [activeId, setActiveId] = useState(null); // Eine Verschiebung, die der Server bestaetigt hat, braucht keine // oertliche Ausnahme mehr. useEffect(() => { setOverrides((current) => { const next: Record = {}; for (const [id, status] of Object.entries(current)) { const project = projects.find((entry) => entry.id === id); if (project && project.status !== status) next[id] = status; } return Object.keys(next).length === Object.keys(current).length ? current : next; }); }, [projects]); const sensors = useSensors( // Erst ab ein paar Pixeln Bewegung ziehen – sonst liesse sich die Karte // nicht mehr anklicken. useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(TouchSensor, { activationConstraint: { delay: 180, tolerance: 8 } }), ); const statusOf = (project: ProjectListItem) => overrides[project.id] ?? project.status; const columns = useMemo(() => { const visible = STATUS_ORDER.filter( (status) => showArchived || status !== "ARCHIVED", ); return visible.map((status) => ({ status, items: projects.filter((project) => statusOf(project) === status), })); // eslint-disable-next-line react-hooks/exhaustive-deps }, [projects, overrides, showArchived]); const active = activeId ? projects.find((project) => project.id === activeId) : null; function onDragStart(event: DragStartEvent) { setActiveId(String(event.active.id)); } async function onDragEnd(event: DragEndEvent) { setActiveId(null); const id = String(event.active.id); const target = event.over?.id ? (String(event.over.id) as ProjectStatus) : null; if (!target) return; const project = projects.find((entry) => entry.id === id); if (!project) return; const previous = statusOf(project); if (previous === target) return; setOverrides((current) => ({ ...current, [id]: target })); try { const response = await fetch(`/api/projects/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: target }), }); if (!response.ok) throw new Error("failed"); router.refresh(); } catch { setOverrides((current) => { const next = { ...current }; delete next[id]; return next; }); } } return ( setActiveId(null)} >
{columns.map((column) => ( {column.items.map((project) => ( ))} ))}
{/* Die mitgezogene Karte haengt am Cursor, nicht in der Spalte. */} {active ? : null}
); } function Column({ status, count, children, }: { status: ProjectStatus; count: number; children: React.ReactNode; }) { const meta = STATUS_META[status]; const { setNodeRef, isOver } = useDroppable({ id: status }); return (

{meta.label}

{count}
{children} {count === 0 ? (

{isOver ? "Hier ablegen" : "leer"}

) : null}
); } function KanbanCard({ project, overlay = false, }: { project: ProjectListItem; overlay?: boolean; }) { const accent = accentOf(project.accent); const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ id: project.id }); return (
); }