diff --git a/apps/client/package.json b/apps/client/package.json
index b36b483..ac86f09 100644
--- a/apps/client/package.json
+++ b/apps/client/package.json
@@ -9,6 +9,8 @@
"preview": "vite preview"
},
"dependencies": {
+ "@emoji-mart/data": "^1.1.2",
+ "@emoji-mart/react": "^1.1.1",
"@mantine/core": "^7.2.2",
"@mantine/form": "^7.2.2",
"@mantine/hooks": "^7.2.2",
@@ -20,6 +22,7 @@
"axios": "^1.6.2",
"clsx": "^2.0.0",
"date-fns": "^2.30.0",
+ "emoji-mart": "^5.5.2",
"jotai": "^2.5.1",
"jotai-optics": "^0.3.1",
"js-cookie": "^3.0.5",
diff --git a/apps/client/src/components/emoji-picker.tsx b/apps/client/src/components/emoji-picker.tsx
new file mode 100644
index 0000000..e7bbef1
--- /dev/null
+++ b/apps/client/src/components/emoji-picker.tsx
@@ -0,0 +1,39 @@
+import React, { ReactNode } from 'react';
+import data from '@emoji-mart/data';
+import Picker from '@emoji-mart/react';
+import { ActionIcon, Popover } from '@mantine/core';
+import { useDisclosure } from '@mantine/hooks';
+
+export interface EmojiPickerInterface {
+ onEmojiSelect: (emoji: any) => void;
+ icon: ReactNode;
+}
+
+function EmojiPicker({ onEmojiSelect, icon }: EmojiPickerInterface) {
+ const [opened, handlers] = useDisclosure(false);
+
+ const handleEmojiSelect = (emoji) => {
+ onEmojiSelect(emoji);
+ handlers.close();
+ };
+
+ return (
+
+
+
+ {icon}
+
+
+
+
+
+
+ );
+}
+
+export default EmojiPicker;
diff --git a/apps/client/src/features/page/queries/page-query.ts b/apps/client/src/features/page/queries/page-query.ts
index 65ecf83..3cdfa56 100644
--- a/apps/client/src/features/page/queries/page-query.ts
+++ b/apps/client/src/features/page/queries/page-query.ts
@@ -1,25 +1,38 @@
-import { useMutation, useQuery, UseQueryResult, useQueryClient } from '@tanstack/react-query';
+import {
+ useMutation,
+ useQuery,
+ UseQueryResult,
+} from "@tanstack/react-query";
import {
createPage,
deletePage,
getPageById,
+ getPages,
getRecentChanges,
updatePage,
-} from '@/features/page/services/page-service';
-import { IPage } from '@/features/page/types/page.types';
-import { notifications } from '@mantine/notifications';
+} from "@/features/page/services/page-service";
+import { IPage } from "@/features/page/types/page.types";
+import { notifications } from "@mantine/notifications";
-const RECENT_CHANGES_KEY = ['recentChanges'];
+const RECENT_CHANGES_KEY = ["recentChanges"];
export function usePageQuery(pageId: string): UseQueryResult {
return useQuery({
- queryKey: ['pages', pageId],
+ queryKey: ["pages", pageId],
queryFn: () => getPageById(pageId),
enabled: !!pageId,
staleTime: 5 * 60 * 1000,
});
}
+export function useGetPagesQuery(): UseQueryResult {
+ return useQuery({
+ queryKey: ["pages"],
+ queryFn: () => getPages(),
+ staleTime: 5 * 60 * 1000,
+ });
+}
+
export function useRecentChangesQuery(): UseQueryResult {
return useQuery({
queryKey: RECENT_CHANGES_KEY,
@@ -31,17 +44,14 @@ export function useRecentChangesQuery(): UseQueryResult {
export function useCreatePageMutation() {
return useMutation>({
mutationFn: (data) => createPage(data),
+ onSuccess: (data) => {},
});
}
export function useUpdatePageMutation() {
- const queryClient = useQueryClient();
-
return useMutation>({
mutationFn: (data) => updatePage(data),
- onSuccess: (data) => {
- queryClient.setQueryData(['pages', data.id], data);
- },
+ onSuccess: (data) => {},
});
}
@@ -49,7 +59,7 @@ export function useDeletePageMutation() {
return useMutation({
mutationFn: (pageId: string) => deletePage(pageId),
onSuccess: () => {
- notifications.show({ message: 'Page deleted successfully' });
+ notifications.show({ message: "Page deleted successfully" });
},
});
}
diff --git a/apps/client/src/features/page/tree/page-tree.tsx b/apps/client/src/features/page/tree/page-tree.tsx
index 2cb43be..2713f9a 100644
--- a/apps/client/src/features/page/tree/page-tree.tsx
+++ b/apps/client/src/features/page/tree/page-tree.tsx
@@ -23,24 +23,28 @@ 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 { getPages } from '@/features/page/services/page-service';
import useWorkspacePageOrder from '@/features/page/tree/hooks/use-workspace-page-order';
import { useNavigate, useParams } from 'react-router-dom';
-import { convertToTree } from '@/features/page/tree/utils';
+import { convertToTree, updateTreeNodeIcon } from '@/features/page/tree/utils';
+import { useGetPagesQuery, useUpdatePageMutation } from '@/features/page/queries/page-query';
+import EmojiPicker from '@/components/emoji-picker';
+import { treeDataAtom } from '@/features/page/tree/atoms/tree-data-atom';
export default function PageTree() {
const { data, setData, controllers } = usePersistence>();
const [tree, setTree] = useAtom>(treeApiAtom);
const { data: pageOrderData } = useWorkspacePageOrder();
+ const { data: pagesData, isLoading } = useGetPagesQuery();
const rootElement = useRef();
const { pageId } = useParams();
const fetchAndSetTreeData = async () => {
if (pageOrderData?.childrenIds) {
try {
- const pages = await getPages();
- const treeData = convertToTree(pages, pageOrderData.childrenIds);
- setData(treeData);
+ if (!isLoading) {
+ const treeData = convertToTree(pagesData, pageOrderData.childrenIds);
+ setData(treeData);
+ }
} catch (err) {
console.error('Error fetching tree data: ', err);
}
@@ -49,7 +53,7 @@ export default function PageTree() {
useEffect(() => {
fetchAndSetTreeData();
- }, [pageOrderData?.childrenIds]);
+ }, [pageOrderData?.childrenIds, isLoading]);
useEffect(() => {
setTimeout(() => {
@@ -87,11 +91,28 @@ export default function PageTree() {
function Node({ node, style, dragHandle }: NodeRendererProps) {
const navigate = useNavigate();
+ const updatePageMutation = useUpdatePageMutation();
+ const [treeData, setTreeData] = useAtom(treeDataAtom);
const handleClick = () => {
navigate(`/p/${node.id}`);
};
+ const handleUpdateNodeIcon = (nodeId, newIcon) => {
+ const updatedTreeData = updateTreeNodeIcon(treeData, nodeId, newIcon);
+ setTreeData(updatedTreeData);
+ };
+
+ const handleEmojiIconClick = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ const handleEmojiSelect = (emoji) => {
+ handleUpdateNodeIcon(node.id, emoji.native);
+ updatePageMutation.mutateAsync({ id: node.id, icon: emoji.native });
+ };
+
if (node.willReceiveDrop && node.isClosed) {
setTimeout(() => {
if (node.state.willReceiveDrop) node.open();
@@ -108,7 +129,13 @@ function Node({ node, style, dragHandle }: NodeRendererProps) {
>
-
+
+
+
+ }/>
+
{node.isEditing ? (
diff --git a/apps/client/src/features/page/tree/utils/index.ts b/apps/client/src/features/page/tree/utils/index.ts
index 4205f05..42a42c9 100644
--- a/apps/client/src/features/page/tree/utils/index.ts
+++ b/apps/client/src/features/page/tree/utils/index.ts
@@ -60,3 +60,15 @@ export const updateTreeNodeName = (nodes: TreeNode[], nodeId: string, newName: s
return node;
});
};
+
+export const updateTreeNodeIcon = (nodes: TreeNode[], nodeId: string, newIcon: string): TreeNode[] => {
+ return nodes.map(node => {
+ if (node.id === nodeId) {
+ return { ...node, icon: newIcon };
+ }
+ if (node.children && node.children.length > 0) {
+ return { ...node, children: updateTreeNodeIcon(node.children, nodeId, newIcon) };
+ }
+ return node;
+ });
+};
diff --git a/apps/server/src/core/page/dto/create-page.dto.ts b/apps/server/src/core/page/dto/create-page.dto.ts
index 3fa4ca2..8881eb3 100644
--- a/apps/server/src/core/page/dto/create-page.dto.ts
+++ b/apps/server/src/core/page/dto/create-page.dto.ts
@@ -9,6 +9,10 @@ export class CreatePageDto {
@IsString()
title?: string;
+ @IsOptional()
+ @IsString()
+ icon?: string;
+
@IsOptional()
@IsString()
parentPageId?: string;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 72af98e..c19b4eb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -111,6 +111,12 @@ importers:
apps/client:
dependencies:
+ '@emoji-mart/data':
+ specifier: ^1.1.2
+ version: 1.1.2
+ '@emoji-mart/react':
+ specifier: ^1.1.1
+ version: 1.1.1(emoji-mart@5.5.2)(react@18.2.0)
'@mantine/core':
specifier: ^7.2.2
version: 7.4.0(@mantine/hooks@7.4.0)(@types/react@18.2.47)(react-dom@18.2.0)(react@18.2.0)
@@ -144,6 +150,9 @@ importers:
date-fns:
specifier: ^2.30.0
version: 2.30.0
+ emoji-mart:
+ specifier: ^5.5.2
+ version: 5.5.2
jotai:
specifier: ^2.5.1
version: 2.6.1(@types/react@18.2.47)(react@18.2.0)
@@ -2377,6 +2386,20 @@ packages:
dependencies:
'@jridgewell/trace-mapping': 0.3.9
+ /@emoji-mart/data@1.1.2:
+ resolution: {integrity: sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==}
+ dev: false
+
+ /@emoji-mart/react@1.1.1(emoji-mart@5.5.2)(react@18.2.0):
+ resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==}
+ peerDependencies:
+ emoji-mart: ^5.2
+ react: ^16.8 || ^17 || ^18
+ dependencies:
+ emoji-mart: 5.5.2
+ react: 18.2.0
+ dev: false
+
/@esbuild/aix-ppc64@0.19.11:
resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==}
engines: {node: '>=12'}
@@ -6567,6 +6590,10 @@ packages:
engines: {node: '>=12'}
dev: true
+ /emoji-mart@5.5.2:
+ resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==}
+ dev: false
+
/emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}