diff --git a/client/src/features/comment/atoms/comment-atom.ts b/client/src/features/comment/atoms/comment-atom.ts index 9fed0737..384a2f3d 100644 --- a/client/src/features/comment/atoms/comment-atom.ts +++ b/client/src/features/comment/atoms/comment-atom.ts @@ -1,5 +1,5 @@ import { atom } from 'jotai'; -export const showCommentPopupAtom = atom(false); -export const activeCommentIdAtom = atom(null); -export const draftCommentIdAtom = atom(null); +export const showCommentPopupAtom = atom(false); +export const activeCommentIdAtom = atom(''); +export const draftCommentIdAtom = atom(''); diff --git a/client/src/features/comment/comments.tsx b/client/src/features/comment/comments.tsx deleted file mode 100644 index e1b83c47..00000000 --- a/client/src/features/comment/comments.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useParams } from 'react-router-dom'; -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 { data, isLoading, isError } = useCommentsQuery(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 2a6a60dd..8fe0b2b2 100644 --- a/client/src/features/comment/components/comment-dialog.tsx +++ b/client/src/features/comment/components/comment-dialog.tsx @@ -11,7 +11,8 @@ 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 { useCreateCommentMutation } from '@/features/comment/queries/comment'; +import { useCreateCommentMutation } from '@/features/comment/queries/comment-query'; +import { asideStateAtom } from '@/components/navbar/atoms/sidebar-atom'; interface CommentDialogProps { editor: Editor, @@ -20,15 +21,16 @@ interface CommentDialogProps { function CommentDialog({ editor, pageId }: CommentDialogProps) { const [comment, setComment] = useState(''); - const [, setShowCommentPopup] = useAtom(showCommentPopupAtom); - const [, setActiveCommentId] = useAtom(activeCommentIdAtom); - const [draftCommentId, setDraftCommentId] = useAtom(draftCommentIdAtom); + const [, setShowCommentPopup] = useAtom(showCommentPopupAtom); + const [, setActiveCommentId] = useAtom(activeCommentIdAtom); + const [draftCommentId, setDraftCommentId] = useAtom(draftCommentIdAtom); const [currentUser] = useAtom(currentUserAtom); + const [, setAsideState] = useAtom(asideStateAtom); const useClickOutsideRef = useClickOutside(() => { handleDialogClose(); }); const createCommentMutation = useCreateCommentMutation(); - const { isLoading } = createCommentMutation; + const { isPending } = createCommentMutation; const handleDialogClose = () => { setShowCommentPopup(false); @@ -54,6 +56,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { editor.chain().setComment(createdComment.id).unsetCommentDecoration().run(); setActiveCommentId(createdComment.id); + setAsideState({ tab: 'comments', isAsideOpen: true }); setTimeout(() => { const selector = `div[data-comment-id="${createdComment.id}"]`; const commentElement = document.querySelector(selector); @@ -61,7 +64,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { }); } finally { setShowCommentPopup(false); - setDraftCommentId(null); + setDraftCommentId(''); } }; @@ -86,7 +89,7 @@ function CommentDialog({ editor, pageId }: CommentDialogProps) { - diff --git a/client/src/features/comment/components/comment-editor.tsx b/client/src/features/comment/components/comment-editor.tsx index e4152448..22fb9475 100644 --- a/client/src/features/comment/components/comment-editor.tsx +++ b/client/src/features/comment/components/comment-editor.tsx @@ -3,10 +3,10 @@ import { Placeholder } from '@tiptap/extension-placeholder'; import { Underline } from '@tiptap/extension-underline'; import { Link } from '@tiptap/extension-link'; import { StarterKit } from '@tiptap/starter-kit'; -import React from 'react'; import classes from './comment.module.css'; import { useFocusWithin } from '@mantine/hooks'; import clsx from 'clsx'; +import { forwardRef, useImperativeHandle } from 'react'; interface CommentEditorProps { defaultContent?: any; @@ -16,7 +16,8 @@ interface CommentEditorProps { autofocus?: boolean; } -function CommentEditor({ defaultContent, onUpdate, editable, placeholder, autofocus }: CommentEditorProps) { +const CommentEditor = forwardRef(({ defaultContent, onUpdate, editable, placeholder, autofocus }: CommentEditorProps, + ref) => { const { ref: focusRef, focused } = useFocusWithin(); const commentEditor = useEditor({ @@ -39,6 +40,12 @@ function CommentEditor({ defaultContent, onUpdate, editable, placeholder, autofo autofocus: (autofocus && 'end') || false, }); + useImperativeHandle(ref, () => ({ + clearContent: () => { + commentEditor.commands.clearContent(); + }, + })); + return (
); - -} +}); export default CommentEditor; diff --git a/client/src/features/comment/components/comment-list-item.tsx b/client/src/features/comment/components/comment-list-item.tsx index a377af4a..ae63d623 100644 --- a/client/src/features/comment/components/comment-list-item.tsx +++ b/client/src/features/comment/components/comment-list-item.tsx @@ -1,22 +1,23 @@ -import { Group, Avatar, Text, Box } from '@mantine/core'; +import { Group, Text, Box } from '@mantine/core'; import React, { useState } from 'react'; import classes from './comment.module.css'; import { useAtomValue } from 'jotai'; -import { timeAgo } from '@/lib/time-ago'; +import { timeAgo } from '@/lib/time'; 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 ResolveComment from '@/features/comment/components/resolve-comment'; import { useHover } from '@mantine/hooks'; -import { useDeleteCommentMutation, useUpdateCommentMutation } from '@/features/comment/queries/comment'; +import { useDeleteCommentMutation, useUpdateCommentMutation } from '@/features/comment/queries/comment-query'; import { IComment } from '@/features/comment/types/comment.types'; +import { UserAvatar } from '@/components/ui/user-avatar'; interface CommentListItemProps { comment: IComment; } function CommentListItem({ comment }: CommentListItemProps) { + const { hovered, ref } = useHover(); const [isEditing, setIsEditing] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -58,28 +59,20 @@ function CommentListItem({ comment }: CommentListItemProps) { return ( - {comment.creator.avatarUrl ? ( - ) : ( - {comment.creator.name.charAt(0)} - )} +
{comment.creator.name}
- {!comment.parentCommentId && ( + {/*!comment.parentCommentId && ( - )} + )*/} - +
diff --git a/client/src/features/comment/components/comment-list.tsx b/client/src/features/comment/components/comment-list.tsx index 2df16168..3faad2e7 100644 --- a/client/src/features/comment/components/comment-list.tsx +++ b/client/src/features/comment/components/comment-list.tsx @@ -1,55 +1,30 @@ -import { Divider, Paper, ScrollArea } from '@mantine/core'; +import React, { useState, useRef } from 'react'; +import { useParams } from 'react-router-dom'; +import { Divider, Paper } from '@mantine/core'; +import CommentListItem from '@/features/comment/components/comment-list-item'; +import { useCommentsQuery, useCreateCommentMutation } from '@/features/comment/queries/comment-query'; + import CommentEditor from '@/features/comment/components/comment-editor'; 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 { useFocusWithin } from '@mantine/hooks'; -import { useCreateCommentMutation } from '@/features/comment/queries/comment'; -interface CommentListProps { - comments: IComment[]; -} - -function CommentList({ comments }: CommentListProps) { +function CommentList() { + const { pageId } = useParams(); + const { data: comments, isLoading: isCommentsLoading, isError } = useCommentsQuery(pageId); + const [isLoading, setIsLoading] = useState(false); const createCommentMutation = useCreateCommentMutation(); - const [isLoading, setIsLoading] = useState(false); - const getChildComments = (parentId) => { - return comments.filter(comment => comment.parentCommentId === parentId); - }; + if (isCommentsLoading) { + return <>; + } - const renderChildComments = (parentId) => { - const children = getChildComments(parentId); - return ( -
- {children.map(childComment => ( -
- - {renderChildComments(childComment.id)} -
- ))} -
- ); - }; + if (isError) { + return
Error loading comments.
; + } - const CommentEditorWithActions = ({ commentId, onSave, isLoading }) => { - const [content, setContent] = useState(''); - const { ref, focused } = useFocusWithin(); - - const handleSave = () => { - onSave(commentId, content); - setContent(''); - }; - - return ( -
- - - {focused && } -
- ); - }; + if (!comments || comments.length === 0) { + return <>No comments yet.; + } const renderComments = (comment) => { const handleAddReply = async (commentId, content) => { @@ -63,41 +38,68 @@ function CommentList({ comments }: CommentListProps) { await createCommentMutation.mutateAsync(commentData); } catch (error) { - console.error('Failed to add reply:', error); + console.error('Failed to post comment:', error); } finally { setIsLoading(false); } - - //setCommentsAtom(prevComments => [...prevComments, createdComment]); }; return ( - +
- {renderChildComments(comment.id)} +
- - +
); }; return ( - -
- {comments - .filter(comment => comment.parentCommentId === null) - .map(comment => renderComments(comment)) - } -
-
+ <> + {comments.filter(comment => comment.parentCommentId === null).map(renderComments)} + ); } +const ChildComments = ({ comments, parentId }) => { + const getChildComments = (parentId) => { + return comments.filter(comment => comment.parentCommentId === parentId); + }; + + return ( +
+ {getChildComments(parentId).map(childComment => ( +
+ + +
+ ))} +
+ ); +}; + +const CommentEditorWithActions = ({ commentId, onSave, isLoading }) => { + const [content, setContent] = useState(''); + const { ref, focused } = useFocusWithin(); + const commentEditorRef = useRef(); + + const handleSave = () => { + onSave(commentId, content); + setContent(''); + commentEditorRef?.current.clearContent(); + }; + + return ( +
+ + {focused && } +
+ ); +}; + + export default CommentList; diff --git a/client/src/features/comment/components/resolve-comment.tsx b/client/src/features/comment/components/resolve-comment.tsx index 5e8b77d6..f6b206f5 100644 --- a/client/src/features/comment/components/resolve-comment.tsx +++ b/client/src/features/comment/components/resolve-comment.tsx @@ -1,7 +1,7 @@ import { ActionIcon } from '@mantine/core'; import { IconCircleCheck } from '@tabler/icons-react'; import { modals } from '@mantine/modals'; -import { useResolveCommentMutation } from '@/features/comment/queries/comment'; +import { useResolveCommentMutation } from '@/features/comment/queries/comment-query'; function ResolveComment({ commentId, pageId, resolvedAt }) { const resolveCommentMutation = useResolveCommentMutation(); diff --git a/client/src/features/comment/queries/comment.ts b/client/src/features/comment/queries/comment-query.ts similarity index 94% rename from client/src/features/comment/queries/comment.ts rename to client/src/features/comment/queries/comment-query.ts index 0bf76b05..97cc0a55 100644 --- a/client/src/features/comment/queries/comment.ts +++ b/client/src/features/comment/queries/comment-query.ts @@ -8,7 +8,7 @@ import { import { IComment, IResolveComment } from '@/features/comment/types/comment.types'; import { notifications } from '@mantine/notifications'; -export const RQ_KEY = (pageId: string) => ['comment', pageId]; +export const RQ_KEY = (pageId: string) => ['comments', pageId]; export function useCommentsQuery(pageId: string): UseQueryResult { return useQuery({ @@ -57,7 +57,7 @@ export function useDeleteCommentMutation(pageId?: string) { return useMutation({ mutationFn: (commentId: string) => deleteComment(commentId), onSuccess: (data, variables) => { - let comments = queryClient.getQueryData(RQ_KEY(pageId)); + let comments = queryClient.getQueryData(RQ_KEY(pageId)) as IComment[]; if (comments) { comments = comments.filter(comment => comment.id !== variables); queryClient.setQueryData(RQ_KEY(pageId), comments); @@ -77,7 +77,7 @@ export function useResolveCommentMutation() { mutationFn: (data: IResolveComment) => resolveComment(data), onSuccess: (data: IComment, variables) => { - const currentComments = queryClient.getQueryData(RQ_KEY(data.pageId)); + const currentComments = queryClient.getQueryData(RQ_KEY(data.pageId)) as IComment[]; if (currentComments) { const updatedComments = currentComments.map((comment) => diff --git a/client/src/features/page/queries/page.ts b/client/src/features/page/queries/page-query.ts similarity index 94% rename from client/src/features/page/queries/page.ts rename to client/src/features/page/queries/page-query.ts index 1fa8482e..903414e2 100644 --- a/client/src/features/page/queries/page.ts +++ b/client/src/features/page/queries/page-query.ts @@ -43,7 +43,7 @@ export function useDeletePageMutation() { return useMutation({ mutationFn: (pageId: string) => deletePage(pageId), onSuccess: () => { - notifications.show({ title: 'Page deleted successfully' }); + notifications.show({ message: 'Page deleted successfully' }); }, }); } diff --git a/client/src/lib/time-ago.ts b/client/src/lib/time-ago.ts deleted file mode 100644 index 152e35c6..00000000 --- a/client/src/lib/time-ago.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { formatDistanceStrict } from 'date-fns'; - -export function timeAgo(date: Date){ - return formatDistanceStrict(new Date(date), new Date(), { addSuffix: true }) -} diff --git a/client/src/lib/time.ts b/client/src/lib/time.ts new file mode 100644 index 00000000..8e20e653 --- /dev/null +++ b/client/src/lib/time.ts @@ -0,0 +1,16 @@ +import { formatDistanceStrict } from 'date-fns'; +import { format, isToday, isYesterday } from 'date-fns'; + +export function timeAgo(date: Date) { + return formatDistanceStrict(new Date(date), new Date(), { addSuffix: true }); +} + +export function formatDate(date: Date) { + if (isToday(date)) { + return `Today, ${format(date, 'h:mma')}`; + } else if (isYesterday(date)) { + return `Yesterday, ${format(date, 'h:mma')}`; + } else { + return format(date, 'MMM dd, yyyy, h:mma'); + } +}