refactor react-query usage

This commit is contained in:
Philipinho
2023-11-13 17:48:32 +00:00
parent dc65fbafa4
commit fb057b7801
22 changed files with 286 additions and 274 deletions

View File

@ -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',

View File

@ -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",

View File

@ -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 (
<UnstyledButton className={classes.user}>
<Group>
<Avatar
src="https://images.unsplash.com/photo-1508214751196-bcfd4ca60f91?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=255&q=80"
src="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-9.png"
radius="xl"
/>
<div style={{ flex: 1 }}>
<Text size="sm" fw={500}>
Harriette Spoonlicker
{currentUser.user.name}
</Text>
<Text c="dimmed" size="xs">
hspoonlicker@outlook.com
{currentUser.user.email}
</Text>
</div>

View File

@ -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<number>(TOAST_LIMIT);
useEffect(() => {
toasts
.filter((tt) => tt.visible)
.filter((_, i) => i >= toastLimit)
.forEach((tt) => {
t.dismiss(tt.id);
});
}, [toastLimit, toasts]);
return <Toaster position={"top-right"}/>;
}

View File

@ -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 };
}

View File

@ -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<IComment[]>([]));
export const showCommentPopupAtom = atom(false);
export const activeCommentIdAtom = atom<string | null>(null);
export const draftCommentIdAtom = atom<string | null>(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);
}
));

View File

@ -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 (
<div className={classes.wrapper}>
<Text mb="md" fw={500}>Comments</Text>
<CommentList comments={comments} />
{data ? <CommentList comments={data} /> : 'No comments yet'}
</div>
);
}

View File

@ -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<boolean>(showCommentPopupAtom);
const [, setActiveCommentId] = useAtom<string | null>(activeCommentIdAtom);
const [draftCommentId, setDraftCommentId] = useAtom<string | null>(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);

View File

@ -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);
}

View File

@ -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<boolean>(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 (

View File

@ -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);
}

View File

@ -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<IComment>) => createComment(data),
{
onSuccess: (data: IComment) => {
toast.success('Comment created successfully');
},
onError: (error) => {
toast.error(`Error creating comment: ${error.message}`);
},
},
);
const updateMutation = useMutation(
(data: Partial<IComment>) => 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,
};
}

View File

@ -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<IComment[], Error> {
return useQuery({
queryKey: RQ_KEY(pageId),
queryFn: () => getPageComments(pageId),
enabled: !!pageId,
});
}
export function useCreateCommentMutation() {
const queryClient = useQueryClient();
return useMutation<IComment, Error, Partial<IComment>>({
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<IComment, Error, Partial<IComment>>({
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' });
},
});
}

View File

@ -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<IPage>(pageId));
const [debouncedTitleState, setDebouncedTitleState] = useState('');
const [debouncedTitle] = useDebouncedValue(debouncedTitleState, 1000);
const { updatePageMutation } = usePage();
const updatePageMutation = useUpdatePageMutation();
const [desktopAsideOpened, setDesktopAsideOpened] = useAtom<boolean>(desktopAsideAtom);
const [activeCommentId, setActiveCommentId] = useAtom<string | null>(activeCommentIdAtom);
const [showCommentPopup, setShowCommentPopup] = useAtom<boolean>(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]);

View File

@ -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 <PageListSkeleton />;
@ -20,9 +19,9 @@ function RecentChanges() {
return (
<div>
{data.map((page) => (
<>
<div key={page.id}>
<UnstyledButton component={Link} to={`/p/${page.id}`}
className={classes.page} p="xs" key={page.id}>
className={classes.page} p="xs">
<Group wrap="noWrap">
<Stack gap="xs" style={{ flex: 1 }}>
@ -37,7 +36,7 @@ function RecentChanges() {
</Group>
</UnstyledButton>
<Divider />
</>
</div>
))}
</div>
);

View File

@ -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<IPage>) => createPage(data),
);
const pageQueryResult: UseQueryResult<IPage, unknown> = useQuery(
['page', pageId],
() => getPageById(pageId as string),
{
enabled: !!pageId,
},
);
const recentPagesQuery: UseQueryResult<IPage[], unknown> = useQuery(
['recentChanges'],
() => getRecentChanges()
);
const updateMutation = useMutation(
(data: Partial<IPage>) => updatePage(data),
);
const removeMutation = useMutation(
(id: string) => deletePage(id),
);
return {
pageQuery: pageQueryResult,
recentPagesQuery: recentPagesQuery,
create: createMutation.mutate,
updatePageMutation: updateMutation.mutate,
remove: removeMutation.mutate,
};
}

View File

@ -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<IPage, Error> {
return useQuery({
queryKey: ['page', pageId],
queryFn: () => getPageById(pageId),
enabled: !!pageId,
});
}
export function useRecentChangesQuery(): UseQueryResult<IPage[], Error> {
return useQuery({
queryKey: RECENT_CHANGES_KEY,
queryFn: () => getRecentChanges(),
refetchOnMount: true,
});
}
export function useCreatePageMutation() {
return useMutation<IPage, Error, Partial<IPage>>({
mutationFn: (data) => createPage(data),
});
}
export function useUpdatePageMutation() {
return useMutation<IPage, Error, Partial<IPage>>({
mutationFn: (data) => updatePage(data),
});
}
export function useDeletePageMutation() {
return useMutation({
mutationFn: (pageId: string) => deletePage(pageId),
});
}

View File

@ -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<T>() {
const [data, setData] = useAtom(treeDataAtom);
const { updatePageMutation } = usePage();
const updatePageMutation = useUpdatePageMutation();
const navigate = useNavigate();
const tree = useMemo(() => new SimpleTree<TreeNode>(data), [data]);
@ -34,7 +34,7 @@ export function usePersistence<T>() {
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<T>() {
};
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<T>() {
setData(tree.data);
try {
updatePageMutation({ id, title: name });
updatePageMutation.mutateAsync({ id, title: name });
} catch (error) {
console.error('Error updating page title:', error);
}

View File

@ -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={
<Button type="submit" disabled={isLoading} loading={isLoading}>
Save
</Button>
}
<Button type="submit" disabled={isLoading} loading={isLoading}>
Save
</Button>
}
/>
</form>
);

View File

@ -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 (

View File

@ -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(
<BrowserRouter>
<MantineProvider theme={theme}>
<ModalsProvider>
<TanstackProvider>
<App />
<CustomToaster />
</TanstackProvider>
<TanstackProvider>
<Notifications />
<App />
</TanstackProvider>
</ModalsProvider>
</MantineProvider>
</BrowserRouter>,

View File

@ -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 <div>Loading...</div>;
}
if (pageQuery.isError) {
if (isError) {
return <div>Error fetching page data.</div>;
}