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 e9d91cb2..dea505fe 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 d8fe45f8..f6292443 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 93d4e6f0..fa9c7c90 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 1728a08d..64715872 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 a86ddf58..18492e04 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 0281dd8d..cb8b28d8 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 9c061979..458f899d 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 }[]; +}