Skip to content

Commit

Permalink
added admin part to approve comments
Browse files Browse the repository at this point in the history
  • Loading branch information
siinghd committed Feb 9, 2024
1 parent 072a0bc commit 809ea58
Show file tree
Hide file tree
Showing 52 changed files with 488 additions and 251 deletions.
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"semi": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
Expand Down
95 changes: 87 additions & 8 deletions src/actions/comment/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use server';
import { getServerSession } from 'next-auth';
import {
InputTypeApproveIntroComment,
InputTypeCreateComment,
InputTypeDeleteComment,
InputTypeUpdateComment,
ReturnTypeApproveIntroComment,
ReturnTypeCreateComment,
ReturnTypeDeleteComment,
ReturnTypeUpdateComment,
Expand All @@ -12,6 +14,7 @@ import { authOptions } from '@/lib/auth';
import { rateLimit } from '@/lib/utils';
import prisma from '@/db';
import {
CommentApproveIntroSchema,
CommentDeleteSchema,
CommentInsertSchema,
CommentUpdateSchema,
Expand Down Expand Up @@ -74,7 +77,7 @@ const parseIntroComment = (
const lastLineParts = lines[lines.length - 1]
.split('-')
.map((part) => part.trim());
if (lastLineParts.length < 2) {
if (lastLineParts.length < 3) {
return null;
}
const timePattern = /(\d{2}):(\d{2})/;
Expand Down Expand Up @@ -154,6 +157,15 @@ const createCommentHandler = async (
| null = [];
if (data.content.startsWith('intro:')) {
introData = parseIntroComment(data.content);
if (
!introData ||
introData.length === 0 ||
introData[introData.length - 1].end === 0
) {
throw new Error(
'Invalid intro comment format, remember to include end time on the segment. Example: 12:24- 23:43 - Introduction to the course',
);
}
// Here you might want to store introData in a specific way, depending on your needs
}

Expand All @@ -177,11 +189,12 @@ const createCommentHandler = async (
});
});
}
revalidatePath(data.currentPath);
if (data.currentPath) {
revalidatePath(data.currentPath);
}
return { data: comment };
} catch (error) {
console.log('error', error);
return { error: 'Failed to create comment.' };
} catch (error: any) {
return { error: error.message || 'Failed to create comment.' };
}
};
const updateCommentHandler = async (
Expand Down Expand Up @@ -222,12 +235,70 @@ const updateCommentHandler = async (
where: { id: commentId },
data: updObj,
});
revalidatePath(data.currentPath);
if (data.currentPath) {
revalidatePath(data.currentPath);
}
return { data: updatedComment };
} catch (error) {
return { error: 'Failed to update comment.' };
}
};
const approveIntroCommentHandler = async (
data: InputTypeApproveIntroComment,
): Promise<ReturnTypeApproveIntroComment> => {
const { content_comment_ids, approved, adminPassword } = data;

if (adminPassword !== process.env.ADMIN_SECRET) {
return { error: 'Unauthorized' };
}

const [contentId, commentId] = content_comment_ids.split(';');
try {
const existingComment = await prisma.comment.findUnique({
where: { id: parseInt(commentId, 10) },
});

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

const introData = parseIntroComment(existingComment.content);

if (
!introData ||
introData.length === 0 ||
existingComment.commentType !== CommentType.INTRO
) {
return {
error:
'Comment is not an intro comment or can not be parsed. Plese check that last segment has end time include.',
};
}
// Update the comment but if its admin we need to check if the comment is approved
const updObj = {
approved,
};
let updatedComment = null;
await prisma.$transaction(async (prisma) => {
updatedComment = await prisma.comment.update({
where: { id: parseInt(commentId, 10) },
data: updObj,
});
await prisma.videoMetadata.update({
where: {
contentId: Number(contentId),
},
data: {
segments: introData,
},
});
});

return { data: updatedComment! };
} catch (error) {
return { error: 'Failed to update comment.' };
}
};

const deleteCommentHandler = async (
data: InputTypeDeleteComment,
Expand Down Expand Up @@ -279,8 +350,12 @@ const deleteCommentHandler = async (
where: { id: commentId },
});
});
revalidatePath(data.currentPath);
return { data: { message: 'Comment and its replies deleted successfully' } };
if (data.currentPath) {
revalidatePath(data.currentPath);
}
return {
data: { message: 'Comment and its replies deleted successfully' },
};
} catch (error) {
return { error: 'Failed to delete comment.' };
}
Expand All @@ -298,3 +373,7 @@ export const deleteMessage = createSafeAction(
CommentDeleteSchema,
deleteCommentHandler,
);
export const approveComment = createSafeAction(
CommentApproveIntroSchema,
approveIntroCommentHandler,
);
12 changes: 8 additions & 4 deletions src/actions/comment/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const CommentInsertSchema = z.object({
content: z.string().min(1, 'Comment content is required'),
contentId: z.number(),
parentId: z.number().optional(),
currentPath: z.string(),
currentPath: z.string().optional(),
});

export const CommentUpdateSchema = z.object({
Expand All @@ -14,11 +14,15 @@ export const CommentUpdateSchema = z.object({
// downVotes: z.number().optional(),
approved: z.boolean().optional(),
adminPassword: z.string().optional(),
currentPath: z.string(),
currentPath: z.string().optional(),
});
export const CommentApproveIntroSchema = z.object({
content_comment_ids: z.string(),
approved: z.boolean().optional(),
adminPassword: z.string().optional(),
});

export const CommentDeleteSchema = z.object({
adminPassword: z.string().optional(),
commentId: z.number(),
currentPath: z.string(),
currentPath: z.string().optional(),
});
22 changes: 15 additions & 7 deletions src/actions/comment/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,36 @@ import {
CommentInsertSchema,
CommentUpdateSchema,
CommentDeleteSchema,
CommentApproveIntroSchema,
} from './schema';
import { Delete } from '../types';
import { User, Comment } from '@prisma/client';

export type InputTypeCreateComment = z.infer<typeof CommentInsertSchema>
export type InputTypeCreateComment = z.infer<typeof CommentInsertSchema>;
export type ReturnTypeCreateComment = ActionState<
InputTypeCreateComment,
Comment
>
>;

export type InputTypeUpdateComment = z.infer<typeof CommentUpdateSchema>
export type InputTypeUpdateComment = z.infer<typeof CommentUpdateSchema>;
export type ReturnTypeUpdateComment = ActionState<
InputTypeUpdateComment,
Comment
>
>;
export type InputTypeApproveIntroComment = z.infer<
typeof CommentApproveIntroSchema
>;
export type ReturnTypeApproveIntroComment = ActionState<
InputTypeApproveIntroComment,
Comment
>;

export type InputTypeDeleteComment = z.infer<typeof CommentDeleteSchema>
export type InputTypeDeleteComment = z.infer<typeof CommentDeleteSchema>;
export type ReturnTypeDeleteComment = ActionState<
InputTypeDeleteComment,
Delete
>
>;

export interface ExtendedComment extends Comment {
user: User
user: User;
}
5 changes: 4 additions & 1 deletion src/actions/commentVote/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,7 @@ const voteHandler = async (
}
};

export const voteHandlerAction = createSafeAction(VoteHandleSchema, voteHandler);
export const voteHandlerAction = createSafeAction(
VoteHandleSchema,
voteHandler,
);
4 changes: 2 additions & 2 deletions src/actions/commentVote/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { ActionState } from '@/lib/create-safe-action';
import { z } from 'zod';
import { VoteHandleSchema } from './schema';

export type InputTypeHandleVote = z.infer<typeof VoteHandleSchema>
export type InputTypeHandleVote = z.infer<typeof VoteHandleSchema>;
export type ReturnTypeHandleVote = ActionState<
InputTypeHandleVote,
Comment | null
>
>;
24 changes: 12 additions & 12 deletions src/actions/types.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { CommentType } from '@prisma/client';

export interface QueryParams {
limit?: number
page?: number
commentfilter?: CommentFilter
search?: string
date?: string
type?: CommentType
parentId?: number
userId?: number
commentId?: number
timestamp?: number
limit?: number;
page?: number;
commentfilter?: CommentFilter;
search?: string;
date?: string;
type?: CommentType;
parentId?: number;
userId?: number;
commentId?: number;
timestamp?: number;
}
export enum CommentFilter {
md = 'Most downvotes',
mu = 'Most upvotes',
mr = 'Most Recent',
}
export type Delete = {
message: string
}
message: string;
};
79 changes: 79 additions & 0 deletions src/app/admin/comment/ApproveComment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use client';
import { approveComment } from '@/actions/comment';
import { FormErrors } from '@/components/FormError';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useAction } from '@/hooks/useAction';
import { Label } from '@radix-ui/react-dropdown-menu';
import React from 'react';
import { toast } from 'sonner';

const ApproveComment = () => {
const formRef = React.useRef<HTMLFormElement>(null);
const { execute, fieldErrors } = useAction(approveComment, {
onSuccess: () => {
toast('Comment added');
formRef.current?.reset();
},
onError: (error) => {
toast.error(error);
},
});

const handleApprove = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);

const commentId = formData.get('commentId') as string;
const adminPassword = formData.get('adminPassword') as string;
execute({
content_comment_ids: commentId,
adminPassword,
approved: true,
});
};
return (
<form onSubmit={handleApprove} ref={formRef}>
<div className="w-full max-w-sm rounded-lg border border-gray-200 grid grid-rows-5 shadow-sm dark:border-gray-800">
<div className="p-6 grid gap-2 items-center row-span-3">
<div className="text-3xl font-bold">Approve Intro comment</div>
<div className="text-sm font-medium leading-none text-gray-500 dark:text-gray-400">
Enter the information below to approve the comment
</div>
</div>

<div className="p-6 flex items-center row-span-2">
<Label className="sr-only">Comment ID</Label>
<Input
className="w-full"
id="commentId"
name="commentId"
placeholder="Content ID; Comment ID"
style={{
minWidth: '0',
}}
/>
<FormErrors id="commentId" errors={fieldErrors} />
</div>
<div className="p-6 flex items-center row-span-2">
<Label className="sr-only">Admin password</Label>
<Input
className="w-full"
id="adminPassword"
name="adminPassword"
placeholder="Admin password"
style={{
minWidth: '0',
}}
/>
<FormErrors id="adminPassword" errors={fieldErrors} />
</div>
<div className="p-6 flex items-center justify-center row-span-2">
<Button className="w-full">Approve</Button>
</div>
</div>
</form>
);
};

export default ApproveComment;
12 changes: 12 additions & 0 deletions src/app/admin/comment/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import ApproveComment from './ApproveComment';

const CommentAdminPage = () => {
return (
<div className="flex justify-center h-[100dvh] items-center">
<ApproveComment />
</div>
);
};

export default CommentAdminPage;
Loading

0 comments on commit 809ea58

Please sign in to comment.