"use client"; import { createContext, useContext, useState, useCallback } from "react"; import { toast } from "sonner"; import type { CategoryDTO, GroupDTO, TodoDTO } from "@/types/board"; import { createCategory, deleteCategory as deleteCategoryAction, reorderCategories, } from "@/lib/actions/categories"; import { createGroup, updateGroup as updateGroupAction, deleteGroup as deleteGroupAction, reorderGroupsInCategory, moveGroupToCategory, } from "@/lib/actions/groups"; import { createTodo, updateTodo as updateTodoAction, toggleTodo as toggleTodoAction, deleteTodo as deleteTodoAction, } from "@/lib/actions/todos"; import { updateGroupNote } from "@/lib/actions/notes"; interface BoardContextValue { categories: CategoryDTO[]; addCategory: (name: string) => Promise; removeCategory: (categoryId: string) => Promise; reorderLanes: (orderedIds: string[]) => Promise; addGroup: (categoryId: string, title: string, color: string) => Promise; editGroup: ( groupId: string, categoryId: string, data: { title?: string; color?: string } ) => Promise; removeGroup: (groupId: string, categoryId: string) => Promise; reorderGroups: (categoryId: string, orderedIds: string[]) => Promise; moveGroup: ( groupId: string, fromCategoryId: string, toCategoryId: string, orderedTarget: string[], orderedSource: string[] ) => Promise; saveNote: (groupId: string, categoryId: string, noteContent: string) => Promise; addTodo: ( groupId: string, categoryId: string, title: string, details?: string ) => Promise; editTodo: ( todoId: string, groupId: string, categoryId: string, data: { title?: string; details?: string | null } ) => Promise; toggleTodoDone: ( todoId: string, groupId: string, categoryId: string, completed: boolean ) => Promise; removeTodo: (todoId: string, groupId: string, categoryId: string) => Promise; } const BoardContext = createContext(null); export function BoardProvider({ initialCategories, children, }: { initialCategories: CategoryDTO[]; children: React.ReactNode; }) { const [categories, setCategories] = useState(initialCategories); const updateCategory = useCallback( (categoryId: string, updater: (c: CategoryDTO) => CategoryDTO) => { setCategories((prev) => prev.map((c) => (c.id === categoryId ? updater(c) : c))); }, [] ); const updateGroupInState = useCallback( (categoryId: string, groupId: string, updater: (g: GroupDTO) => GroupDTO) => { updateCategory(categoryId, (c) => ({ ...c, groups: c.groups.map((g) => (g.id === groupId ? updater(g) : g)), })); }, [updateCategory] ); const addCategory = useCallback(async (name: string) => { try { const category = await createCategory(name); setCategories((prev) => [...prev, category]); } catch { toast.error("Couldn't create category. Try again."); } }, []); const removeCategory = useCallback(async (categoryId: string) => { let prevState: CategoryDTO[] = []; setCategories((prev) => { prevState = prev; return prev.filter((c) => c.id !== categoryId); }); try { await deleteCategoryAction(categoryId); } catch { setCategories(prevState); toast.error("Couldn't delete category. Try again."); } }, []); const reorderLanes = useCallback(async (orderedIds: string[]) => { let prevState: CategoryDTO[] = []; setCategories((prev) => { prevState = prev; const byId = new Map(prev.map((c) => [c.id, c])); return orderedIds.map((id, order) => ({ ...byId.get(id)!, order })); }); try { await reorderCategories(orderedIds); } catch { setCategories(prevState); toast.error("Couldn't reorder categories. Try again."); } }, []); const addGroup = useCallback(async (categoryId: string, title: string, color: string) => { try { const group = await createGroup(categoryId, title, color); updateCategory(categoryId, (c) => ({ ...c, groups: [...c.groups, group] })); return group; } catch { toast.error("Couldn't create group. Try again."); return undefined; } }, [updateCategory]); const editGroup = useCallback( async (groupId: string, categoryId: string, data: { title?: string; color?: string }) => { try { await updateGroupAction(groupId, data); updateGroupInState(categoryId, groupId, (g) => ({ ...g, ...data })); } catch { toast.error("Couldn't update group. Try again."); } }, [updateGroupInState] ); const removeGroup = useCallback(async (groupId: string, categoryId: string) => { let prevState: CategoryDTO[] = []; setCategories((prev) => { prevState = prev; return prev.map((c) => c.id === categoryId ? { ...c, groups: c.groups.filter((g) => g.id !== groupId) } : c ); }); try { await deleteGroupAction(groupId); } catch { setCategories(prevState); toast.error("Couldn't delete group. Try again."); } }, []); const reorderGroups = useCallback(async (categoryId: string, orderedIds: string[]) => { let prevState: CategoryDTO[] = []; setCategories((prev) => { prevState = prev; return prev.map((c) => { if (c.id !== categoryId) return c; const byId = new Map(c.groups.map((g) => [g.id, g])); return { ...c, groups: orderedIds.map((id, order) => ({ ...byId.get(id)!, order })) }; }); }); try { await reorderGroupsInCategory(categoryId, orderedIds); } catch { setCategories(prevState); toast.error("Couldn't reorder groups. Try again."); } }, []); const moveGroup = useCallback( async ( groupId: string, fromCategoryId: string, toCategoryId: string, orderedTarget: string[], orderedSource: string[] ) => { let prevState: CategoryDTO[] = []; setCategories((prev) => { prevState = prev; const sourceCat = prev.find((c) => c.id === fromCategoryId); const movedGroup = sourceCat?.groups.find((g) => g.id === groupId); if (!movedGroup) return prev; return prev.map((c) => { if (c.id === fromCategoryId) { const byId = new Map(c.groups.map((g) => [g.id, g])); return { ...c, groups: orderedSource.map((id, order) => ({ ...byId.get(id)!, order })), }; } if (c.id === toCategoryId) { const byId = new Map(c.groups.map((g) => [g.id, g])); byId.set(groupId, movedGroup); return { ...c, groups: orderedTarget.map((id, order) => ({ ...byId.get(id)!, categoryId: toCategoryId, order, })), }; } return c; }); }); try { await moveGroupToCategory(groupId, toCategoryId, orderedTarget, orderedSource); } catch { setCategories(prevState); toast.error("Couldn't move group. Try again."); } }, [] ); const saveNote = useCallback( async (groupId: string, categoryId: string, noteContent: string) => { try { await updateGroupNote(groupId, noteContent); updateGroupInState(categoryId, groupId, (g) => ({ ...g, noteContent })); return true; } catch { toast.error("Couldn't save note. Try again."); return false; } }, [updateGroupInState] ); const addTodo = useCallback( async (groupId: string, categoryId: string, title: string, details?: string) => { try { const todo = await createTodo(groupId, title, details); updateGroupInState(categoryId, groupId, (g) => ({ ...g, todos: [...g.todos, todo] })); return true; } catch { toast.error("Couldn't add to-do. Try again."); return false; } }, [updateGroupInState] ); const editTodo = useCallback( async ( todoId: string, groupId: string, categoryId: string, data: { title?: string; details?: string | null } ) => { try { await updateTodoAction(todoId, data); updateGroupInState(categoryId, groupId, (g) => ({ ...g, todos: g.todos.map((t) => (t.id === todoId ? { ...t, ...data } : t)), })); return true; } catch { toast.error("Couldn't update to-do. Try again."); return false; } }, [updateGroupInState] ); const toggleTodoDone = useCallback( async (todoId: string, groupId: string, categoryId: string, completed: boolean) => { let prevTodos: TodoDTO[] = []; updateCategory(categoryId, (c) => ({ ...c, groups: c.groups.map((g) => { if (g.id !== groupId) return g; prevTodos = g.todos; return { ...g, todos: g.todos.map((t) => (t.id === todoId ? { ...t, completed } : t)) }; }), })); try { await toggleTodoAction(todoId, completed); } catch { updateGroupInState(categoryId, groupId, (g) => ({ ...g, todos: prevTodos })); toast.error("Couldn't update to-do. Try again."); } }, [updateCategory, updateGroupInState] ); const removeTodo = useCallback( async (todoId: string, groupId: string, categoryId: string) => { let prevTodos: TodoDTO[] = []; updateCategory(categoryId, (c) => ({ ...c, groups: c.groups.map((g) => { if (g.id !== groupId) return g; prevTodos = g.todos; return { ...g, todos: g.todos.filter((t) => t.id !== todoId) }; }), })); try { await deleteTodoAction(todoId); } catch { updateGroupInState(categoryId, groupId, (g) => ({ ...g, todos: prevTodos })); toast.error("Couldn't delete to-do. Try again."); } }, [updateCategory, updateGroupInState] ); return ( {children} ); } export function useBoard() { const ctx = useContext(BoardContext); if (!ctx) throw new Error("useBoard must be used within a BoardProvider"); return ctx; }