├── .gitignore
├── LICENSE
├── README.md
├── demo.png
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.js
├── assets
├── down-arrow.svg
└── up-arrow.svg
├── components
├── Action.js
└── Comment.js
├── hooks
└── useNode.js
├── index.js
└── styles.css
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Suvang Samal
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # infinite-nested-comments
2 |
3 | Build a inifnite nested comments system using react js
4 |
5 | 
6 |
--------------------------------------------------------------------------------
/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xplodivity/infinite-nested-comments/0bef081cb0929c813062dd993d93a7e3b12b0de8/demo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "infinite-nested-comments",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.5",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "react": "^18.2.0",
10 | "react-dom": "^18.2.0",
11 | "react-scripts": "5.0.1",
12 | "web-vitals": "^2.1.4"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": [
22 | "react-app",
23 | "react-app/jest"
24 | ]
25 | },
26 | "browserslist": {
27 | "production": [
28 | ">0.2%",
29 | "not dead",
30 | "not op_mini all"
31 | ],
32 | "development": [
33 | "last 1 chrome version",
34 | "last 1 firefox version",
35 | "last 1 safari version"
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xplodivity/infinite-nested-comments/0bef081cb0929c813062dd993d93a7e3b12b0de8/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xplodivity/infinite-nested-comments/0bef081cb0929c813062dd993d93a7e3b12b0de8/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xplodivity/infinite-nested-comments/0bef081cb0929c813062dd993d93a7e3b12b0de8/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import Comment from "./components/Comment";
3 | import useNode from "./hooks/useNode";
4 | import "./styles.css";
5 |
6 | const comments = {
7 | id: 1,
8 | items: [],
9 | };
10 | const App = () => {
11 | const [commentsData, setCommentsData] = useState(comments);
12 |
13 | const { insertNode, editNode, deleteNode } = useNode();
14 |
15 | const handleInsertNode = (folderId, item) => {
16 | const finalStructure = insertNode(commentsData, folderId, item);
17 | setCommentsData(finalStructure);
18 | };
19 |
20 | const handleEditNode = (folderId, value) => {
21 | const finalStructure = editNode(commentsData, folderId, value);
22 | setCommentsData(finalStructure);
23 | };
24 |
25 | const handleDeleteNode = (folderId) => {
26 | const finalStructure = deleteNode(commentsData, folderId);
27 | const temp = { ...finalStructure };
28 | setCommentsData(temp);
29 | };
30 |
31 | return (
32 |
33 |
39 |
40 | );
41 | };
42 |
43 | export default App;
44 |
--------------------------------------------------------------------------------
/src/assets/down-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/assets/up-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/Action.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Action = ({ handleClick, type, className }) => {
4 | return (
5 |
6 | {type}
7 |
8 | );
9 | };
10 |
11 | export default Action;
12 |
--------------------------------------------------------------------------------
/src/components/Comment.js:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from "react";
2 | import Action from "./Action";
3 | import { ReactComponent as DownArrow } from "../assets/down-arrow.svg";
4 | import { ReactComponent as UpArrow } from "../assets/up-arrow.svg";
5 |
6 | const Comment = ({
7 | handleInsertNode,
8 | handleEditNode,
9 | handleDeleteNode,
10 | comment,
11 | }) => {
12 | const [input, setInput] = useState("");
13 | const [editMode, setEditMode] = useState(false);
14 | const [showInput, setShowInput] = useState(false);
15 | const [expand, setExpand] = useState(false);
16 | const inputRef = useRef(null);
17 |
18 | useEffect(() => {
19 | inputRef?.current?.focus();
20 | }, [editMode]);
21 |
22 | const handleNewComment = () => {
23 | setExpand(!expand);
24 | setShowInput(true);
25 | };
26 |
27 | const onAddComment = () => {
28 | if (editMode) {
29 | handleEditNode(comment.id, inputRef?.current?.innerText);
30 | } else {
31 | setExpand(true);
32 | handleInsertNode(comment.id, input);
33 | setShowInput(false);
34 | setInput("");
35 | }
36 |
37 | if (editMode) setEditMode(false);
38 | };
39 |
40 | const handleDelete = () => {
41 | handleDeleteNode(comment.id);
42 | };
43 |
44 | return (
45 |
46 |
47 | {comment.id === 1 ? (
48 | <>
49 |
setInput(e.target.value)}
55 | placeholder="type..."
56 | />
57 |
58 |
63 | >
64 | ) : (
65 | <>
66 |
72 | {comment.name}
73 |
74 |
75 |
76 | {editMode ? (
77 | <>
78 |
83 |
{
87 | if (inputRef.current)
88 | inputRef.current.innerText = comment.name;
89 | setEditMode(false);
90 | }}
91 | />
92 | >
93 | ) : (
94 | <>
95 |
99 | {expand ? (
100 |
101 | ) : (
102 |
103 | )}{" "}
104 | REPLY
105 | >
106 | }
107 | handleClick={handleNewComment}
108 | />
109 | {
113 | setEditMode(true);
114 | }}
115 | />
116 |
121 | >
122 | )}
123 |
124 | >
125 | )}
126 |
127 |
128 |
129 | {showInput && (
130 |
131 |
setInput(e.target.value)}
136 | />
137 |
138 |
{
142 | setShowInput(false);
143 | if (!comment?.items?.length) setExpand(false);
144 | }}
145 | />
146 |
147 | )}
148 |
149 | {comment?.items?.map((cmnt) => {
150 | return (
151 |
158 | );
159 | })}
160 |
161 |
162 | );
163 | };
164 |
165 | export default Comment;
166 |
--------------------------------------------------------------------------------
/src/hooks/useNode.js:
--------------------------------------------------------------------------------
1 | const useNode = () => {
2 | const insertNode = function (tree, commentId, item) {
3 | if (tree.id === commentId) {
4 | tree.items.push({
5 | id: new Date().getTime(),
6 | name: item,
7 | items: [],
8 | });
9 |
10 | return tree;
11 | }
12 |
13 | let latestNode = [];
14 | latestNode = tree.items.map((ob) => {
15 | return insertNode(ob, commentId, item);
16 | });
17 |
18 | return { ...tree, items: latestNode };
19 | };
20 |
21 | const editNode = (tree, commentId, value) => {
22 | if (tree.id === commentId) {
23 | tree.name = value;
24 | return tree;
25 | }
26 |
27 | tree.items.map((ob) => {
28 | return editNode(ob, commentId, value);
29 | });
30 |
31 | return { ...tree };
32 | };
33 |
34 | const deleteNode = (tree, id) => {
35 | for (let i = 0; i < tree.items.length; i++) {
36 | const currentItem = tree.items[i];
37 | if (currentItem.id === id) {
38 | tree.items.splice(i, 1);
39 | return tree;
40 | } else {
41 | deleteNode(currentItem, id);
42 | }
43 | }
44 | return tree;
45 | };
46 |
47 | return { insertNode, editNode, deleteNode };
48 | };
49 |
50 | export default useNode;
51 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App";
4 |
5 | const root = ReactDOM.createRoot(document.getElementById("root"));
6 | root.render(
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | font-family: Monaco, sans-serif;
3 | }
4 |
5 | .App {
6 | margin-top: 20px;
7 | width: fit-content;
8 | }
9 |
10 | .commentContainer {
11 | margin-top: 6px;
12 | background-color: #d3d3d3e0;
13 | display: flex;
14 | flex-direction: column;
15 | padding: 5px 10px;
16 | width: 300px;
17 | cursor: pointer;
18 | border-radius: 5px;
19 | }
20 |
21 | .commentContainer:hover {
22 | background-color: #d3d3d3bf;
23 | }
24 |
25 | .inputContainer {
26 | display: flex;
27 | align-items: baseline;
28 | gap: 5px;
29 | }
30 |
31 | .inputContainer > span {
32 | margin-top: 5px;
33 | }
34 |
35 | .inputContainer__input {
36 | margin: 6px 0 0px 0;
37 | padding: 5px;
38 | display: flex;
39 | border: 1px solid lightgray;
40 | align-items: center;
41 | justify-content: space-between;
42 | cursor: pointer;
43 | border-radius: 5px;
44 | background-color: #e7e7e7;
45 | }
46 |
47 | .first_input {
48 | margin: 0;
49 | }
50 |
51 | .commentContainer > span {
52 | margin: 0 5px;
53 | }
54 |
55 | .reply {
56 | font-size: 12px;
57 | padding: 5px;
58 | border-radius: 5px;
59 | color: #4e4e4e;
60 | font-weight: 600;
61 | cursor: pointer;
62 | }
63 |
64 | .comment {
65 | color: #ffffff;
66 | background-color: #569dff;
67 | letter-spacing: 0.8px;
68 | }
69 |
--------------------------------------------------------------------------------