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==}