Skip to content

Commit

Permalink
🛠️ [FIX] Reply to a comment. (#7)
Browse files Browse the repository at this point in the history
- Default sort is by oldest comments
- Set nesting level to 2
- Autofocus textbox when clicking reply

---------

Co-authored-by: Tareq Hasan <[email protected]>
  • Loading branch information
RatulHasan and tareq1988 authored May 17, 2024
1 parent f7a2b57 commit 7266b5c
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 128 deletions.
8 changes: 7 additions & 1 deletion app/Http/Controllers/Frontend/CommentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class CommentController extends Controller
// index function
public function index(Request $request, Post $post)
{
$sort = $request->has('sort') && in_array($request->sort, ['latest', 'oldest']) ? $request->sort : 'latest';
$sort = $request->has('sort') && in_array($request->sort, ['latest', 'oldest']) ? $request->sort : 'oldest';
$orderBy = ($sort === 'latest') ? 'desc' : 'asc';

$comments = $post->comments()->with('user', 'status')->orderBy('created_at', $orderBy)->get();
Expand All @@ -27,13 +27,19 @@ public function index(Request $request, Post $post)
// Recursive function to build comment tree
$buildCommentTree = function ($parentId = null) use (&$buildCommentTree, &$groupedComments) {
$result = [];

if (isset($groupedComments[$parentId])) {
foreach ($groupedComments[$parentId] as $comment) {
$children = $buildCommentTree($comment->id);
$comment->body = Formatting::transformBody($comment->body);
$comment->children = $children;
$result[] = $comment;
}

// Sort comments by id
usort($result, function ($a, $b) {
return $a->id - $b->id;
});
}
return $result;
};
Expand Down
8 changes: 8 additions & 0 deletions app/Models/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ protected static function boot()
]);
});

// Delete all children when a parent comment is deleted
static::deleting(function ($comment) {
foreach ($comment->children as $child) {
$child->delete();
}
});

// Update the comments count when a comment is deleted
static::deleted(function ($comment) {
$comment->post->update([
'comments' => $comment->post->comments()->where('parent_id', null)->count('id')
Expand Down
144 changes: 144 additions & 0 deletions resources/js/Components/Comment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React, { useState } from 'react';
import axios from 'axios';
import classNames from 'classnames';
import { usePage } from '@inertiajs/react';

import { CommentType, PageProps, PostType, User } from '@/types';
import { formatDate } from '@/utils';
import CommentBox from './CommentBox';

type CommentProps = {
comment: CommentType;
post: PostType;
onCommentDelete: (commentId: number) => void;
level?: number;
};

const Comment = ({
post,
comment,
onCommentDelete,
level = 0,
}: CommentProps) => {
const [showReplyBox, setShowReplyBox] = useState(false);
const { auth } = usePage<PageProps>().props;
const [commentState, setCommentState] = useState<CommentType>(comment);

const deleteComment = (commentId: number) => {
if (!confirm('Are you sure you want to delete this comment?')) {
return;
}

axios
.delete(route('admin.comments.destroy', [commentId]))
.then(() => {
onCommentDelete(commentId);
})
.catch((error) => {
alert(error.response.data.message);
});
};

const appendComment = (comment: CommentType) => {
setCommentState({
...commentState,
children: [...commentState.children, comment],
});

setShowReplyBox(false);
};

return (
<div className="flex py-3">
<div className="w-9 mr-3">
<img
src={commentState.user?.avatar}
className={classNames(
'rounded-full h-7 w-7',
commentState.user?.role === 'admin' ? 'ring-2 ring-indigo-500' : ''
)}
/>
</div>

<div className="flex-1">
<div className="flex items-center text-sm mb-2">
<div className="font-semibold dark:text-gray-300">
{commentState.user?.name}
</div>
{commentState.status && (
<div className="text-sm text-gray-700 ml-2">
<span>marked this post as</span>
<span
className="uppercase text-xs font-bold ml-2 text-white px-2 py-1 rounded"
style={{ backgroundColor: commentState.status.color }}
>
{commentState.status.name}
</span>
</div>
)}
</div>
<div
className="text-sm text-gray-800 dark:text-gray-300 mb-2"
dangerouslySetInnerHTML={{ __html: commentState.body }}
></div>
<div className="flex text-xs text-gray-500">
<div className="">{formatDate(commentState.created_at)}</div>

{(level === 0 || level === 1) && auth.user !== null && (
<>
<div className="mx-1"></div>
<div
className="cursor-pointer hover:text-gray-800 dark:hover:text-gray-300"
onClick={(_) => setShowReplyBox((prev) => !prev)}
>
Reply
</div>
</>
)}

{auth.user?.role === 'admin' && (
<>
<div className="mx-1"></div>
<div className="">
<button
className="text-xs text-red-500 dark:text-red-300"
onClick={() => deleteComment(commentState.id)}
>
Delete
</button>
</div>
</>
)}
</div>

{commentState.children.length > 0 && (
<div className="mt-3">
{commentState.children.map((child) => (
<Comment
key={child.id}
post={post}
comment={child}
level={level + 1}
onCommentDelete={() => {}}
/>
))}
</div>
)}

{showReplyBox && (
<div className="mt-4">
<CommentBox
post={post}
parent={comment.id}
onComment={appendComment}
focus={showReplyBox}
onCancel={() => setShowReplyBox(false)}
/>
</div>
)}
</div>
</div>
);
};

export default Comment;
17 changes: 15 additions & 2 deletions resources/js/Components/CommentBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ import { BoardType, PageProps, PostType, CommentType, User } from '@/types';
type Props = {
post: PostType;
parent?: number;
focus?: boolean;
onComment?: (comment: CommentType) => void;
onCancel?: () => void;
};

const CommentBox = ({ post, parent, onComment }: Props) => {
const CommentBox = ({
post,
parent,
onComment,
focus = false,
onCancel,
}: Props) => {
const { auth } = usePage<PageProps>().props;
const [isFocused, setIsFocused] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
Expand Down Expand Up @@ -107,6 +115,7 @@ const CommentBox = ({ post, parent, onComment }: Props) => {
onBlur={() => setIsFocused(false)}
placeholder="Write a comment..."
onChange={onTextareaChange}
autoFocus={focus}
onKeyDown={(e) => {
if (e.metaKey && e.key === 'Enter') {
createComment();
Expand All @@ -118,7 +127,11 @@ const CommentBox = ({ post, parent, onComment }: Props) => {
<div className="flex border-t border-gray-300 dark:border-gray-700 px-3 py-2 justify-between items-center">
<div className="text-xs text-gray-500">
{parent ? (
<span>&nbsp;</span>
<span>
<Button variant="secondary" style="link" onClick={onCancel}>
Cancel
</Button>
</span>
) : (
<span>
The post author and the voters will get an email notification.
Expand Down
Loading

0 comments on commit 7266b5c

Please sign in to comment.