added deleted comment indicator

This commit is contained in:
mr-shortman 2025-03-22 17:48:05 +01:00
parent 4a67f1f94b
commit b2cd0fc560
10 changed files with 46 additions and 155 deletions

View File

@ -60,7 +60,6 @@
"lucide-react": "^0.477.0",
"next": "^15.0.1",
"next-auth": "5.0.0-beta.25",
"next-safe-action": "^7.10.4",
"next-themes": "^0.4.4",
"novel": "^1.0.2",
"postgres": "^3.4.4",

48
pnpm-lock.yaml generated
View File

@ -122,9 +122,6 @@ importers:
next-auth:
specifier: 5.0.0-beta.25
version: 5.0.0-beta.25(next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
next-safe-action:
specifier: ^7.10.4
version: 7.10.4(next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.24.2)
next-themes:
specifier: ^0.4.4
version: 0.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -3572,27 +3569,6 @@ packages:
nodemailer:
optional: true
next-safe-action@7.10.4:
resolution: {integrity: sha512-rZE89DTNgiTJ8oPQBOZm+jd0Uf/pLkN+GE3PndER6vWqHGYGN7HMLvhpggkbE8W4TGp+qM7CPyrXye1wCjZLVg==}
engines: {node: '>=18.17'}
peerDependencies:
'@sinclair/typebox': '>= 0.33.3'
next: '>= 14.0.0'
react: '>= 18.2.0'
react-dom: '>= 18.2.0'
valibot: '>= 0.36.0'
yup: '>= 1.0.0'
zod: '>= 3.0.0'
peerDependenciesMeta:
'@sinclair/typebox':
optional: true
valibot:
optional: true
yup:
optional: true
zod:
optional: true
next-themes@0.4.4:
resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==}
peerDependencies:
@ -7182,8 +7158,8 @@ snapshots:
'@typescript-eslint/parser': 8.26.0(eslint@8.57.1)(typescript@5.8.2)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1)
eslint-plugin-react: 7.37.4(eslint@8.57.1)
eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1)
@ -7202,7 +7178,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1):
eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.0(supports-color@5.5.0)
@ -7213,18 +7189,18 @@ snapshots:
stable-hash: 0.0.4
tinyglobby: 0.2.12
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.26.0(eslint@8.57.1)(typescript@5.8.2)
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1)
eslint-import-resolver-typescript: 3.8.3(eslint-plugin-import@2.31.0)(eslint@8.57.1)
transitivePeerDependencies:
- supports-color
@ -7232,7 +7208,7 @@ snapshots:
dependencies:
eslint: 8.57.1
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.8
@ -7243,7 +7219,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.8.3)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@ -8260,14 +8236,6 @@ snapshots:
next: 15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
next-safe-action@7.10.4(next@15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.24.2):
dependencies:
next: 15.2.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
zod: 3.24.2
next-themes@0.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1

View File

@ -15,7 +15,6 @@ import {
} from "@/components/ui/form";
import { commentSchema } from "@/lib/validation/zod/comment";
import CommentEditor from "../editor/comment-editor";
import { createComment } from "@/server/actions/comment";
import { toast } from "sonner";
import { EditorInstance } from "novel";
import { XIcon } from "lucide-react";
@ -39,8 +38,15 @@ function CommentForm({
onSuccess: (comment) => {
setLoading(false);
socket?.emit(ADD_COMMENT_EVENT, comment);
toast.success("Kommentar hinzugefügt.");
setTimeout(() => {
const el = commentRefs.current.get(comment?.id!);
el?.scrollIntoView({
behavior: "smooth",
block: "start",
});
el?.classList.add("animate-fade-in");
}, 500);
},
onError: () => {
setLoading(false);
@ -65,15 +71,6 @@ function CommentForm({
form.reset();
editor?.commands?.clearContent();
removeReplyComment();
// setTimeout(() => {
// const el = commentRefs.current.get(res?.data?.data!);
// console.log("el", el);
// el?.scrollIntoView({
// behavior: "smooth",
// block: "start",
// });
// el?.classList.add("animate-fade-in");
// }, 500);
}
React.useEffect(() => {
@ -116,6 +113,7 @@ function CommentForm({
type="submit"
className="ml-auto h-max py-1"
size="sm"
disabled={loading || !form.formState.isDirty}
>
Absenden
</Button>

View File

@ -4,7 +4,7 @@ import { buildCommentTree } from "@/lib/utils/comments";
import Comment from "./comment";
import { useSession } from "../session-provider";
function CommentList({
export const CommentList = React.memo(function ({
comments,
setReplyComment,
commentRefs,
@ -29,5 +29,5 @@ function CommentList({
))}
</ul>
);
}
});
export default CommentList;

View File

@ -19,32 +19,22 @@ const Comment = React.memo(
level?: number;
}) => {
const { comment, setReplyComment, setRef, session, level = 0 } = props;
const isAuthor = session?.user?.id === comment.author?.id;
const isAuthor = session?.user?.id === comment?.author?.id;
return (
// <div className="">
// </div>
// <div className="comment-replies" style={{ marginLeft: "20px" }}>
// {comment.children.map((child) => (
// <Comment
// key={child.id}
// comment={child}
// setReplyComment={setReplyComment}
// setRef={(el) => setRef(el)} // Ensure this is stable
// session={session}
// level={level + 1}
// />
// ))}
// </div>
// </div>
<div
className={cn(
level > 0 && "ml-4 border-l",
comment.children.length && "border-l",
comment?.children?.length && "border-l",
comment?.missingParent && "border-l",
)}
>
{comment.missingParent && (
<div className="p-4 pt-0 text-sm italic text-muted-foreground">
Antwort auf: Kommentar wurde gelöscht
</div>
)}
<div
className="flex items-start gap-2 pl-2 hover:bg-muted"
ref={setRef}
@ -57,7 +47,7 @@ const Comment = React.memo(
<div className="w-full">
<div className="flex w-full items-center gap-2">
<h5 className="text-lg font-medium capitalize">
{comment.author?.name} {level}
{comment.author?.name}
</h5>
{isAuthor && (
<Badge variant={"outline"} className="text-xs">
@ -85,39 +75,21 @@ const Comment = React.memo(
</div>
</div>
</div>
{comment.children && comment.children.length > 0 && (
{comment?.children && comment.children.length > 0 && (
<div className="pl-4">
{comment.children.map((child) => (
{comment.children.map((child, idx) => (
<Comment
key={child.id}
key={child?.id ?? idx}
{...props}
comment={child}
comment={child as CommentNode}
level={level + 1}
/>
))}
</div>
)}
</div>
// <div className={`flex ${level > 0 ? "ml-4" : ""} w-full`}>
// <div
// className={`${comment.children?.length || level > 0 ? "border-l" : ""} w-full pl-2`}
// >
// {comment.children && comment.children.length > 0 && (
// <div className="pl-4">
// {comment.children.map((child) => (
// <Comment
// key={child.id}
// {...props}
// comment={child}
// level={level + 1}
// />
// ))}
// </div>
// )}
// </div>
// </div>
);
},
);
export default Comment;

View File

@ -9,28 +9,32 @@ export function buildCommentTree(comments: Comment[]): Array<CommentNode> {
commentMap[comment.id] = {
...comment,
children: [],
missingParent: false,
};
});
// Second pass: Build the tree by connecting children to parents
const rootComments: CommentNode[] = [];
const rootComments: Array<CommentNode> = [];
comments.forEach((comment) => {
const node = commentMap[comment.id];
const node = commentMap[comment.id]!;
// If parentId is null or empty string, it's a root comment
if (!comment.parentId) {
rootComments.push(node!);
rootComments.push(node);
} else {
// Add as a child to its parent if parent exists
const parent = commentMap[comment.parentId];
if (parent) {
parent.children.push(node!);
// Add as a child to its parent if parent exists
parent.children.push(node);
} else {
// Parent doesn't exist, treat as root comment
// Mark that the parent is missing
node.missingParent = true;
rootComments.push(node!);
}
}
});
return rootComments;
}

View File

@ -1,48 +0,0 @@
"use server";
import { appRoutes } from "@/config";
import { commentSchema } from "@/lib/validation/zod/comment";
import { api } from "@/trpc/server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { actionClient } from ".";
export const createComment = actionClient
.schema(
z.object({
comment: commentSchema,
articleId: z.string(),
}),
)
.action(async ({ parsedInput: { comment, articleId } }) => {
const commentId = await api.comment.create({
comment,
articleId,
});
if (!commentId)
return {
failure: "Incorrect credentials",
};
revalidatePath(appRoutes.article(articleId));
return {
data: commentId,
success: true,
};
});
export async function deleteComment(commentId: string) {
const articleId = (
await api.comment.delete({
commentId,
})
)?.articleId;
if (!articleId)
return {
success: false,
message: "Error deleting comment",
};
revalidatePath(appRoutes.article(articleId));
return {
success: true,
};
}

View File

@ -1,3 +0,0 @@
import { createSafeActionClient } from "next-safe-action";
export const actionClient = createSafeActionClient();

View File

@ -60,7 +60,8 @@ export type Comment = typeof comments.$inferSelect & {
};
export type CommentNode = Comment & {
children: CommentNode[];
children: Array<CommentNode | DeletedNode>;
missingParent: boolean;
};
export const commentVotes = createTable(

View File

@ -19,7 +19,7 @@ const createSocketServer = () => {
if (!global.io) {
global.io = new Server(server, {
cors: {
origin,
origin: "*",
methods: ["GET", "POST"],
},
});