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 }[]; +}