Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/Feat: comment counters, actions based on role, pinned comment #170

Merged
merged 3 commits into from
Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ model Comment {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
votes Vote[]
isPinned Boolean @default(false)
}

model Vote {
Expand All @@ -196,3 +197,4 @@ enum CommentType {
INTRO
DEFAULT
}

91 changes: 83 additions & 8 deletions src/actions/comment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import {
InputTypeApproveIntroComment,
InputTypeCreateComment,
InputTypeDeleteComment,
InputTypePinComment,
InputTypeUpdateComment,
ReturnTypeApproveIntroComment,
ReturnTypeCreateComment,
ReturnTypeDeleteComment,
ReturnTypePinComment,
ReturnTypeUpdateComment,
} from './types';
import { authOptions } from '@/lib/auth';
Expand All @@ -17,11 +19,13 @@ import {
CommentApproveIntroSchema,
CommentDeleteSchema,
CommentInsertSchema,
CommentPinSchema,
CommentUpdateSchema,
} from './schema';
import { createSafeAction } from '@/lib/create-safe-action';
import { CommentType, Prisma } from '@prisma/client';
import { revalidatePath } from 'next/cache';
import { ROLES } from '../types';

export const getComments = async (
q: Prisma.CommentFindManyArgs,
Expand All @@ -39,11 +43,30 @@ export const getComments = async (
if (!parentComment) {
delete q.where?.parentId;
}
const pinnedComment = await prisma.comment.findFirst({
where: {
contentId: q.where?.contentId,
isPinned: true,
...(parentId ? { parentId: parseInt(parentId.toString(), 10) } : {}),
},
include: q.include,
});
if (pinnedComment) {
q.where = {
...q.where,
NOT: {
id: pinnedComment.id,
},
};
}

const comments = await prisma.comment.findMany(q);
const combinedComments = pinnedComment
? [pinnedComment, ...comments]
: comments;

return {
comments,
comments: combinedComments,
parentComment,
};
};
Expand Down Expand Up @@ -268,10 +291,15 @@ const updateCommentHandler = async (
const approveIntroCommentHandler = async (
data: InputTypeApproveIntroComment,
): Promise<ReturnTypeApproveIntroComment> => {
const { content_comment_ids, approved, adminPassword } = data;
const session = await getServerSession(authOptions);
const { content_comment_ids, approved, adminPassword, currentPath } = data;

if (adminPassword !== process.env.ADMIN_SECRET) {
return { error: 'Unauthorized' };
if (adminPassword) {
if (adminPassword !== process.env.ADMIN_SECRET) {
return { error: 'Unauthorized' };
}
} else if (!session || !session.user || session.user.role !== ROLES.ADMIN) {
return { error: 'Unauthorized ' };
}

const [contentId, commentId] = content_comment_ids.split(';');
Expand Down Expand Up @@ -315,7 +343,9 @@ const approveIntroCommentHandler = async (
},
});
});

if (currentPath) {
revalidatePath(currentPath);
}
return { data: updatedComment! };
} catch (error) {
return { error: 'Failed to update comment.' };
Expand All @@ -331,21 +361,24 @@ const deleteCommentHandler = async (
return { error: 'Unauthorized or insufficient permissions' };
}

const { commentId, adminPassword } = data;
const { commentId } = data;
const userId = session.user.id;

try {
const existingComment = await prisma.comment.findUnique({
where: { id: commentId },
include: {
parent: true,
},
});

if (!existingComment) {
return { error: 'Comment not found.' };
}

if (
existingComment.userId !== userId &&
adminPassword !== process.env.ADMIN_SECRET
session.user?.role !== ROLES.ADMIN ||
existingComment.userId !== userId
) {
return { error: 'Unauthorized to delete this comment.' };
}
Expand All @@ -365,6 +398,15 @@ const deleteCommentHandler = async (
await prisma.comment.deleteMany({
where: { parentId: commentId },
});
await prisma.content.update({
where: { id: existingComment.contentId },
data: { commentsCount: { decrement: 1 } },
});
} else {
await prisma.comment.update({
where: { id: existingComment.parentId },
data: { repliesCount: { decrement: 1 } },
});
}

// Then delete the comment itself
Expand All @@ -383,6 +425,38 @@ const deleteCommentHandler = async (
}
};

const pinCommentHandler = async (
data: InputTypePinComment,
): Promise<ReturnTypePinComment> => {
const { commentId, contentId, currentPath } = data;
const session = await getServerSession(authOptions);

if (!session || !session.user || session.user.role !== ROLES.ADMIN) {
return { error: 'Unauthorized or insufficient permissions' };
}
let updatedComment;
try {
await prisma.$transaction(async (prisma) => {
// Unpin any currently pinned comment for the content
await prisma.comment.updateMany({
where: { contentId, isPinned: true },
data: { isPinned: false },
});

updatedComment = await prisma.comment.update({
where: { id: commentId },
data: { isPinned: true },
});
});
if (currentPath) {
revalidatePath(currentPath);
}
return { data: updatedComment };
} catch (error: any) {
return { error: error.message || 'Failed to pin comment.' };
}
};

export const createMessage = createSafeAction(
CommentInsertSchema,
createCommentHandler,
Expand All @@ -399,3 +473,4 @@ export const approveComment = createSafeAction(
CommentApproveIntroSchema,
approveIntroCommentHandler,
);
export const pinComment = createSafeAction(CommentPinSchema, pinCommentHandler);
6 changes: 6 additions & 0 deletions src/actions/comment/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ export const CommentApproveIntroSchema = z.object({
content_comment_ids: z.string(),
approved: z.boolean().optional(),
adminPassword: z.string().optional(),
currentPath: z.string().optional(),
});
export const CommentDeleteSchema = z.object({
adminPassword: z.string().optional(),
commentId: z.number(),
currentPath: z.string().optional(),
});
export const CommentPinSchema = z.object({
commentId: z.number(),
contentId: z.number(),
currentPath: z.string().optional(),
});
8 changes: 6 additions & 2 deletions src/actions/comment/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
CommentUpdateSchema,
CommentDeleteSchema,
CommentApproveIntroSchema,
CommentPinSchema,
} from './schema';
import { Delete } from '../types';
import { User, Comment } from '@prisma/client';
import { User, Comment, Vote } from '@prisma/client';

export type InputTypeCreateComment = z.infer<typeof CommentInsertSchema>;
export type ReturnTypeCreateComment = ActionState<
Expand All @@ -33,7 +34,10 @@ export type ReturnTypeDeleteComment = ActionState<
InputTypeDeleteComment,
Delete
>;
export type InputTypePinComment = z.infer<typeof CommentPinSchema>;
export type ReturnTypePinComment = ActionState<InputTypePinComment, Comment>;

export interface ExtendedComment extends Comment {
user: User;
user?: User;
votes?: Vote[];
}
5 changes: 5 additions & 0 deletions src/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,8 @@ export enum CommentFilter {
export type Delete = {
message: string;
};

export enum ROLES {
ADMIN = 'admin',
USER = 'user',
}
1 change: 0 additions & 1 deletion src/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export default function Courses() {
const { register, handleSubmit } = useForm<FormInput>();

const onSubmit: SubmitHandler<FormInput> = async (data) => {
console.log(data);
await fetch('/api/admin/course', {
body: JSON.stringify(data),
method: 'POST',
Expand Down
1 change: 1 addition & 0 deletions src/app/courses/[...courseId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { QueryParams } from '@/actions/types';

const checkAccess = async (courseId: string) => {
const session = await getServerSession(authOptions);

if (!session?.user) {
return false;
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/Copy-to-clipbord.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ const CopyToClipboard = ({
return (
<div>
<button onClick={handleCopyClick}>
<CopyIcon size={15} />
<div className="flex items-center gap-1">
Copy <CopyIcon size={15} />
</div>
</button>
</div>
);
Expand Down
47 changes: 47 additions & 0 deletions src/components/comment/CommentApproveForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

import { approveComment } from '@/actions/comment';
import { useAction } from '@/hooks/useAction';
import { CheckIcon } from 'lucide-react';
import { usePathname } from 'next/navigation';
import React from 'react';
import { toast } from 'sonner';

const CommentApproveForm = ({
commentId,
contentId,
}: {
commentId: number;
contentId: number;
}) => {
const currentPath = usePathname();

const { execute } = useAction(approveComment, {
onSuccess: () => {
toast('Comment Approved');
},
onError: (error) => {
toast.error(error);
},
});
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

execute({
content_comment_ids: `${contentId};${commentId}`,
approved: true,
currentPath,
});
};
return (
<form onSubmit={handleFormSubmit}>
<button type="submit">
<div className="flex gap-1 items-center">
Approve Chapters <CheckIcon className="w-4 h-4" />
</div>
</button>
</form>
);
};

export default CommentApproveForm;
4 changes: 3 additions & 1 deletion src/components/comment/CommentDeleteForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ const CommentDeleteForm = ({ commentId }: { commentId: number }) => {
return (
<form onSubmit={handleFormSubmit}>
<button type="submit">
<Trash2Icon className="w-4 h-4" />
<div className="flex gap-1 items-center">
Delete <Trash2Icon className="w-4 h-4" />
</div>
</button>
</form>
);
Expand Down
47 changes: 47 additions & 0 deletions src/components/comment/CommentPinForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

import { pinComment } from '@/actions/comment';
import { useAction } from '@/hooks/useAction';
import { PinIcon } from 'lucide-react';
import { usePathname } from 'next/navigation';
import React from 'react';
import { toast } from 'sonner';

const CommentPinForm = ({
commentId,
contentId,
}: {
commentId: number;
contentId: number;
}) => {
const currentPath = usePathname();

const { execute } = useAction(pinComment, {
onSuccess: () => {
toast('Comment Pinned');
},
onError: (error) => {
toast.error(error);
},
});
const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();

execute({
commentId,
contentId,
currentPath,
});
};
return (
<form onSubmit={handleFormSubmit}>
<button type="submit">
<div className="flex gap-1 items-center">
Pin <PinIcon className="w-4 h-4" />
</div>
</button>
</form>
);
};

export default CommentPinForm;
Loading
Loading