diff --git a/apps/client/src/components/navbar/navbar.tsx b/apps/client/src/components/navbar/navbar.tsx index 8087321c..8a38eb35 100644 --- a/apps/client/src/components/navbar/navbar.tsx +++ b/apps/client/src/components/navbar/navbar.tsx @@ -22,6 +22,7 @@ import { SearchSpotlight } from "@/features/search/search-spotlight"; import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom"; import PageTree from "@/features/page/tree/page-tree"; import { useNavigate } from "react-router-dom"; +import SpaceContent from "@/features/page/component/space-content.tsx"; interface PrimaryMenuItem { icon: React.ElementType; @@ -103,7 +104,8 @@ export function Navbar() {
- + + {/* */}
diff --git a/apps/client/src/components/providers/tanstack-provider.tsx b/apps/client/src/components/providers/tanstack-provider.tsx index c4e9c773..8292bdd2 100644 --- a/apps/client/src/components/providers/tanstack-provider.tsx +++ b/apps/client/src/components/providers/tanstack-provider.tsx @@ -1,19 +1,18 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import React from 'react'; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React from "react"; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnMount: false, refetchOnWindowFocus: false, + retry: false, }, }, }); export function TanstackProvider({ children }: React.PropsWithChildren) { return ( - - {children} - + {children} ); } diff --git a/apps/client/src/features/editor/title-editor.tsx b/apps/client/src/features/editor/title-editor.tsx index 0e94e2fd..88dc3290 100644 --- a/apps/client/src/features/editor/title-editor.tsx +++ b/apps/client/src/features/editor/title-editor.tsx @@ -1,17 +1,20 @@ -import '@/features/editor/styles/index.css'; -import React, { useEffect, useState } from 'react'; -import { EditorContent, useEditor } from '@tiptap/react'; -import { Document } from '@tiptap/extension-document'; -import { Heading } from '@tiptap/extension-heading'; -import { Text } from '@tiptap/extension-text'; -import { Placeholder } from '@tiptap/extension-placeholder'; -import { useAtomValue } from 'jotai'; -import { pageEditorAtom, titleEditorAtom } from '@/features/editor/atoms/editor-atoms'; -import { useUpdatePageMutation } from '@/features/page/queries/page-query'; -import { useDebouncedValue } from '@mantine/hooks'; -import { useAtom } from 'jotai'; -import { treeDataAtom } from '@/features/page/tree/atoms/tree-data-atom'; -import { updateTreeNodeName } from '@/features/page/tree/utils'; +import "@/features/editor/styles/index.css"; +import React, { useEffect, useState } from "react"; +import { EditorContent, useEditor } from "@tiptap/react"; +import { Document } from "@tiptap/extension-document"; +import { Heading } from "@tiptap/extension-heading"; +import { Text } from "@tiptap/extension-text"; +import { Placeholder } from "@tiptap/extension-placeholder"; +import { useAtomValue } from "jotai"; +import { + pageEditorAtom, + titleEditorAtom, +} from "@/features/editor/atoms/editor-atoms"; +import { useUpdatePageMutation } from "@/features/page/queries/page-query"; +import { useDebouncedValue } from "@mantine/hooks"; +import { useAtom } from "jotai"; +import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom"; +import { updateTreeNodeName } from "@/features/page/tree/utils"; export interface TitleEditorProps { pageId: string; @@ -19,7 +22,7 @@ export interface TitleEditorProps { } export function TitleEditor({ pageId, title }: TitleEditorProps) { - const [debouncedTitleState, setDebouncedTitleState] = useState(''); + const [debouncedTitleState, setDebouncedTitleState] = useState(""); const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 1000); const updatePageMutation = useUpdatePageMutation(); const pageEditor = useAtomValue(pageEditorAtom); @@ -29,14 +32,14 @@ export function TitleEditor({ pageId, title }: TitleEditorProps) { const titleEditor = useEditor({ extensions: [ Document.extend({ - content: 'heading', + content: "heading", }), Heading.configure({ levels: [1], }), Text, Placeholder.configure({ - placeholder: 'Untitled', + placeholder: "Untitled", }), ], onCreate({ editor }) { @@ -53,8 +56,8 @@ export function TitleEditor({ pageId, title }: TitleEditorProps) { }); useEffect(() => { - if (debouncedTitle !== '') { - updatePageMutation.mutate({ id: pageId, title: debouncedTitle }); + if (debouncedTitle !== "") { + updatePageMutation.mutate({ pageId, title: debouncedTitle }); const newTreeData = updateTreeNodeName(treeData, pageId, debouncedTitle); setTreeData(newTreeData); @@ -69,7 +72,7 @@ export function TitleEditor({ pageId, title }: TitleEditorProps) { useEffect(() => { setTimeout(() => { - titleEditor?.commands.focus('end'); + titleEditor?.commands.focus("end"); }, 500); }, [titleEditor]); @@ -79,15 +82,15 @@ export function TitleEditor({ pageId, title }: TitleEditorProps) { const { key } = event; const { $head } = titleEditor.state.selection; - const shouldFocusEditor = (key === 'Enter' || key === 'ArrowDown') || - (key === 'ArrowRight' && !$head.nodeAfter); + const shouldFocusEditor = + key === "Enter" || + key === "ArrowDown" || + (key === "ArrowRight" && !$head.nodeAfter); if (shouldFocusEditor) { - pageEditor.commands.focus('start'); + pageEditor.commands.focus("start"); } } - return ( - - ); + return ; } diff --git a/apps/client/src/features/page/component/space-content.tsx b/apps/client/src/features/page/component/space-content.tsx new file mode 100644 index 00000000..8ddb10d5 --- /dev/null +++ b/apps/client/src/features/page/component/space-content.tsx @@ -0,0 +1,64 @@ +import { useSpaceQuery } from "@/features/space/queries/space-query.ts"; +import { useAtom } from "jotai/index"; +import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { + Accordion, + AccordionControlProps, + ActionIcon, + Center, + rem, + Tooltip, +} from "@mantine/core"; +import { IconPlus } from "@tabler/icons-react"; +import React from "react"; +import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts"; +import PageTree from "@/features/page/tree/page-tree.tsx"; + +export default function SpaceContent() { + const [currentUser] = useAtom(currentUserAtom); + const { data: space } = useSpaceQuery(currentUser?.workspace.defaultSpaceId); + + if (!space) { + return
Loading...
; + } + + return ( + <> + + + {space.name} + + + + + + + ); +} + +function AccordionControl(props: AccordionControlProps) { + const [tree] = useAtom(treeApiAtom); + + function handleCreatePage() { + tree?.create({ parentId: null, type: "internal", index: 0 }); + } + + return ( +
+ + {/* + + */} + + + + + +
+ ); +} diff --git a/apps/client/src/features/page/queries/page-query.ts b/apps/client/src/features/page/queries/page-query.ts index 3cdfa56d..db60215d 100644 --- a/apps/client/src/features/page/queries/page-query.ts +++ b/apps/client/src/features/page/queries/page-query.ts @@ -1,17 +1,14 @@ -import { - useMutation, - useQuery, - UseQueryResult, -} from "@tanstack/react-query"; +import { useMutation, useQuery, UseQueryResult } from "@tanstack/react-query"; import { createPage, deletePage, getPageById, getPages, getRecentChanges, + getSpacePageOrder, updatePage, } from "@/features/page/services/page-service"; -import { IPage } from "@/features/page/types/page.types"; +import { IPage, IWorkspacePageOrder } from "@/features/page/types/page.types"; import { notifications } from "@mantine/notifications"; const RECENT_CHANGES_KEY = ["recentChanges"]; @@ -25,10 +22,12 @@ export function usePageQuery(pageId: string): UseQueryResult { }); } -export function useGetPagesQuery(): UseQueryResult { +export function useGetPagesQuery( + spaceId: string, +): UseQueryResult { return useQuery({ - queryKey: ["pages"], - queryFn: () => getPages(), + queryKey: ["pages", spaceId], + queryFn: () => getPages(spaceId), staleTime: 5 * 60 * 1000, }); } @@ -63,3 +62,14 @@ export function useDeletePageMutation() { }, }); } + +export default function useSpacePageOrder( + spaceId: string, +): UseQueryResult { + return useQuery({ + queryKey: ["page-order", spaceId], + queryFn: async () => { + return await getSpacePageOrder(spaceId); + }, + }); +} diff --git a/apps/client/src/features/page/services/page-service.ts b/apps/client/src/features/page/services/page-service.ts index f3dbfeb4..ddd4e54c 100644 --- a/apps/client/src/features/page/services/page-service.ts +++ b/apps/client/src/features/page/services/page-service.ts @@ -1,28 +1,36 @@ -import api from '@/lib/api-client'; -import { IMovePage, IPage, IWorkspacePageOrder } from '@/features/page/types/page.types'; +import api from "@/lib/api-client"; +import { + IMovePage, + IPage, + IWorkspacePageOrder, +} from "@/features/page/types/page.types"; export async function createPage(data: Partial): Promise { - const req = await api.post('/pages/create', data); + const req = await api.post("/pages/create", data); return req.data as IPage; } -export async function getPageById(id: string): Promise { - const req = await api.post('/pages/info', { id }); +export async function getPageById(pageId: string): Promise { + const req = await api.post("/pages/info", { pageId }); return req.data as IPage; } export async function getRecentChanges(): Promise { - const req = await api.post('/pages/recent'); + const req = await api.post("/pages/recent"); return req.data as IPage[]; } -export async function getPages(): Promise { - const req = await api.post('/pages'); +export async function getPages(spaceId: string): Promise { + const req = await api.post("/pages", { spaceId }); return req.data as IPage[]; } -export async function getWorkspacePageOrder(): Promise { - const req = await api.post('/pages/ordering'); +export async function getSpacePageOrder( + spaceId: string, +): Promise { + const req = await api.post("/pages/ordering", { + spaceId, + }); return req.data as IWorkspacePageOrder[]; } @@ -32,9 +40,9 @@ export async function updatePage(data: Partial): Promise { } export async function movePage(data: IMovePage): Promise { - await api.post('/pages/move', data); + await api.post("/pages/move", data); } export async function deletePage(id: string): Promise { - await api.post('/pages/delete', { id }); + await api.post("/pages/delete", { id }); } diff --git a/apps/client/src/features/page/tree/hooks/use-persistence.ts b/apps/client/src/features/page/tree/hooks/use-persistence.ts index 6a58b898..609be350 100644 --- a/apps/client/src/features/page/tree/hooks/use-persistence.ts +++ b/apps/client/src/features/page/tree/hooks/use-persistence.ts @@ -1,21 +1,28 @@ -import { useMemo } from 'react'; +import { useMemo } from "react"; import { CreateHandler, DeleteHandler, MoveHandler, RenameHandler, SimpleTree, -} from 'react-arborist'; -import { useAtom } from 'jotai'; -import { treeDataAtom } from '@/features/page/tree/atoms/tree-data-atom'; -import { movePage } from '@/features/page/services/page-service'; -import { v4 as uuidv4 } from 'uuid'; -import { IMovePage } from '@/features/page/types/page.types'; -import { useNavigate } from 'react-router-dom'; -import { TreeNode } from '@/features/page/tree/types'; -import { useCreatePageMutation, useDeletePageMutation, useUpdatePageMutation } from '@/features/page/queries/page-query'; +} from "react-arborist"; +import { useAtom } from "jotai"; +import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom"; +import { movePage } from "@/features/page/services/page-service"; +import { v4 as uuidv4 } from "uuid"; +import { IMovePage } from "@/features/page/types/page.types"; +import { useNavigate } from "react-router-dom"; +import { TreeNode } from "@/features/page/tree/types"; +import { + useCreatePageMutation, + useDeletePageMutation, + useUpdatePageMutation, +} from "@/features/page/queries/page-query"; -export function usePersistence() { +interface Props { + spaceId: string; +} +export function usePersistence(spaceId: string) { const [data, setData] = useAtom(treeDataAtom); const createPageMutation = useCreatePageMutation(); const updatePageMutation = useUpdatePageMutation(); @@ -25,7 +32,13 @@ export function usePersistence() { const tree = useMemo(() => new SimpleTree(data), [data]); - const onMove: MoveHandler = (args: { parentId, index, parentNode, dragNodes, dragIds }) => { + const onMove: MoveHandler = (args: { + parentId; + index; + parentNode; + dragNodes; + dragIds; + }) => { for (const id of args.dragIds) { tree.move({ id, parentId: args.parentId, index: args.index }); } @@ -33,25 +46,30 @@ export function usePersistence() { const newDragIndex = tree.find(args.dragIds[0])?.childIndex; - const currentTreeData = args.parentId ? tree.find(args.parentId).children : tree.data; + const currentTreeData = args.parentId + ? tree.find(args.parentId).children + : tree.data; const afterId = currentTreeData[newDragIndex - 1]?.id || null; - const beforeId = !afterId && currentTreeData[newDragIndex + 1]?.id || null; + const beforeId = + (!afterId && currentTreeData[newDragIndex + 1]?.id) || null; const params: IMovePage = { - id: args.dragIds[0], + pageId: args.dragIds[0], after: afterId, before: beforeId, parentId: args.parentId || null, }; const payload = Object.fromEntries( - Object.entries(params).filter(([key, value]) => value !== null && value !== undefined), + Object.entries(params).filter( + ([key, value]) => value !== null && value !== undefined, + ), ); try { movePage(payload as IMovePage); } catch (error) { - console.error('Error moving page:', error); + console.error("Error moving page:", error); } }; @@ -60,28 +78,32 @@ export function usePersistence() { setData(tree.data); try { - updatePageMutation.mutateAsync({ id, title: name }); + updatePageMutation.mutateAsync({ pageId: id, title: name }); } catch (error) { - console.error('Error updating page title:', error); + console.error("Error updating page title:", error); } }; const onCreate: CreateHandler = async ({ parentId, index, type }) => { - const data = { id: uuidv4(), name: '' } as any; + const data = { id: uuidv4(), name: "" } as any; data.children = []; tree.create({ parentId, index, data }); setData(tree.data); - const payload: { id: string; parentPageId?: string } = { id: data.id }; + const payload: { pageId: string; parentPageId?: string; spaceId: string } = + { + pageId: data.id, + spaceId: spaceId, + }; if (parentId) { payload.parentPageId = parentId; } try { await createPageMutation.mutateAsync(payload); - navigate(`/p/${payload.id}`); + navigate(`/p/${payload.pageId}`); } catch (error) { - console.error('Error creating the page:', error); + console.error("Error creating the page:", error); } return data; @@ -93,9 +115,9 @@ export function usePersistence() { try { await deletePageMutation.mutateAsync(args.ids[0]); - navigate('/home'); + navigate("/home"); } catch (error) { - console.error('Error deleting page:', error); + console.error("Error deleting page:", error); } }; diff --git a/apps/client/src/features/page/tree/hooks/use-workspace-page-order.ts b/apps/client/src/features/page/tree/hooks/use-workspace-page-order.ts deleted file mode 100644 index 694963a8..00000000 --- a/apps/client/src/features/page/tree/hooks/use-workspace-page-order.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useQuery, UseQueryResult } from "@tanstack/react-query"; -import { IWorkspacePageOrder } from '@/features/page/types/page.types'; -import { getWorkspacePageOrder } from '@/features/page/services/page-service'; - -export default function useWorkspacePageOrder(): UseQueryResult { - return useQuery({ - queryKey: ["workspace-page-order"], - queryFn: async () => { - return await getWorkspacePageOrder(); - }, - }); -} diff --git a/apps/client/src/features/page/tree/page-tree.tsx b/apps/client/src/features/page/tree/page-tree.tsx index dea9a25e..0335a35a 100644 --- a/apps/client/src/features/page/tree/page-tree.tsx +++ b/apps/client/src/features/page/tree/page-tree.tsx @@ -23,21 +23,25 @@ import { FillFlexParent } from "./components/fill-flex-parent"; import { TreeNode } from "./types"; import { treeApiAtom } from "./atoms/tree-api-atom"; import { usePersistence } from "@/features/page/tree/hooks/use-persistence"; -import useWorkspacePageOrder from "@/features/page/tree/hooks/use-workspace-page-order"; import { useNavigate, useParams } from "react-router-dom"; import { convertToTree, updateTreeNodeIcon } from "@/features/page/tree/utils"; -import { +import useSpacePageOrder, { useGetPagesQuery, useUpdatePageMutation, } from "@/features/page/queries/page-query"; import EmojiPicker from "@/components/ui/emoji-picker.tsx"; import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom"; -export default function PageTree() { - const { data, setData, controllers } = usePersistence>(); +interface PageTreeProps { + spaceId: string; +} + +export default function PageTree({ spaceId }: PageTreeProps) { + const { data, setData, controllers } = + usePersistence>(spaceId); const [tree, setTree] = useAtom>(treeApiAtom); - const { data: pageOrderData } = useWorkspacePageOrder(); - const { data: pagesData, isLoading } = useGetPagesQuery(); + const { data: pageOrderData } = useSpacePageOrder(spaceId); + const { data: pagesData, isLoading } = useGetPagesQuery(spaceId); const rootElement = useRef(); const { pageId } = useParams(); @@ -113,12 +117,12 @@ function Node({ node, style, dragHandle }: NodeRendererProps) { const handleEmojiSelect = (emoji) => { handleUpdateNodeIcon(node.id, emoji.native); - updatePageMutation.mutateAsync({ id: node.id, icon: emoji.native }); + updatePageMutation.mutateAsync({ pageId: node.id, icon: emoji.native }); }; const handleRemoveEmoji = () => { handleUpdateNodeIcon(node.id, null); - updatePageMutation.mutateAsync({ id: node.id, icon: null }); + updatePageMutation.mutateAsync({ pageId: node.id, icon: null }); }; if (node.willReceiveDrop && node.isClosed) { diff --git a/apps/client/src/features/page/tree/types.ts b/apps/client/src/features/page/tree/types.ts index 7ab9a4a5..8a0c8d17 100644 --- a/apps/client/src/features/page/tree/types.ts +++ b/apps/client/src/features/page/tree/types.ts @@ -1,7 +1,7 @@ export type TreeNode = { - id: string - name: string - icon?: string - slug?: string - children: TreeNode[] -} + id: string; + name: string; + icon?: string; + slug?: string; + children: TreeNode[]; +}; diff --git a/apps/client/src/features/page/types/page.types.ts b/apps/client/src/features/page/types/page.types.ts index 69ed1e6e..ab4cb96e 100644 --- a/apps/client/src/features/page/types/page.types.ts +++ b/apps/client/src/features/page/types/page.types.ts @@ -1,4 +1,5 @@ export interface IPage { + pageId: string; id: string; title: string; content: string; @@ -10,9 +11,10 @@ export interface IPage { shareId: string; parentPageId: string; creatorId: string; + spaceId: string; workspaceId: string; - children:[] - childrenIds:[] + children: []; + childrenIds: []; isLocked: boolean; status: string; publishedAt: Date; @@ -22,7 +24,7 @@ export interface IPage { } export interface IMovePage { - id: string; + pageId: string; after?: string; before?: string; parentId?: string; diff --git a/apps/client/src/features/workspace/queries/workspace-query.ts b/apps/client/src/features/workspace/queries/workspace-query.ts index bc31ec7e..6eb542ae 100644 --- a/apps/client/src/features/workspace/queries/workspace-query.ts +++ b/apps/client/src/features/workspace/queries/workspace-query.ts @@ -1,10 +1,24 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + useMutation, + useQuery, + useQueryClient, + UseQueryResult, +} from "@tanstack/react-query"; import { changeMemberRole, + getWorkspace, getWorkspaceMembers, } from "@/features/workspace/services/workspace-service"; import { QueryParams } from "@/lib/types.ts"; import { notifications } from "@mantine/notifications"; +import { IWorkspace } from "@/features/workspace/types/workspace.types.ts"; + +export function useWorkspace(): UseQueryResult { + return useQuery({ + queryKey: ["workspace"], + queryFn: () => getWorkspace(), + }); +} export function useWorkspaceMembersQuery(params?: QueryParams) { return useQuery({ diff --git a/apps/client/src/features/workspace/types/workspace.types.ts b/apps/client/src/features/workspace/types/workspace.types.ts index bee33355..ccfb5a5a 100644 --- a/apps/client/src/features/workspace/types/workspace.types.ts +++ b/apps/client/src/features/workspace/types/workspace.types.ts @@ -4,6 +4,7 @@ export interface IWorkspace { description: string; logo: string; hostname: string; + defaultSpaceId: string; customDomain: string; enableInvite: boolean; inviteCode: string;