diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs index 21016626..1dfaf4a1 100644 --- a/client/.eslintrc.cjs +++ b/client/.eslintrc.cjs @@ -5,6 +5,7 @@ module.exports = { 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', + 'plugin:@tanstack/eslint-plugin-query/recommended', ], ignorePatterns: ['dist', '.eslintrc.cjs'], parser: '@typescript-eslint/parser', diff --git a/client/package.json b/client/package.json index 35d4a927..ae7c6682 100644 --- a/client/package.json +++ b/client/package.json @@ -10,13 +10,14 @@ }, "dependencies": { "@hocuspocus/provider": "^2.7.1", - "@mantine/core": "^7.2.1", - "@mantine/form": "^7.2.1", - "@mantine/hooks": "^7.2.1", - "@mantine/modals": "^7.2.1", - "@mantine/spotlight": "^7.2.1", + "@mantine/core": "^7.2.2", + "@mantine/form": "^7.2.2", + "@mantine/hooks": "^7.2.2", + "@mantine/modals": "^7.2.2", + "@mantine/notifications": "^7.2.2", + "@mantine/spotlight": "^7.2.2", "@tabler/icons-react": "^2.40.0", - "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query": "^5.8.3", "@tiptap/extension-code-block": "^2.1.12", "@tiptap/extension-collaboration": "^2.1.12", "@tiptap/extension-collaboration-cursor": "^2.1.12", @@ -61,6 +62,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@tanstack/eslint-plugin-query": "^5.8.3", "@types/js-cookie": "^3.0.4", "@types/node": "20.8.6", "@types/react": "^18.2.29", @@ -74,7 +76,7 @@ "eslint-plugin-react-refresh": "^0.4.3", "optics-ts": "^2.4.1", "postcss": "^8.4.31", - "postcss-preset-mantine": "^1.9.0", + "postcss-preset-mantine": "^1.11.0", "postcss-simple-vars": "^7.0.1", "prettier": "^3.0.3", "typescript": "^5.2.2", diff --git a/client/src/components/navbar/user-button.tsx b/client/src/components/navbar/user-button.tsx index 1785148b..da847cd1 100644 --- a/client/src/components/navbar/user-button.tsx +++ b/client/src/components/navbar/user-button.tsx @@ -1,23 +1,27 @@ import { UnstyledButton, Group, Avatar, Text, rem } from '@mantine/core'; import { IconChevronRight } from '@tabler/icons-react'; import classes from './user-button.module.css'; +import { useAtom } from 'jotai/index'; +import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; export function UserButton() { + const [currentUser] = useAtom(currentUserAtom); + return (
- Harriette Spoonlicker + {currentUser.user.name} - hspoonlicker@outlook.com + {currentUser.user.email}
diff --git a/client/src/components/ui/custom-toaster.tsx b/client/src/components/ui/custom-toaster.tsx deleted file mode 100644 index 657eaf4f..00000000 --- a/client/src/components/ui/custom-toaster.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import t, { Toaster, useToasterStore } from "react-hot-toast"; -import { useEffect, useState } from "react"; - -export default function CustomToaster() { - const { toasts } = useToasterStore(); - const TOAST_LIMIT = 3; - const [toastLimit, setToastLimit] = useState(TOAST_LIMIT); - - useEffect(() => { - toasts - .filter((tt) => tt.visible) - .filter((_, i) => i >= toastLimit) - .forEach((tt) => { - t.dismiss(tt.id); - }); - }, [toastLimit, toasts]); - - return ; -} diff --git a/client/src/features/auth/hooks/use-auth.ts b/client/src/features/auth/hooks/use-auth.ts index 980f6a31..2e0e786b 100644 --- a/client/src/features/auth/hooks/use-auth.ts +++ b/client/src/features/auth/hooks/use-auth.ts @@ -1,11 +1,11 @@ -import { useState } from "react"; -import { login, register } from "@/features/auth/services/auth-service"; -import { useNavigate } from "react-router-dom"; -import { useAtom } from "jotai"; -import { authTokensAtom } from "@/features/auth/atoms/auth-tokens-atom"; -import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; -import { ILogin, IRegister } from "@/features/auth/types/auth.types"; -import toast from "react-hot-toast"; +import { useState } from 'react'; +import { login, register } from '@/features/auth/services/auth-service'; +import { useNavigate } from 'react-router-dom'; +import { useAtom } from 'jotai'; +import { authTokensAtom } from '@/features/auth/atoms/auth-tokens-atom'; +import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; +import { ILogin, IRegister } from '@/features/auth/types/auth.types'; +import { notifications } from '@mantine/notifications'; export default function useAuth() { const [isLoading, setIsLoading] = useState(false); @@ -22,10 +22,13 @@ export default function useAuth() { setIsLoading(false); setAuthToken(res.tokens); - navigate("/home"); + navigate('/home'); } catch (err) { setIsLoading(false); - toast.error(err.response?.data.message) + notifications.show({ + message: err.response?.data.message, + color: 'red', + }); } }; @@ -38,10 +41,13 @@ export default function useAuth() { setAuthToken(res.tokens); - navigate("/home"); + navigate('/home'); } catch (err) { setIsLoading(false); - toast.error(err.response?.data.message) + notifications.show({ + message: err.response?.data.message, + color: 'red', + }); } }; @@ -52,7 +58,7 @@ export default function useAuth() { const handleLogout = async () => { setAuthToken(null); setCurrentUser(null); - } + }; return { signIn: handleSignIn, signUp: handleSignUp, isLoading, hasTokens }; } diff --git a/client/src/features/comment/atoms/comment-atom.ts b/client/src/features/comment/atoms/comment-atom.ts index 6fb8c3d8..9fed0737 100644 --- a/client/src/features/comment/atoms/comment-atom.ts +++ b/client/src/features/comment/atoms/comment-atom.ts @@ -1,17 +1,5 @@ import { atom } from 'jotai'; -import { atomFamily } from 'jotai/utils'; -import { IComment } from '@/features/comment/types/comment.types'; -export const commentsAtom = atomFamily((pageId: string) => atom([])); export const showCommentPopupAtom = atom(false); export const activeCommentIdAtom = atom(null); export const draftCommentIdAtom = atom(null); - -export const deleteCommentAtom = atomFamily((pageId: string) => atom( - null, - (get, set, idToDelete: string) => { - const currentPageComments = get(commentsAtom(pageId)); - const updatedComments = currentPageComments.filter(comment => comment.id !== idToDelete); - set(commentsAtom(pageId), updatedComments); - } -)); diff --git a/client/src/features/comment/comments.tsx b/client/src/features/comment/comments.tsx index 3177d20e..e1b83c47 100644 --- a/client/src/features/comment/comments.tsx +++ b/client/src/features/comment/comments.tsx @@ -1,34 +1,22 @@ import { useParams } from 'react-router-dom'; -import { useAtom } from 'jotai'; -import { commentsAtom } from '@/features/comment/atoms/comment-atom'; -import React, { useEffect } from 'react'; -import { getPageComments } from '@/features/comment/services/comment-service'; +import React from 'react'; import classes from '@/features/comment/components/comment.module.css'; import { Text } from '@mantine/core'; import CommentList from '@/features/comment/components/comment-list'; +import { useCommentsQuery } from '@/features/comment/queries/comment'; export default function Comments() { const { pageId } = useParams(); - const [comments, setComments] = useAtom(commentsAtom(pageId)); + const { data, isLoading, isError } = useCommentsQuery(pageId); - useEffect(() => { - const fetchComments = async () => { - try { - const response = await getPageComments(pageId); - setComments(response); - } catch (error) { - console.error('Failed to fetch comments:', error); - } - }; - - fetchComments(); - }, [pageId]); + if (isLoading) { + return <>; + } return (
Comments - - + {data ? : 'No comments yet'}
); } diff --git a/client/src/features/comment/components/comment-dialog.tsx b/client/src/features/comment/components/comment-dialog.tsx index 3423343b..2a6a60dd 100644 --- a/client/src/features/comment/components/comment-dialog.tsx +++ b/client/src/features/comment/components/comment-dialog.tsx @@ -4,7 +4,6 @@ import { useClickOutside } from '@mantine/hooks'; import { useAtom } from 'jotai'; import { activeCommentIdAtom, - commentsAtom, draftCommentIdAtom, showCommentPopupAtom, } from '@/features/comment/atoms/comment-atom'; @@ -12,7 +11,7 @@ import { Editor } from '@tiptap/core'; import CommentEditor from '@/features/comment/components/comment-editor'; import CommentActions from '@/features/comment/components/comment-actions'; import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; -import useComment from '@/features/comment/hooks/use-comment'; +import { useCreateCommentMutation } from '@/features/comment/queries/comment'; interface CommentDialogProps { editor: Editor, @@ -24,12 +23,11 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { const [, setShowCommentPopup] = useAtom(showCommentPopupAtom); const [, setActiveCommentId] = useAtom(activeCommentIdAtom); const [draftCommentId, setDraftCommentId] = useAtom(draftCommentIdAtom); - const [comments, setComments] = useAtom(commentsAtom(pageId)); const [currentUser] = useAtom(currentUserAtom); const useClickOutsideRef = useClickOutside(() => { handleDialogClose(); }); - const { createCommentMutation } = useComment(); + const createCommentMutation = useCreateCommentMutation(); const { isLoading } = createCommentMutation; const handleDialogClose = () => { @@ -43,17 +41,16 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { }; const handleAddComment = async () => { - const selectedText = getSelectedText(); - const commentData = { - id: draftCommentId, - pageId: pageId, - content: JSON.stringify(comment), - selection: selectedText, - }; - try { + const selectedText = getSelectedText(); + const commentData = { + id: draftCommentId, + pageId: pageId, + content: JSON.stringify(comment), + selection: selectedText, + }; + const createdComment = await createCommentMutation.mutateAsync(commentData); - setComments(prevComments => [...prevComments, createdComment]); editor.chain().setComment(createdComment.id).unsetCommentDecoration().run(); setActiveCommentId(createdComment.id); diff --git a/client/src/features/comment/components/comment-list-item.tsx b/client/src/features/comment/components/comment-list-item.tsx index 3c195c7b..a377af4a 100644 --- a/client/src/features/comment/components/comment-list-item.tsx +++ b/client/src/features/comment/components/comment-list-item.tsx @@ -1,45 +1,51 @@ import { Group, Avatar, Text, Box } from '@mantine/core'; import React, { useState } from 'react'; import classes from './comment.module.css'; -import { useAtom, useAtomValue } from 'jotai'; -import { deleteCommentAtom } from '@/features/comment/atoms/comment-atom'; +import { useAtomValue } from 'jotai'; import { timeAgo } from '@/lib/time-ago'; import CommentEditor from '@/features/comment/components/comment-editor'; import { editorAtom } from '@/features/editor/atoms/editorAtom'; import CommentActions from '@/features/comment/components/comment-actions'; import CommentMenu from '@/features/comment/components/comment-menu'; -import useComment from '@/features/comment/hooks/use-comment'; import ResolveComment from '@/features/comment/components/resolve-comment'; import { useHover } from '@mantine/hooks'; +import { useDeleteCommentMutation, useUpdateCommentMutation } from '@/features/comment/queries/comment'; +import { IComment } from '@/features/comment/types/comment.types'; -function CommentListItem({ comment }) { +interface CommentListItemProps { + comment: IComment; +} + +function CommentListItem({ comment }: CommentListItemProps) { const { hovered, ref } = useHover(); const [isEditing, setIsEditing] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const editor = useAtomValue(editorAtom); const [content, setContent] = useState(comment.content); - const { updateCommentMutation, deleteCommentMutation } = useComment(); - const { isLoading } = updateCommentMutation; - const [, deleteComment] = useAtom(deleteCommentAtom(comment.pageId)); + const updateCommentMutation = useUpdateCommentMutation(); + const deleteCommentMutation = useDeleteCommentMutation(comment.pageId); async function handleUpdateComment() { - const commentToUpdate = { - id: comment.id, - content: JSON.stringify(content), - }; - try { + setIsLoading(true); + const commentToUpdate = { + id: comment.id, + content: JSON.stringify(content), + }; await updateCommentMutation.mutateAsync(commentToUpdate); setIsEditing(false); } catch (error) { console.error('Failed to update comment:', error); + } finally { + setIsLoading(false); } } async function handleDeleteComment() { try { - await deleteCommentMutation(comment.id); + await deleteCommentMutation.mutateAsync(comment.id); editor?.commands.unsetComment(comment.id); - deleteComment(comment.id); // Todo: unify code } catch (error) { console.error('Failed to delete comment:', error); } diff --git a/client/src/features/comment/components/comment-list.tsx b/client/src/features/comment/components/comment-list.tsx index fb715333..2df16168 100644 --- a/client/src/features/comment/components/comment-list.tsx +++ b/client/src/features/comment/components/comment-list.tsx @@ -4,17 +4,16 @@ import CommentActions from '@/features/comment/components/comment-actions'; import React, { useState } from 'react'; import CommentListItem from '@/features/comment/components/comment-list-item'; import { IComment } from '@/features/comment/types/comment.types'; -import useComment from '@/features/comment/hooks/use-comment'; -import { useAtom } from 'jotai'; -import { commentsAtom } from '@/features/comment/atoms/comment-atom'; -import { useParams } from 'react-router-dom'; import { useFocusWithin } from '@mantine/hooks'; +import { useCreateCommentMutation } from '@/features/comment/queries/comment'; -function CommentList({ comments }: IComment[]) { - const { createCommentMutation } = useComment(); - const { isLoading } = createCommentMutation; - const { pageId } = useParams(); - const [, setCommentsAtom] = useAtom(commentsAtom(pageId)); +interface CommentListProps { + comments: IComment[]; +} + +function CommentList({ comments }: CommentListProps) { + const createCommentMutation = useCreateCommentMutation(); + const [isLoading, setIsLoading] = useState(false); const getChildComments = (parentId) => { return comments.filter(comment => comment.parentCommentId === parentId); @@ -54,14 +53,22 @@ function CommentList({ comments }: IComment[]) { const renderComments = (comment) => { const handleAddReply = async (commentId, content) => { - const commentData = { - pageId: comment.pageId, - parentCommentId: comment.id, - content: JSON.stringify(content), - }; + try { + setIsLoading(true); + const commentData = { + pageId: comment.pageId, + parentCommentId: comment.id, + content: JSON.stringify(content), + }; - const createdComment = await createCommentMutation.mutateAsync(commentData); - setCommentsAtom(prevComments => [...prevComments, createdComment]); + await createCommentMutation.mutateAsync(commentData); + } catch (error) { + console.error('Failed to add reply:', error); + } finally { + setIsLoading(false); + } + + //setCommentsAtom(prevComments => [...prevComments, createdComment]); }; return ( diff --git a/client/src/features/comment/components/resolve-comment.tsx b/client/src/features/comment/components/resolve-comment.tsx index 2103b1d8..5e8b77d6 100644 --- a/client/src/features/comment/components/resolve-comment.tsx +++ b/client/src/features/comment/components/resolve-comment.tsx @@ -1,14 +1,10 @@ import { ActionIcon } from '@mantine/core'; import { IconCircleCheck } from '@tabler/icons-react'; - -import useComment from '@/features/comment/hooks/use-comment'; -import { useAtom } from 'jotai'; -import { commentsAtom } from '@/features/comment/atoms/comment-atom'; import { modals } from '@mantine/modals'; +import { useResolveCommentMutation } from '@/features/comment/queries/comment'; function ResolveComment({ commentId, pageId, resolvedAt }) { - const [, setComments] = useAtom(commentsAtom(pageId)); - const { resolveCommentMutation } = useComment(); + const resolveCommentMutation = useResolveCommentMutation(); const isResolved = resolvedAt != null; const iconColor = isResolved ? 'green' : 'gray'; @@ -23,17 +19,9 @@ function ResolveComment({ commentId, pageId, resolvedAt }) { const handleResolveToggle = async () => { try { - const resolvedComment = await resolveCommentMutation.mutateAsync({ commentId, resolved: !isResolved }); + await resolveCommentMutation.mutateAsync({ commentId, resolved: !isResolved }); //TODO: remove comment mark // Remove comment thread from state on resolve - - setComments((oldComments) => - oldComments.map((comment) => - comment.id === commentId - ? { ...comment, resolvedAt: resolvedComment.resolvedAt, resolvedById: resolvedComment.resolvedById } - : comment, - ), - ); } catch (error) { console.error('Failed to toggle resolved state:', error); } diff --git a/client/src/features/comment/hooks/use-comment.ts b/client/src/features/comment/hooks/use-comment.ts deleted file mode 100644 index 121746eb..00000000 --- a/client/src/features/comment/hooks/use-comment.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; -import toast from 'react-hot-toast'; -import { - createComment, - deleteComment, - resolveComment, - updateComment, -} from '@/features/comment/services/comment-service'; -import { IComment, IResolveComment } from '@/features/comment/types/comment.types'; - -export default function useComment() { - - const createMutation = useMutation( - (data: Partial) => createComment(data), - { - onSuccess: (data: IComment) => { - toast.success('Comment created successfully'); - }, - onError: (error) => { - toast.error(`Error creating comment: ${error.message}`); - }, - }, - ); - - const updateMutation = useMutation( - (data: Partial) => updateComment(data), - { - onSuccess: (data: IComment) => { - toast.success('Comment updated successfully'); - }, - onError: (error) => { - toast.error(`Error updating comment: ${error.message}`); - }, - }, - ); - - const resolveMutation = useMutation( - (data: IResolveComment) => resolveComment(data), - { - onError: (error) => { - toast.error(`Failed to perform resolve action: ${error.message}`); - }, - }, - ); - - const deleteMutation = useMutation( - (id: string) => deleteComment(id), - { - onSuccess: () => { - toast.success('Comment deleted successfully'); - }, - onError: (error) => { - toast.error(`Error deleting comment: ${error.message}`); - }, - }, - ); - - return { - createCommentMutation: createMutation, - updateCommentMutation: updateMutation, - resolveCommentMutation: resolveMutation, - deleteCommentMutation: deleteMutation.mutateAsync, - }; -} diff --git a/client/src/features/comment/queries/comment.ts b/client/src/features/comment/queries/comment.ts new file mode 100644 index 00000000..0bf76b05 --- /dev/null +++ b/client/src/features/comment/queries/comment.ts @@ -0,0 +1,96 @@ +import { useMutation, useQuery, useQueryClient, UseQueryResult } from '@tanstack/react-query'; +import { + createComment, + deleteComment, getPageComments, + resolveComment, + updateComment, +} from '@/features/comment/services/comment-service'; +import { IComment, IResolveComment } from '@/features/comment/types/comment.types'; +import { notifications } from '@mantine/notifications'; + +export const RQ_KEY = (pageId: string) => ['comment', pageId]; + +export function useCommentsQuery(pageId: string): UseQueryResult { + return useQuery({ + queryKey: RQ_KEY(pageId), + queryFn: () => getPageComments(pageId), + enabled: !!pageId, + }); +} + +export function useCreateCommentMutation() { + const queryClient = useQueryClient(); + + return useMutation>({ + mutationFn: (data) => createComment(data), + onSuccess: (data) => { + const newComment = data; + let comments = queryClient.getQueryData(RQ_KEY(data.pageId)); + if (comments) { + comments = prevComments => [...prevComments, newComment]; + queryClient.setQueryData(RQ_KEY(data.pageId), comments); + } + + notifications.show({ message: 'Comment created successfully' }); + }, + onError: (error) => { + notifications.show({ message: 'Error creating comment', color: 'red' }); + }, + }); +} + +export function useUpdateCommentMutation() { + return useMutation>({ + mutationFn: (data) => updateComment(data), + onSuccess: (data) => { + notifications.show({ message: 'Comment updated successfully' }); + }, + onError: (error) => { + notifications.show({ message: 'Failed to update comment', color: 'red' }); + }, + }); +} + +export function useDeleteCommentMutation(pageId?: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (commentId: string) => deleteComment(commentId), + onSuccess: (data, variables) => { + let comments = queryClient.getQueryData(RQ_KEY(pageId)); + if (comments) { + comments = comments.filter(comment => comment.id !== variables); + queryClient.setQueryData(RQ_KEY(pageId), comments); + } + notifications.show({ message: 'Comment deleted successfully' }); + }, + onError: (error) => { + notifications.show({ message: 'Failed to delete comment', color: 'red' }); + }, + }); +} + +export function useResolveCommentMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: IResolveComment) => resolveComment(data), + onSuccess: (data: IComment, variables) => { + + const currentComments = queryClient.getQueryData(RQ_KEY(data.pageId)); + + if (currentComments) { + const updatedComments = currentComments.map((comment) => + comment.id === variables.commentId ? { ...comment, ...data } : comment, + ); + queryClient.setQueryData(RQ_KEY(data.pageId), updatedComments); + } + + notifications.show({ message: 'Comment resolved successfully' }); + }, + onError: (error) => { + notifications.show({ message: 'Failed to resolve comment', color: 'red' }); + }, + }); +} + diff --git a/client/src/features/editor/editor.tsx b/client/src/features/editor/editor.tsx index 21d82393..8c4ace59 100644 --- a/client/src/features/editor/editor.tsx +++ b/client/src/features/editor/editor.tsx @@ -14,7 +14,6 @@ import { EditorBubbleMenu } from '@/features/editor/components/bubble-menu/bubbl import { Document } from '@tiptap/extension-document'; import { Text } from '@tiptap/extension-text'; import { Heading } from '@tiptap/extension-heading'; -import usePage from '@/features/page/hooks/use-page'; import { useDebouncedValue } from '@mantine/hooks'; import { pageAtom } from '@/features/page/atoms/page-atom'; import { IPage } from '@/features/page/types/page.types'; @@ -24,6 +23,7 @@ import { activeCommentIdAtom, showCommentPopupAtom } from '@/features/comment/at import CommentDialog from '@/features/comment/components/comment-dialog'; import { editorAtom } from '@/features/editor/atoms/editorAtom'; import { collabExtensions, mainExtensions } from '@/features/editor/extensions'; +import { useUpdatePageMutation } from '@/features/page/queries/page'; interface EditorProps { pageId: string, @@ -93,7 +93,7 @@ function TiptapEditor({ ydoc, provider, pageId }: TiptapEditorProps) { const [page, setPage] = useAtom(pageAtom(pageId)); const [debouncedTitleState, setDebouncedTitleState] = useState(''); const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 1000); - const { updatePageMutation } = usePage(); + const updatePageMutation = useUpdatePageMutation(); const [desktopAsideOpened, setDesktopAsideOpened] = useAtom(desktopAsideAtom); const [activeCommentId, setActiveCommentId] = useAtom(activeCommentIdAtom); const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom); @@ -127,7 +127,7 @@ function TiptapEditor({ ydoc, provider, pageId }: TiptapEditorProps) { useEffect(() => { if (debouncedTitle !== '') { - updatePageMutation({ id: pageId, title: debouncedTitle }); + updatePageMutation.mutate({ id: pageId, title: debouncedTitle }); } }, [debouncedTitle]); diff --git a/client/src/features/home/components/recent-changes.tsx b/client/src/features/home/components/recent-changes.tsx index 842c0421..a275973a 100644 --- a/client/src/features/home/components/recent-changes.tsx +++ b/client/src/features/home/components/recent-changes.tsx @@ -3,11 +3,10 @@ import { format } from 'date-fns'; import classes from './home.module.css'; import { Link } from 'react-router-dom'; import PageListSkeleton from '@/features/home/components/page-list-skeleton'; -import usePage from '@/features/page/hooks/use-page'; +import { useRecentChangesQuery } from '@/features/page/queries/page'; function RecentChanges() { - const { recentPagesQuery } = usePage(); - const { data, isLoading, isError } = recentPagesQuery; + const { data, isLoading, isError } = useRecentChangesQuery(); if (isLoading) { return ; @@ -20,9 +19,9 @@ function RecentChanges() { return (
{data.map((page) => ( - <> +
+ className={classes.page} p="xs"> @@ -37,7 +36,7 @@ function RecentChanges() { - +
))}
); diff --git a/client/src/features/page/hooks/use-page.ts b/client/src/features/page/hooks/use-page.ts deleted file mode 100644 index f6b131c0..00000000 --- a/client/src/features/page/hooks/use-page.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; -import { createPage, deletePage, getPageById, getRecentChanges, updatePage } from '@/features/page/services/page-service'; -import { IPage } from '@/features/page/types/page.types'; - -export default function usePage(pageId?: string) { - const createMutation = useMutation( - (data: Partial) => createPage(data), - ); - - const pageQueryResult: UseQueryResult = useQuery( - ['page', pageId], - () => getPageById(pageId as string), - { - enabled: !!pageId, - }, - ); - - const recentPagesQuery: UseQueryResult = useQuery( - ['recentChanges'], - () => getRecentChanges() - ); - - const updateMutation = useMutation( - (data: Partial) => updatePage(data), - ); - - const removeMutation = useMutation( - (id: string) => deletePage(id), - ); - - return { - pageQuery: pageQueryResult, - recentPagesQuery: recentPagesQuery, - create: createMutation.mutate, - updatePageMutation: updateMutation.mutate, - remove: removeMutation.mutate, - }; -} diff --git a/client/src/features/page/queries/page.ts b/client/src/features/page/queries/page.ts new file mode 100644 index 00000000..cc01a0c8 --- /dev/null +++ b/client/src/features/page/queries/page.ts @@ -0,0 +1,45 @@ +import { useMutation, useQuery, UseQueryResult, useQueryClient } from '@tanstack/react-query'; +import { + createPage, + deletePage, + getPageById, + getRecentChanges, + updatePage, +} from '@/features/page/services/page-service'; +import { IPage } from '@/features/page/types/page.types'; + +const RECENT_CHANGES_KEY = ['recentChanges']; + +export function usePageQuery(pageId: string): UseQueryResult { + return useQuery({ + queryKey: ['page', pageId], + queryFn: () => getPageById(pageId), + enabled: !!pageId, + }); +} + +export function useRecentChangesQuery(): UseQueryResult { + return useQuery({ + queryKey: RECENT_CHANGES_KEY, + queryFn: () => getRecentChanges(), + refetchOnMount: true, + }); +} + +export function useCreatePageMutation() { + return useMutation>({ + mutationFn: (data) => createPage(data), + }); +} + +export function useUpdatePageMutation() { + return useMutation>({ + mutationFn: (data) => updatePage(data), + }); +} + +export function useDeletePageMutation() { + return useMutation({ + mutationFn: (pageId: string) => deletePage(pageId), + }); +} diff --git a/client/src/features/page/tree/hooks/use-persistence.ts b/client/src/features/page/tree/hooks/use-persistence.ts index e3e41894..1a7f36be 100644 --- a/client/src/features/page/tree/hooks/use-persistence.ts +++ b/client/src/features/page/tree/hooks/use-persistence.ts @@ -11,13 +11,13 @@ import { treeDataAtom } from '@/features/page/tree/atoms/tree-data-atom'; import { createPage, deletePage, 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 { useNavigate } from 'react-router-dom'; import { TreeNode } from '@/features/page/tree/types'; -import usePage from '@/features/page/hooks/use-page'; +import { useUpdatePageMutation } from '@/features/page/queries/page'; export function usePersistence() { const [data, setData] = useAtom(treeDataAtom); - const { updatePageMutation } = usePage(); + const updatePageMutation = useUpdatePageMutation(); const navigate = useNavigate(); const tree = useMemo(() => new SimpleTree(data), [data]); @@ -34,7 +34,7 @@ export function usePersistence() { const afterId = currentTreeData[newDragIndex - 1]?.id || null; const beforeId = !afterId && currentTreeData[newDragIndex + 1]?.id || null; - const params: IMovePage= { + const params: IMovePage = { id: args.dragIds[0], after: afterId, before: beforeId, @@ -42,7 +42,7 @@ export function usePersistence() { }; 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 { @@ -57,7 +57,7 @@ export function usePersistence() { setData(tree.data); try { - updatePageMutation({ id, title: name }); + updatePageMutation.mutateAsync({ id, title: name }); } catch (error) { console.error('Error updating page title:', error); } diff --git a/client/src/features/settings/account/settings/components/account-name-form.tsx b/client/src/features/settings/account/settings/components/account-name-form.tsx index f92c6448..f9171d80 100644 --- a/client/src/features/settings/account/settings/components/account-name-form.tsx +++ b/client/src/features/settings/account/settings/components/account-name-form.tsx @@ -6,8 +6,8 @@ import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; import { updateUser } from '@/features/user/services/user-service'; import { IUser } from '@/features/user/types/user.types'; import { useState } from 'react'; -import toast from 'react-hot-toast'; import { TextInput, Button } from '@mantine/core'; +import { notifications } from '@mantine/notifications'; const formSchema = z.object({ name: z.string().min(2).max(40).nonempty('Your name cannot be blank'), @@ -35,10 +35,15 @@ export default function AccountNameForm() { try { const updatedUser = await updateUser(data); setUser(updatedUser); - toast.success('Updated successfully'); + notifications.show({ + message: 'Updated successfully', + }); } catch (err) { console.log(err); - toast.error('Failed to update data.'); + notifications.show({ + message: 'Failed to update data', + color: 'red', + }); } setIsLoading(false); @@ -53,10 +58,10 @@ export default function AccountNameForm() { variant="filled" {...form.getInputProps('name')} rightSection={ - - } + + } /> ); diff --git a/client/src/features/settings/workspace/settings/components/workspace-name-form.tsx b/client/src/features/settings/workspace/settings/components/workspace-name-form.tsx index 73e8b9d7..7649d85c 100644 --- a/client/src/features/settings/workspace/settings/components/workspace-name-form.tsx +++ b/client/src/features/settings/workspace/settings/components/workspace-name-form.tsx @@ -1,13 +1,13 @@ import { currentUserAtom } from '@/features/user/atoms/current-user-atom'; import { useAtom } from 'jotai'; import * as z from 'zod'; -import toast from 'react-hot-toast'; import { useState } from 'react'; import { focusAtom } from 'jotai-optics'; import { updateWorkspace } from '@/features/workspace/services/workspace-service'; import { IWorkspace } from '@/features/workspace/types/workspace.types'; import { TextInput, Button } from '@mantine/core'; import { useForm, zodResolver } from '@mantine/form'; +import { notifications } from '@mantine/notifications'; const formSchema = z.object({ name: z.string().nonempty('Workspace name cannot be blank'), @@ -35,14 +35,15 @@ export default function WorkspaceNameForm() { try { const updatedWorkspace = await updateWorkspace(data); setWorkspace(updatedWorkspace); - toast.success('Updated successfully'); + notifications.show({ message: 'Updated successfully' }); } catch (err) { console.log(err); - toast.error('Failed to update data.'); + notifications.show({ + message: 'Failed to update data', + color: 'red', + }); } - setIsLoading(false); - } return ( diff --git a/client/src/main.tsx b/client/src/main.tsx index b774b4aa..56dd627b 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,15 +1,15 @@ import '@mantine/core/styles.css'; import '@mantine/spotlight/styles.css'; - +import '@mantine/notifications/styles.css'; import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import { theme } from '@/theme'; import { MantineProvider } from '@mantine/core'; import { TanstackProvider } from '@/components/providers/tanstack-provider'; -import CustomToaster from '@/components/ui/custom-toaster'; import { BrowserRouter } from 'react-router-dom'; import { ModalsProvider } from '@mantine/modals'; +import { Notifications } from '@mantine/notifications'; const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); @@ -17,10 +17,10 @@ root.render( - - - - + + + + , diff --git a/client/src/pages/page/page.tsx b/client/src/pages/page/page.tsx index ca21417d..b8ff467f 100644 --- a/client/src/pages/page/page.tsx +++ b/client/src/pages/page/page.tsx @@ -1,26 +1,26 @@ import { useParams } from 'react-router-dom'; import React, { useEffect } from 'react'; -import { useAtom } from 'jotai/index'; -import usePage from '@/features/page/hooks/use-page'; +import { useAtom } from 'jotai'; import Editor from '@/features/editor/editor'; import { pageAtom } from '@/features/page/atoms/page-atom'; +import { usePageQuery } from '@/features/page/queries/page'; export default function Page() { const { pageId } = useParams(); const [, setPage] = useAtom(pageAtom(pageId)); - const { pageQuery } = usePage(pageId); + const { data, isLoading, isError } = usePageQuery(pageId); useEffect(() => { - if (pageQuery.data) { - setPage(pageQuery.data); + if (data) { + setPage(data); } - }, [pageQuery.data, pageQuery.isLoading, setPage, pageId]); + }, [data, isLoading, setPage, pageId]); - if (pageQuery.isLoading) { + if (isLoading) { return
Loading...
; } - if (pageQuery.isError) { + if (isError) { return
Error fetching page data.
; }