page icon emoji picker

This commit is contained in:
Philipinho
2024-01-22 16:39:20 +01:00
parent 616da875cd
commit e05caef8fe
7 changed files with 141 additions and 19 deletions

View File

@ -9,6 +9,8 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"@mantine/core": "^7.2.2", "@mantine/core": "^7.2.2",
"@mantine/form": "^7.2.2", "@mantine/form": "^7.2.2",
"@mantine/hooks": "^7.2.2", "@mantine/hooks": "^7.2.2",
@ -20,6 +22,7 @@
"axios": "^1.6.2", "axios": "^1.6.2",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"emoji-mart": "^5.5.2",
"jotai": "^2.5.1", "jotai": "^2.5.1",
"jotai-optics": "^0.3.1", "jotai-optics": "^0.3.1",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",

View File

@ -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 (
<Popover
opened={opened}
onClose={handlers.close}
width={200}
position="bottom"
>
<Popover.Target>
<ActionIcon color="gray" variant="transparent" onClick={handlers.toggle}>
{icon}
</ActionIcon>
</Popover.Target>
<Popover.Dropdown bg="000" style={{ border: "none" }}>
<Picker data={data} onEmojiSelect={handleEmojiSelect} />
</Popover.Dropdown>
</Popover>
);
}
export default EmojiPicker;

View File

@ -1,25 +1,38 @@
import { useMutation, useQuery, UseQueryResult, useQueryClient } from '@tanstack/react-query'; import {
useMutation,
useQuery,
UseQueryResult,
} from "@tanstack/react-query";
import { import {
createPage, createPage,
deletePage, deletePage,
getPageById, getPageById,
getPages,
getRecentChanges, getRecentChanges,
updatePage, updatePage,
} from '@/features/page/services/page-service'; } from "@/features/page/services/page-service";
import { IPage } from '@/features/page/types/page.types'; import { IPage } from "@/features/page/types/page.types";
import { notifications } from '@mantine/notifications'; import { notifications } from "@mantine/notifications";
const RECENT_CHANGES_KEY = ['recentChanges']; const RECENT_CHANGES_KEY = ["recentChanges"];
export function usePageQuery(pageId: string): UseQueryResult<IPage, Error> { export function usePageQuery(pageId: string): UseQueryResult<IPage, Error> {
return useQuery({ return useQuery({
queryKey: ['pages', pageId], queryKey: ["pages", pageId],
queryFn: () => getPageById(pageId), queryFn: () => getPageById(pageId),
enabled: !!pageId, enabled: !!pageId,
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
}); });
} }
export function useGetPagesQuery(): UseQueryResult<IPage[], Error> {
return useQuery({
queryKey: ["pages"],
queryFn: () => getPages(),
staleTime: 5 * 60 * 1000,
});
}
export function useRecentChangesQuery(): UseQueryResult<IPage[], Error> { export function useRecentChangesQuery(): UseQueryResult<IPage[], Error> {
return useQuery({ return useQuery({
queryKey: RECENT_CHANGES_KEY, queryKey: RECENT_CHANGES_KEY,
@ -31,17 +44,14 @@ export function useRecentChangesQuery(): UseQueryResult<IPage[], Error> {
export function useCreatePageMutation() { export function useCreatePageMutation() {
return useMutation<IPage, Error, Partial<IPage>>({ return useMutation<IPage, Error, Partial<IPage>>({
mutationFn: (data) => createPage(data), mutationFn: (data) => createPage(data),
onSuccess: (data) => {},
}); });
} }
export function useUpdatePageMutation() { export function useUpdatePageMutation() {
const queryClient = useQueryClient();
return useMutation<IPage, Error, Partial<IPage>>({ return useMutation<IPage, Error, Partial<IPage>>({
mutationFn: (data) => updatePage(data), mutationFn: (data) => updatePage(data),
onSuccess: (data) => { onSuccess: (data) => {},
queryClient.setQueryData(['pages', data.id], data);
},
}); });
} }
@ -49,7 +59,7 @@ export function useDeletePageMutation() {
return useMutation({ return useMutation({
mutationFn: (pageId: string) => deletePage(pageId), mutationFn: (pageId: string) => deletePage(pageId),
onSuccess: () => { onSuccess: () => {
notifications.show({ message: 'Page deleted successfully' }); notifications.show({ message: "Page deleted successfully" });
}, },
}); });
} }

View File

@ -23,24 +23,28 @@ import { FillFlexParent } from './components/fill-flex-parent';
import { TreeNode } from './types'; import { TreeNode } from './types';
import { treeApiAtom } from './atoms/tree-api-atom'; import { treeApiAtom } from './atoms/tree-api-atom';
import { usePersistence } from '@/features/page/tree/hooks/use-persistence'; 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 useWorkspacePageOrder from '@/features/page/tree/hooks/use-workspace-page-order';
import { useNavigate, useParams } from 'react-router-dom'; 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() { export default function PageTree() {
const { data, setData, controllers } = usePersistence<TreeApi<TreeNode>>(); const { data, setData, controllers } = usePersistence<TreeApi<TreeNode>>();
const [tree, setTree] = useAtom<TreeApi<TreeNode>>(treeApiAtom); const [tree, setTree] = useAtom<TreeApi<TreeNode>>(treeApiAtom);
const { data: pageOrderData } = useWorkspacePageOrder(); const { data: pageOrderData } = useWorkspacePageOrder();
const { data: pagesData, isLoading } = useGetPagesQuery();
const rootElement = useRef<HTMLDivElement>(); const rootElement = useRef<HTMLDivElement>();
const { pageId } = useParams(); const { pageId } = useParams();
const fetchAndSetTreeData = async () => { const fetchAndSetTreeData = async () => {
if (pageOrderData?.childrenIds) { if (pageOrderData?.childrenIds) {
try { try {
const pages = await getPages(); if (!isLoading) {
const treeData = convertToTree(pages, pageOrderData.childrenIds); const treeData = convertToTree(pagesData, pageOrderData.childrenIds);
setData(treeData); setData(treeData);
}
} catch (err) { } catch (err) {
console.error('Error fetching tree data: ', err); console.error('Error fetching tree data: ', err);
} }
@ -49,7 +53,7 @@ export default function PageTree() {
useEffect(() => { useEffect(() => {
fetchAndSetTreeData(); fetchAndSetTreeData();
}, [pageOrderData?.childrenIds]); }, [pageOrderData?.childrenIds, isLoading]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
@ -87,11 +91,28 @@ export default function PageTree() {
function Node({ node, style, dragHandle }: NodeRendererProps<any>) { function Node({ node, style, dragHandle }: NodeRendererProps<any>) {
const navigate = useNavigate(); const navigate = useNavigate();
const updatePageMutation = useUpdatePageMutation();
const [treeData, setTreeData] = useAtom(treeDataAtom);
const handleClick = () => { const handleClick = () => {
navigate(`/p/${node.id}`); 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) { if (node.willReceiveDrop && node.isClosed) {
setTimeout(() => { setTimeout(() => {
if (node.state.willReceiveDrop) node.open(); if (node.state.willReceiveDrop) node.open();
@ -108,7 +129,13 @@ function Node({ node, style, dragHandle }: NodeRendererProps<any>) {
> >
<PageArrow node={node} /> <PageArrow node={node} />
<IconFileDescription size="18px" style={{ marginRight: '4px' }} /> <div onClick={handleEmojiIconClick} style={{ marginRight: '4px' }}>
<EmojiPicker onEmojiSelect={handleEmojiSelect} icon={
node.data.icon ? node.data.icon :
<IconFileDescription size="18px" />
}/>
</div>
<span className={classes.text}> <span className={classes.text}>
{node.isEditing ? ( {node.isEditing ? (

View File

@ -60,3 +60,15 @@ export const updateTreeNodeName = (nodes: TreeNode[], nodeId: string, newName: s
return node; 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;
});
};

View File

@ -9,6 +9,10 @@ export class CreatePageDto {
@IsString() @IsString()
title?: string; title?: string;
@IsOptional()
@IsString()
icon?: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
parentPageId?: string; parentPageId?: string;

27
pnpm-lock.yaml generated
View File

@ -111,6 +111,12 @@ importers:
apps/client: apps/client:
dependencies: 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': '@mantine/core':
specifier: ^7.2.2 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) 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: date-fns:
specifier: ^2.30.0 specifier: ^2.30.0
version: 2.30.0 version: 2.30.0
emoji-mart:
specifier: ^5.5.2
version: 5.5.2
jotai: jotai:
specifier: ^2.5.1 specifier: ^2.5.1
version: 2.6.1(@types/react@18.2.47)(react@18.2.0) version: 2.6.1(@types/react@18.2.47)(react@18.2.0)
@ -2377,6 +2386,20 @@ packages:
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.9 '@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: /@esbuild/aix-ppc64@0.19.11:
resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -6567,6 +6590,10 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true dev: true
/emoji-mart@5.5.2:
resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==}
dev: false
/emoji-regex@8.0.0: /emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}