From 68c56060b38fb545484b6199665a377eaed36582 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 16 Jan 2026 21:05:43 -0800 Subject: [PATCH] Add board state management with updateBoard and saveBoard methods. BoardProvider now manages local board state and exposes updateBoard for editing and saveBoard for persisting to the database. Supports both create and edit modes with proper redirect after creation. Co-Authored-By: Claude Opus 4.5 --- package.json | 1 + pnpm-lock.yaml | 14 ++++ src/app/(main)/boards/BoardProvider.tsx | 83 +++++++++++++++++-- src/app/(main)/boards/[boardId]/BoardBody.tsx | 68 ++++++++++++++- .../(main)/boards/[boardId]/BoardHeader.tsx | 50 +++-------- src/components/hooks/context/useBoard.ts | 4 +- src/lib/types.ts | 4 + 7 files changed, 176 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index e9d91cb2e..dea505fee 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "react-dom": "^19.2.3", "react-error-boundary": "^4.0.4", "react-intl": "^7.1.14", + "react-resizable-panels": "^4.4.1", "react-simple-maps": "^2.3.0", "react-use-measure": "^2.0.4", "react-window": "^1.8.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8fe45f8a..f62924439 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,6 +164,9 @@ importers: react-intl: specifier: ^7.1.14 version: 7.1.14(react@19.2.3)(typescript@5.9.3) + react-resizable-panels: + specifier: ^4.4.1 + version: 4.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-simple-maps: specifier: ^2.3.0 version: 2.3.0(prop-types@15.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -6181,6 +6184,12 @@ packages: redux: optional: true + react-resizable-panels@4.4.1: + resolution: {integrity: sha512-dpM9oI6rGlAq7VYDeafSRA1JmkJv8aNuKySR+tZLQQLfaeqTnQLSM52EcoI/QdowzsjVUCk6jViKS0xHWITVRQ==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + react-simple-maps@2.3.0: resolution: {integrity: sha512-IZVeiPSRZKwD6I/2NvXpQ2uENYGDGZp8DvZjkapcxuJ/LQHTfl+Byb+KNgY7s+iatRA2ad8LnZ3AgqcjziCCsw==} peerDependencies: @@ -13909,6 +13918,11 @@ snapshots: '@types/react': 19.2.8 redux: 5.0.1 + react-resizable-panels@4.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-simple-maps@2.3.0(prop-types@15.8.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: d3-geo: 2.0.2 diff --git a/src/app/(main)/boards/BoardProvider.tsx b/src/app/(main)/boards/BoardProvider.tsx index 93d4e6f04..fa9c7c901 100644 --- a/src/app/(main)/boards/BoardProvider.tsx +++ b/src/app/(main)/boards/BoardProvider.tsx @@ -1,17 +1,86 @@ 'use client'; -import { Loading } from '@umami/react-zen'; -import { createContext, type ReactNode } from 'react'; +import { Loading, useToast } from '@umami/react-zen'; +import { createContext, type ReactNode, useCallback, useEffect, useState } from 'react'; +import { useApi, useMessages, useModified, useNavigation } from '@/components/hooks'; import { useBoardQuery } from '@/components/hooks/queries/useBoardQuery'; import type { Board } from '@/generated/prisma/client'; -export const BoardContext = createContext(null); +export interface BoardContextValue { + board: Partial; + updateBoard: (data: Partial) => void; + saveBoard: () => Promise; + isPending: boolean; +} -export function BoardProvider({ boardId, children }: { boardId: string; children: ReactNode }) { - const { data: board, isFetching, isLoading } = useBoardQuery(boardId); +export const BoardContext = createContext(null); - if (isFetching && isLoading) { +const defaultBoard: Partial = { + name: '', + description: '', +}; + +export function BoardProvider({ boardId, children }: { boardId?: string; children: ReactNode }) { + const { data, isFetching, isLoading } = useBoardQuery(boardId); + const { post, useMutation } = useApi(); + const { touch } = useModified(); + const { toast } = useToast(); + const { formatMessage, labels, messages } = useMessages(); + const { router, renderUrl } = useNavigation(); + + const [board, setBoard] = useState>(data ?? defaultBoard); + + useEffect(() => { + if (data) { + setBoard(data); + } + }, [data]); + + const { mutateAsync, isPending } = useMutation({ + mutationFn: (boardData: Partial) => { + if (boardData.id) { + return post(`/boards/${boardData.id}`, boardData); + } + return post('/boards', { ...boardData, type: 'dashboard', slug: '' }); + }, + }); + + const updateBoard = useCallback((data: Partial) => { + setBoard(current => ({ ...current, ...data })); + }, []); + + const saveBoard = useCallback(async () => { + const defaultName = formatMessage(labels.untitled); + const result = await mutateAsync({ ...board, name: board.name || defaultName }); + + toast(formatMessage(messages.saved)); + touch('boards'); + + if (board.id) { + touch(`board:${board.id}`); + } else if (result?.id) { + router.push(renderUrl(`/boards/${result.id}`)); + } + + return result; + }, [ + board, + mutateAsync, + toast, + formatMessage, + labels.untitled, + messages.saved, + touch, + router, + renderUrl, + ]); + + if (boardId && isFetching && isLoading) { return ; } - return {children}; + return ( + + {children} + + ); } diff --git a/src/app/(main)/boards/[boardId]/BoardBody.tsx b/src/app/(main)/boards/[boardId]/BoardBody.tsx index 1728a08db..647158728 100644 --- a/src/app/(main)/boards/[boardId]/BoardBody.tsx +++ b/src/app/(main)/boards/[boardId]/BoardBody.tsx @@ -1,3 +1,69 @@ +import { Button, Column, Icon, Row } from '@umami/react-zen'; +import { produce } from 'immer'; +import { v4 as uuid } from 'uuid'; +import { useBoard } from '@/components/hooks'; +import { Plus } from '@/components/icons'; + export function BoardBody() { - return

i am bored.

; + const { board, updateBoard, saveBoard, isPending } = useBoard(); + + console.log({ board }); + + const handleAddRow = () => { + updateBoard({ + parameters: produce(board.parameters, draft => { + if (!draft.rows) { + draft.rows = []; + } + draft.rows.push({ id: uuid(), components: [] }); + }), + }); + }; + + return ( + + {board?.parameters?.rows?.map((row, rowIndex) => { + return ; + })} + + + + + ); +} + +function BoardComponent() { + return hi; +} + +function BoardRow({ rowIndex, components }: { rowIndex: number; components: any[] }) { + const { board, updateBoard } = useBoard(); + + const handleAddComponent = () => { + updateBoard({ + parameters: produce(board.parameters, draft => { + if (!draft.rows[rowIndex]) { + draft.rows[rowIndex] = { id: uuid(), components: [] }; + } + draft.rows[rowIndex].components.push({ id: uuid(), type: 'text', value: '' }); + }), + }); + }; + + return ( + + {components?.map(component => { + return ; + })} + + + ); } diff --git a/src/app/(main)/boards/[boardId]/BoardHeader.tsx b/src/app/(main)/boards/[boardId]/BoardHeader.tsx index a86ddf581..18492e04a 100644 --- a/src/app/(main)/boards/[boardId]/BoardHeader.tsx +++ b/src/app/(main)/boards/[boardId]/BoardHeader.tsx @@ -1,47 +1,21 @@ -import { Column, Grid, Heading, LoadingButton, Row, TextField, useToast } from '@umami/react-zen'; -import { useState } from 'react'; -import { useApi, useBoard, useMessages, useModified, useNavigation } from '@/components/hooks'; +import { Column, Grid, Heading, LoadingButton, Row, TextField } from '@umami/react-zen'; +import { useBoard, useMessages } from '@/components/hooks'; export function BoardHeader() { - const board = useBoard(); - const { formatMessage, labels, messages } = useMessages(); - const { post, useMutation } = useApi(); - const { touch } = useModified(); - const { router, renderUrl } = useNavigation(); - const { toast } = useToast(); + const { board, updateBoard, saveBoard, isPending } = useBoard(); + const { formatMessage, labels } = useMessages(); const defaultName = formatMessage(labels.untitled); - const [name, setName] = useState(board?.name ?? ''); - const [description, setDescription] = useState(board?.description ?? ''); - - const { mutateAsync, isPending } = useMutation({ - mutationFn: (data: { name: string; description: string }) => { - if (board) { - return post(`/boards/${board.id}`, data); - } - return post('/boards', { ...data, type: 'dashboard', slug: '' }); - }, - }); - const handleNameChange = (value: string) => { - setName(value); + updateBoard({ name: value }); }; const handleDescriptionChange = (value: string) => { - setDescription(value); + updateBoard({ description: value }); }; - const handleSave = async () => { - const result = await mutateAsync({ name: name || defaultName, description }); - - toast(formatMessage(messages.saved)); - touch('boards'); - - if (board) { - touch(`board:${board.id}`); - } else if (result?.id) { - router.push(renderUrl(`/boards/${result.id}`)); - } + const handleSave = () => { + saveBoard(); }; return ( @@ -57,26 +31,26 @@ export function BoardHeader() { - {name} + {board?.name} - {description} + {board?.description} diff --git a/src/components/hooks/context/useBoard.ts b/src/components/hooks/context/useBoard.ts index 0281dd8d3..cb8b28d8a 100644 --- a/src/components/hooks/context/useBoard.ts +++ b/src/components/hooks/context/useBoard.ts @@ -1,6 +1,6 @@ import { useContext } from 'react'; -import { BoardContext } from '@/app/(main)/boards/BoardProvider'; +import { BoardContext, type BoardContextValue } from '@/app/(main)/boards/BoardProvider'; -export function useBoard() { +export function useBoard(): BoardContextValue { return useContext(BoardContext); } diff --git a/src/lib/types.ts b/src/lib/types.ts index 9c0619794..458f899dc 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -141,3 +141,7 @@ export interface ApiError extends Error { code?: string; message: string; } + +export interface BoardData { + rows: { id: string; name: string; value: number }[]; +}