Skip to content

Commit

Permalink
🛠 Initial implementation of reply feature
Browse files Browse the repository at this point in the history
  • Loading branch information
jgudo committed Mar 16, 2021
1 parent 8899739 commit 4e47dae
Show file tree
Hide file tree
Showing 13 changed files with 630 additions and 121 deletions.
6 changes: 1 addition & 5 deletions frontend/.eslintcache

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"@craco/craco": "^6.0.0",
"@neojp/tailwindcss-important-variant": "^1.0.1",
"@tailwindcss/custom-forms": "^0.2.1",
"@tailwindcss/jit": "^0.1.1",
"@types/lodash.debounce": "^4.0.6",
"@types/react-modal": "^3.10.6",
"@types/react-transition-group": "^4.4.0",
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/components/main/Comments/CommentInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { forwardRef } from "react";
import { Avatar } from "~/components/shared";

interface IProps {
isLoading: boolean;
isSubmitting: boolean;
isUpdateMode: boolean;
userPicture: {
url: string;
[prop: string]: any;
},
[prop: string]: any;
}

const CommentInput = forwardRef<HTMLInputElement, IProps>((props, ref) => {
const { isUpdateMode, isSubmitting, userPicture, isLoading, ...rest } = props;

return (
<div className={`flex items-center py-4 px-2 ${isUpdateMode && 'bg-yellow-100 dark:bg-indigo-1100 rounded-2xl'}`}>
<Avatar url={userPicture?.url} className="mr-2" />
<div className="flex-grow">
<input
{...rest}
className={`${isSubmitting && isLoading && 'opacity-50'} dark:bg-indigo-1100 dark:!border-gray-800 dark:text-white`}
type="text"
readOnly={isLoading || isSubmitting}
ref={ref}
/>
</div>
</div>
);
});

export default CommentInput;
163 changes: 163 additions & 0 deletions frontend/src/components/main/Comments/CommentItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { DownOutlined } from "@ant-design/icons";
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { useRef, useState } from "react";
import { Link } from "react-router-dom";
import { Avatar } from "~/components/shared";
import { useDidMount } from "~/hooks";
import { getCommentReplies, replyOnComment } from "~/services/api";
import { IComment } from "~/types/types";
import CommentInput from "./CommentInput";
import CommentList from "./CommentList";

dayjs.extend(relativeTime);

interface IProps {
comment: IComment;
}

const CommentItem: React.FC<IProps> = ({ comment }) => {
const [offset, setOffset] = useState(0);
const [replies, setReplies] = useState<IComment[]>([]);
const [isOpenInput, setOpenInput] = useState(false);
const [replyBody, setReplyBody] = useState('');
const [isSubmitting, setSubmitting] = useState(false);
const [isUpdateMode, setUpdateMode] = useState(false);
const didMount = useDidMount(true);
const replyInputRef = useRef<HTMLInputElement | null>(null);

const getReplies = async (comment_id: string) => {
try {
const res = await getCommentReplies({ offset, comment_id: comment.id, post_id: comment.post_id });

setReplies(res.replies);
setOffset(offset + 1);
} catch (e) {
console.log(e);
}
}

const handleSubmitReply = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && replyBody) {
try {
setSubmitting(true);
const reply = await replyOnComment(replyBody, comment.id, comment.post_id);

if (didMount) {
if (isUpdateMode) {
} else {
setReplies([...replies, reply]);
}

setReplyBody('');
setUpdateMode(false);
setSubmitting(false);
}
} catch (e) {
if (didMount) {
setSubmitting(false);
// setError(e.error.message);
}
}
} else if (e.key === 'Escape') {
// if (isUpdateMode) handleCancelUpdate();
if (replyInputRef.current) replyInputRef.current.blur();
}

};

const onClickReply = () => {
setOpenInput(!isOpenInput);
};

const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setReplyBody(e.target.value);
};

return (
<div
className="flex py-2 items-start"
key={comment.id}
>
<Link to={`/user/${comment.author.username}`} className="mr-2">
<Avatar url={comment.author.profilePicture?.url} />
</Link>
<div className="inline-flex items-start flex-col w-full laptop:w-auto">
<div className="flex items-start">
{/* ------ USERNAME AND COMMENT TEXT ----- */}
<div className="bg-gray-100 dark:bg-indigo-950 px-2 py-1 rounded-md">
<Link to={`/user/${comment.author.username}`}>
<h5 className="dark:text-indigo-400">{comment.author.username}</h5>
</Link>
<p className="text-gray-800 text-sm min-w-full break-all dark:text-gray-200">
{comment.body}
</p>
</div>
{/* {(comment.isOwnComment || comment.isPostOwner) && (
<CommentOptions
setCommentBody={setCommentBody}
comment={comment}
openDeleteModal={deleteModal.openModal}
setTargetID={setTargetID}
setIsUpdating={setIsUpdating}
commentInputRef={commentInputRef}
setInputCommentVisible={setInputCommentVisible}
/>
)} */}
</div>
<div className="mx-2">
{/* ---- DATE AND LIKE BUTTON ----- */}
<div className="mt-1 flex items-center space-x-2">
{/* ---- LIKE BUTTON ---- */}
<span className="text-gray-400 hover:cursor-pointer hover:text-gray-800 dark:hover:text-gray-200 text-xs">
Like
</span>
{/* ---- REPLY BUTTON */}
{comment.depth < 3 && (
<span
className="text-gray-400 hover:cursor-pointer hover:text-gray-800 dark:hover:text-gray-200 text-xs"
onClick={onClickReply}
>
Reply
</span>
)}
{/* ---- DATE ---- */}
<span className="text-xs text-gray-400 dark:text-gray-500">
{dayjs(comment.createdAt).fromNow()}
</span>
{comment.isEdited && (
<span className="text-xs text-gray-400 dark:text-gray-500 ml-2">
Edited
</span>
)}
</div>
{/* ---- VIEW MORE BUTTON ---- */}
{comment.replyCount > 0 && (
<span
className="text-xs text-indigo-400 hover:text-indigo-500 dark:hover:text-indigo-200 mt-2 hover:cursor-pointer"
onClick={() => getReplies(comment.id)}
>
View Replies <DownOutlined className="text-1xs" />
</span>
)}
{/* ---- REPLY LIST ------- */}
<CommentList comments={replies} />
{/* ------ REPLY INPUT ----- */}
{isOpenInput && (
<CommentInput
value={replyBody}
placeholder="Write a reply..."
onChange={handleOnChange}
isSubmitting={isSubmitting}
ref={replyInputRef}
isUpdateMode={isUpdateMode}
onKeyDown={handleSubmitReply}
/>
)}
</div>
</div>
</div>
)
};

export default CommentItem;
25 changes: 25 additions & 0 deletions frontend/src/components/main/Comments/CommentList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CSSTransition, TransitionGroup } from "react-transition-group";
import { IComment } from "~/types/types";
import CommentItem from "./CommentItem";

interface IProps {
comments: IComment[];
}

const CommentList: React.FC<IProps> = ({ comments }) => {
return (
<TransitionGroup component={null}>
{comments.map(comment => (
<CSSTransition
timeout={500}
classNames="fade"
key={comment.id}
>
<CommentItem comment={comment} />
</CSSTransition>
))}
</TransitionGroup>
);
};

export default CommentList;
71 changes: 71 additions & 0 deletions frontend/src/components/main/Comments/Replies.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
export default () => {

}
// import { DownOutlined } from "@ant-design/icons";
// import { useState } from "react";
// import { Link } from "react-router-dom";
// import { Avatar } from "~/components/shared";

// const Replies = () => {
// const [replies, setReplies] = useState([]);

// return (
// <div
// className="flex py-2 items-start"
// key={comment.id}
// >
// <Link to={`/user/${comment.author.username}`} className="mr-2">
// <Avatar url={comment.author.profilePicture?.url} />
// </Link>
// <div className="inline-flex items-start flex-col w-full laptop:w-auto">
// <div className="flex items-start">
// {/* ------ USERNAME AND COMMENT TEXT ----- */}
// <div className="bg-gray-100 dark:bg-indigo-950 px-2 py-1 rounded-md">
// <Link to={`/user/${comment.author.username}`}>
// <h5 className="dark:text-indigo-400">{comment.author.username}</h5>
// </Link>
// <p className="text-gray-800 text-sm min-w-full break-all dark:text-gray-200">
// {comment.body}
// </p>
// </div>
// {(comment.isOwnComment || comment.isPostOwner) && (
// <CommentOptions
// setCommentBody={setCommentBody}
// comment={comment}
// openDeleteModal={deleteModal.openModal}
// setTargetID={setTargetID}
// setIsUpdating={setIsUpdating}
// commentInputRef={commentInputRef}
// setInputCommentVisible={setInputCommentVisible}
// />
// )}
// </div>
// <div className="mx-2">
// {/* ---- DATE AND LIKE BUTTON ----- */}
// <div className="mt-1 flex items-center space-x-2">
// <span className="text-gray-400 hover:cursor-pointer hover:text-gray-800 dark:hover:text-gray-200 text-xs">Like</span>
// <span className="text-gray-400 hover:cursor-pointer hover:text-gray-800 dark:hover:text-gray-200 text-xs">Reply</span>
// <span className="text-xs text-gray-400 dark:text-gray-500">
// {dayjs(comment.createdAt).fromNow()}
// </span>
// {comment.isEdited && (
// <span className="text-xs text-gray-400 dark:text-gray-500 ml-2">
// Edited
// </span>
// )}
// </div>
// {comment.replyCount > 0 && (
// <span
// className="text-xs text-indigo-400 hover:text-indigo-500 dark:hover:text-indigo-200 mt-2 hover:cursor-pointer"
// onClick={() => getReplies(comment.id)}
// >
// View Replies <DownOutlined className="text-1xs" />
// </span>
// )}
// </div>
// </div>
// </div>
// )
// };

// export default Replies;
Loading

0 comments on commit 4e47dae

Please sign in to comment.