"use client"; import type { Announcements, DndContextProps, DragEndEvent, DragOverEvent, DragStartEvent, } from "@dnd-kit/core"; import { closestCenter, DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor, useDroppable, useSensor, useSensors, } from "@dnd-kit/core"; import { arrayMove, SortableContext, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { GripHorizontal } from "lucide-react"; import { createContext, type HTMLAttributes, type ReactNode, useContext, useState, } from "react"; import { createPortal } from "react-dom"; import tunnel from "tunnel-rat"; import { Card } from "~/components/ui/card"; import { ScrollArea, ScrollBar } from "~/components/ui/scroll-area"; import { cn } from "~/lib/utils"; //https://www.shadcn.io/components/data/kanban#installation const t = tunnel(); export type { DragEndEvent } from "@dnd-kit/core"; type KanbanItemProps = { id: string; name: string; column: string; } & Record; type KanbanColumnProps = { id: string; name: string; } & Record; type KanbanContextProps< T extends KanbanItemProps = KanbanItemProps, C extends KanbanColumnProps = KanbanColumnProps, > = { columns: C[]; data: T[]; activeCardId: string | null; }; const KanbanContext = createContext({ columns: [], data: [], activeCardId: null, }); export type KanbanBoardProps = { id: string; children: ReactNode; className?: string; }; export const KanbanBoard = ({ id, children, className }: KanbanBoardProps) => { const { isOver, setNodeRef } = useDroppable({ id, }); return (
{children}
); }; export type KanbanCardProps = T & { children?: ReactNode; className?: string; cardOnClick?: () => void; }; export const KanbanCard = ({ id, name, children, className, cardOnClick, }: KanbanCardProps) => { const { attributes, listeners, setNodeRef, setActivatorNodeRef, transition, transform, isDragging, } = useSortable({ id, }); const { activeCardId } = useContext(KanbanContext) as KanbanContextProps; const style = { transition, transform: CSS.Transform.toString(transform), }; return ( <>
{children ??

{name}

}
{activeCardId === id && (
{children ??

{name}

}
)} ); }; export type KanbanCardsProps = Omit, "children" | "id"> & { children: (item: T) => ReactNode; id: string; }; export const KanbanCards = ({ children, className, ...props }: KanbanCardsProps) => { const { data } = useContext(KanbanContext) as KanbanContextProps; const filteredData = data.filter((item) => item.column === props.id); const items = filteredData.map((item) => item.id); return (
{filteredData.map(children)}
); }; export type KanbanHeaderProps = HTMLAttributes; export const KanbanHeader = ({ className, ...props }: KanbanHeaderProps) => (
); export type KanbanProviderProps< T extends KanbanItemProps = KanbanItemProps, C extends KanbanColumnProps = KanbanColumnProps, > = Omit & { children: (column: C) => ReactNode; className?: string; columns: C[]; data: T[]; onDataChange?: (data: T[]) => void; onDragStart?: (event: DragStartEvent) => void; onDragEnd?: (event: DragEndEvent) => void; onDragOver?: (event: DragOverEvent) => void; }; export const KanbanProvider = < T extends KanbanItemProps = KanbanItemProps, C extends KanbanColumnProps = KanbanColumnProps, >({ children, onDragStart, onDragEnd, onDragOver, className, columns, data, onDataChange, ...props }: KanbanProviderProps) => { const [activeCardId, setActiveCardId] = useState(null); const sensors = useSensors( useSensor(MouseSensor), useSensor(TouchSensor), useSensor(KeyboardSensor), ); const handleDragStart = (event: DragStartEvent) => { const card = data.find((item) => item.id === event.active.id); if (card) { setActiveCardId(event.active.id as string); } onDragStart?.(event); }; const handleDragOver = (event: DragOverEvent) => { const { active, over } = event; if (!over) { return; } const activeItem = data.find((item) => item.id === active.id); const overItem = data.find((item) => item.id === over.id); if (!activeItem) { return; } const activeColumn = activeItem.column; const overColumn = overItem?.column || columns.find((col) => col.id === over.id)?.id || columns[0]?.id; if (!overColumn) { return; } if (activeColumn !== overColumn) { let newData = [...data]; const activeIndex = newData.findIndex((item) => item.id === active.id); const overIndex = newData.findIndex((item) => item.id === over.id); if (newData[activeIndex] === undefined) { return; } newData[activeIndex].column = overColumn; // Now TypeScript knows this is a string newData = arrayMove(newData, activeIndex, overIndex); onDataChange?.(newData); } onDragOver?.(event); }; const handleDragEnd = (event: DragEndEvent) => { setActiveCardId(null); onDragEnd?.(event); const { active, over } = event; if (!over || active.id === over.id) { return; } let newData = [...data]; const oldIndex = newData.findIndex((item) => item.id === active.id); const newIndex = newData.findIndex((item) => item.id === over.id); newData = arrayMove(newData, oldIndex, newIndex); onDataChange?.(newData); }; const announcements: Announcements = { onDragStart({ active }) { const { name, column } = data.find((item) => item.id === active.id) ?? {}; return `Picked up the card "${name}" from the "${column}" column`; }, onDragOver({ active, over }) { const { name } = data.find((item) => item.id === active.id) ?? {}; const newColumn = columns.find((column) => column.id === over?.id)?.name; return `Dragged the card "${name}" over the "${newColumn}" column`; }, onDragEnd({ active, over }) { const { name } = data.find((item) => item.id === active.id) ?? {}; const newColumn = columns.find((column) => column.id === over?.id)?.name; return `Dropped the card "${name}" into the "${newColumn}" column`; }, onDragCancel({ active }) { const { name } = data.find((item) => item.id === active.id) ?? {}; return `Cancelled dragging the card "${name}"`; }, }; return (
{columns.map((column) => children(column))}
{typeof window !== "undefined" && createPortal( , document.body, )}
); };