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;