'use client'; import * as React from 'react'; import { createContext, useCallback, useMemo, useState, } from 'react'; import type { KanbanCard, KanbanCardId, KanbanColumn, KanbanColumnId, KanbanDragState, KanbanProps, } from '../types'; // ===================================================================== // Context value // ===================================================================== export interface KanbanContextValue { columns: KanbanColumn[]; dragState: KanbanDragState; setDragState: React.Dispatch>; moveCard: (cardId: KanbanCardId, fromColumnId: KanbanColumnId, toColumnId: KanbanColumnId, toIndex: number) => void; reorderCard: (columnId: KanbanColumnId, cardId: KanbanCardId, toIndex: number) => void; renderCard?: (card: KanbanCard, columnId: KanbanColumnId) => React.ReactNode; renderColumnHeader?: (column: KanbanColumn) => React.ReactNode; loading: boolean; emptyColumnMessage: React.ReactNode; } const KanbanContext = createContext(null); export function useKanbanContext(): KanbanContextValue { const ctx = React.useContext(KanbanContext); if (!ctx) { throw new Error('useKanbanContext must be used inside '); } return ctx; } // ===================================================================== // Provider // ===================================================================== export interface KanbanProviderProps extends Pick< KanbanProps, | 'columns' | 'onMoveCard' | 'onReorderCard' | 'renderCard' | 'renderColumnHeader' | 'loading' | 'emptyColumnMessage' > { children: React.ReactNode; } export function KanbanProvider(props: KanbanProviderProps) { const { columns: initialColumns, onMoveCard, onReorderCard, renderCard, renderColumnHeader, loading = false, emptyColumnMessage = 'No cards', children, } = props; const [columns, setColumns] = useState(initialColumns); const [dragState, setDragState] = useState({ activeCardId: null, activeColumnId: null, overColumnId: null, overCardId: null, }); // Sync with external columns prop React.useEffect(() => { setColumns(initialColumns); }, [initialColumns]); const moveCard = useCallback( (cardId: KanbanCardId, fromColumnId: KanbanColumnId, toColumnId: KanbanColumnId, toIndex: number) => { setColumns((prev) => { const next = prev.map((col) => ({ ...col, cards: [...col.cards] })); const fromCol = next.find((c) => c.id === fromColumnId); const toCol = next.find((c) => c.id === toColumnId); if (!fromCol || !toCol) return prev; const cardIndex = fromCol.cards.findIndex((c) => c.id === cardId); if (cardIndex === -1) return prev; const [card] = fromCol.cards.splice(cardIndex, 1); const insertIndex = Math.min(toIndex, toCol.cards.length); toCol.cards.splice(insertIndex, 0, card); return next; }); onMoveCard?.(cardId, fromColumnId, toColumnId, toIndex); }, [onMoveCard], ); const reorderCard = useCallback( (columnId: KanbanColumnId, cardId: KanbanCardId, toIndex: number) => { setColumns((prev) => { const next = prev.map((col) => col.id === columnId ? { ...col, cards: [...col.cards] } : col, ); const col = next.find((c) => c.id === columnId); if (!col) return prev; const fromIndex = col.cards.findIndex((c) => c.id === cardId); if (fromIndex === -1 || fromIndex === toIndex) return prev; const [card] = col.cards.splice(fromIndex, 1); const insertIndex = Math.min(toIndex, col.cards.length); col.cards.splice(insertIndex, 0, card); return next; }); onReorderCard?.(columnId, cardId, toIndex); }, [onReorderCard], ); const value = useMemo( () => ({ columns, dragState, setDragState, moveCard, reorderCard, renderCard, renderColumnHeader, loading, emptyColumnMessage, }), [ columns, dragState, moveCard, reorderCard, renderCard, renderColumnHeader, loading, emptyColumnMessage, ], ); return ( {children} ); }