├── README.md
├── assets
└── favicon.ico
├── constants
└── button-types.js
├── index.html
├── scripts
└── index.js
└── styles
└── styles.css
/README.md:
--------------------------------------------------------------------------------
1 | # Comment Box
2 |
3 | [Demo](https://sakshamgupta-comment-box.vercel.app)
4 |
5 | # Project description
6 |
7 | A vanilla javascript project to mimic Linkedin's comment functionality. Users can add a comment to a post, reply to a comment, edit or delete it.
8 |
9 | # Key Features
10 |
11 | - Add multiple comments on a post.
12 | - Reply to a comment.
13 | - Edit comments and replies.
14 | - Delete comments and replies.
15 |
16 | # Screenshots
17 |
18 |
19 |
--------------------------------------------------------------------------------
/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sakshamGupta09/comment-box/37d013fc19deae8d4d934a0b07a35626e797c0a3/assets/favicon.ico
--------------------------------------------------------------------------------
/constants/button-types.js:
--------------------------------------------------------------------------------
1 | export const BUTTON_TYPES = {
2 | postComment : "post-comment",
3 | postReply : "post-reply",
4 | replyToComment : "reply-to-comment",
5 | deleteComment : "delete-comment",
6 | editComment : "edit-comment",
7 | cancelChanges : "cancel-changes",
8 | saveChanges : "save-changes",
9 | };
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Comment box
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/scripts/index.js:
--------------------------------------------------------------------------------
1 | import { BUTTON_TYPES } from "../constants/button-types.js";
2 |
3 | // Dom elements
4 |
5 | const postContainerElement = document.getElementById("post-container");
6 |
7 | const commentsContainerElement = document.getElementById("comments");
8 |
9 | // Event listeners
10 |
11 | postContainerElement.addEventListener("click", postEventsHandler);
12 |
13 | // Functions
14 |
15 | function postEventsHandler(e) {
16 | // Post Comment button
17 | if (e.target.name === BUTTON_TYPES.postComment) {
18 | const comment = getCommentValue(e);
19 | comment && postComment(comment);
20 | return;
21 | }
22 | // Reply button
23 | if (e.target.name === BUTTON_TYPES.replyToComment) {
24 | replyClickHandler(e);
25 | }
26 | // Post Reply button
27 | if (e.target.name === BUTTON_TYPES.postReply) {
28 | const comment = getCommentValue(e);
29 | comment && postReplyToComment(e, comment);
30 | return;
31 | }
32 | // Delete comment
33 | if (e.target.name === BUTTON_TYPES.deleteComment) {
34 | deleteCommentHandler(e);
35 | }
36 | // Edit comment
37 | if (e.target.name === BUTTON_TYPES.editComment) {
38 | editCommentHandler(e);
39 | }
40 | // Cancel editing comment
41 | if (e.target.name === BUTTON_TYPES.cancelChanges) {
42 | cancelEditHandler(e);
43 | }
44 | // save updated comment
45 | if (e.target.name === BUTTON_TYPES.saveChanges) {
46 | saveUpdatedCommentHandler(e);
47 | }
48 | }
49 |
50 | function replyClickHandler(e) {
51 | const commentWrapper = e.target.closest(".comment__container");
52 |
53 | let replyContainer;
54 |
55 | if (commentWrapper.dataset.type === "comment") {
56 | replyContainer = commentWrapper.querySelector(".replies");
57 | } else {
58 | replyContainer = commentWrapper.closest(".replies");
59 | }
60 | if (replyContainer.querySelector(".comment__box")) {
61 | return;
62 | }
63 | replyContainer.appendChild(getCommentBoxNode());
64 | }
65 |
66 | function deleteCommentHandler(e) {
67 | e.target.closest("article.comment__container").remove();
68 | }
69 |
70 | function editCommentHandler(e) {
71 | const commentNode = e.target
72 | .closest(".comment__container")
73 | .querySelector(".comment");
74 |
75 | commentNode.appendChild(getEditCommentCTAElement());
76 | const comment = commentNode.querySelector("p.comment__content");
77 | comment.setAttribute("contentEditable", true);
78 | placeCursorAtEnd(comment);
79 | e.target.parentNode.remove();
80 | }
81 |
82 | function cancelEditHandler(e) {
83 | const parent = e.target.parentNode;
84 | parent.parentNode.after(getCommentCTAElement());
85 | parent.remove();
86 | }
87 |
88 | function saveUpdatedCommentHandler(e) {
89 | const parent = e.target.parentNode;
90 | parent.parentNode.after(getCommentCTAElement());
91 | parent.remove();
92 | }
93 |
94 | // Utilities
95 |
96 | function getCommentBoxNode() {
97 | const DIV = document.createElement("div");
98 | DIV.classList.add("comment__box", "mt-xs");
99 |
100 | const INPUT = document.createElement("input");
101 | INPUT.setAttribute("type", "text");
102 | INPUT.setAttribute("name", "comment-input");
103 | INPUT.setAttribute("placeholder", "Add a reply ...");
104 | INPUT.classList.add("comment__input");
105 |
106 | const BUTTON = document.createElement("button");
107 | BUTTON.setAttribute("name", "post-reply");
108 | BUTTON.classList.add("btn", "btn--primary");
109 | BUTTON.textContent = "Post";
110 |
111 | DIV.append(INPUT, BUTTON);
112 | return DIV;
113 | }
114 |
115 | function getCommentValue(e) {
116 | const inputElement = e.target.previousElementSibling;
117 | let value = "";
118 | if (
119 | inputElement?.nodeName === "INPUT" &&
120 | inputElement?.name === "comment-input"
121 | ) {
122 | value = inputElement.value;
123 | inputElement.value = "";
124 | }
125 | return value;
126 | }
127 |
128 | function getCommentNode(commentValue, isComment = true) {
129 | const ARTICLE = document.createElement("article");
130 | ARTICLE.classList.add("comment__container");
131 | ARTICLE.dataset.type = isComment ? "comment" : "reply";
132 |
133 | const DIV = document.createElement("div");
134 | DIV.classList.add("comment");
135 |
136 | const PARAGRAPH = document.createElement("p");
137 | PARAGRAPH.classList.add("comment__content");
138 | PARAGRAPH.textContent = commentValue;
139 |
140 | DIV.appendChild(PARAGRAPH);
141 |
142 | ARTICLE.append(DIV, getCommentCTAElement());
143 |
144 | if (isComment) {
145 | const DIV_REPLIES = document.createElement("div");
146 | DIV_REPLIES.classList.add("replies");
147 | ARTICLE.append(DIV_REPLIES);
148 | }
149 |
150 | return ARTICLE;
151 | }
152 |
153 | function getCommentCTAElement() {
154 | const DIV_CTA = document.createElement("div");
155 | DIV_CTA.classList.add("comment__cta");
156 |
157 | const BUTTON_REPLY = getButtonElement(
158 | BUTTON_TYPES.replyToComment,
159 | "Reply",
160 | "btn btn--secondary"
161 | );
162 |
163 | const BUTTON_EDIT = getButtonElement(
164 | BUTTON_TYPES.editComment,
165 | "Edit",
166 | "btn btn--secondary"
167 | );
168 |
169 | const BUTTON_DELETE = getButtonElement(
170 | BUTTON_TYPES.deleteComment,
171 | "Delete",
172 | "btn btn--secondary"
173 | );
174 |
175 | DIV_CTA.append(
176 | BUTTON_REPLY,
177 | getVerticalDividerElement(),
178 | BUTTON_EDIT,
179 | getVerticalDividerElement(),
180 | BUTTON_DELETE
181 | );
182 |
183 | return DIV_CTA;
184 | }
185 |
186 | function getEditCommentCTAElement() {
187 | const DIV_CTA = document.createElement("div");
188 | DIV_CTA.classList.add("comment__edit-cta");
189 |
190 | const BUTTON_SAVE = getButtonElement(
191 | BUTTON_TYPES.saveChanges,
192 | "Save Changes",
193 | "btn btn--primary"
194 | );
195 |
196 | const BUTTON_CANCEL = getButtonElement(
197 | BUTTON_TYPES.cancelChanges,
198 | "Cancel",
199 | "btn btn--tertiary"
200 | );
201 |
202 | DIV_CTA.append(BUTTON_SAVE, BUTTON_CANCEL);
203 |
204 | return DIV_CTA;
205 | }
206 |
207 | function getButtonElement(name, text, classes) {
208 | const BUTTON = document.createElement("button");
209 | BUTTON.textContent = text;
210 | BUTTON.className = classes;
211 | BUTTON.name = name;
212 | return BUTTON;
213 | }
214 |
215 | function postComment(comment) {
216 | const commentNode = getCommentNode(comment);
217 | commentsContainerElement.prepend(commentNode);
218 | }
219 |
220 | function postReplyToComment(e, comment) {
221 | const commentNode = getCommentNode(comment, false);
222 | const repliesContainer = e.target.closest(".replies");
223 | repliesContainer.appendChild(commentNode);
224 | e.target.parentNode.remove();
225 | }
226 |
227 | function getVerticalDividerElement() {
228 | const DIV = document.createElement("div");
229 | DIV.classList.add("vertical-divider");
230 | return DIV;
231 | }
232 |
233 | function placeCursorAtEnd(node) {
234 | const selection = window.getSelection();
235 | const range = document.createRange();
236 | selection.removeAllRanges();
237 | range.selectNodeContents(node);
238 | range.collapse(false);
239 | selection.addRange(range);
240 | node.focus();
241 | }
242 |
--------------------------------------------------------------------------------
/styles/styles.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bodybackgroundcolor: #f3f2ef;
3 | --text-sm: 0.875rem;
4 | --rounded-lg: 1.6rem;
5 | --rounded-md: 1.25rem;
6 | --rounded-sm: 0.8rem;
7 | --rounded-xs: 0.4rem;
8 | --gray-primary: rgba(0, 0, 0, 0.3);
9 | --gray-60: rgba(0, 0, 0, 0.6);
10 | --gray-80: rgba(0, 0, 0, 0.08);
11 | --blue-70: #0a66c2;
12 | --blue-80: #004182;
13 | --commentBgColor: #f2f2f2;
14 | }
15 |
16 | *,
17 | *::after,
18 | *::before {
19 | margin: 0;
20 | padding: 0;
21 | box-sizing: border-box;
22 | }
23 |
24 | html {
25 | font-size: 100%;
26 | }
27 |
28 | body {
29 | font-family: "Open Sans", sans-serif;
30 | font-weight: 600;
31 | font-size: 1.25rem;
32 | color: #000000e6;
33 | background-color: var(--bodybackgroundcolor);
34 | }
35 |
36 | .comment__input {
37 | display: block;
38 | width: 100%;
39 | border: 1px solid var(--gray-primary);
40 | border-radius: var(--rounded-md);
41 | padding: 0.725rem 1rem;
42 | outline: none;
43 | font-size: var(--text-sm);
44 | font-weight: inherit;
45 | color: inherit;
46 | }
47 |
48 | .comment__input:focus {
49 | box-shadow: inset 0 0 0 1px var(--gray-primary);
50 | }
51 |
52 | .btn {
53 | display: inline-block;
54 | border: none;
55 | cursor: pointer;
56 | font-size: var(--text-sm);
57 | font-weight: 700;
58 | text-align: center;
59 | transition-property: background-color, box-shadow, color;
60 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
61 | transition-duration: 167ms;
62 | }
63 |
64 | .btn--primary {
65 | background-color: var(--blue-70);
66 | color: #fff;
67 | min-height: 1.5rem;
68 | border-radius: var(--rounded-lg);
69 | padding: 0.2rem 0.8rem;
70 | margin-top: 0.75rem;
71 | }
72 |
73 | .btn--primary:hover {
74 | background-color: var(--blue-80);
75 | }
76 |
77 | .btn--secondary {
78 | background-color: transparent;
79 | border-radius: var(--rounded-xs);
80 | padding: 0 0.4rem;
81 | color: rgba(0, 0, 0, 0.6);
82 | }
83 |
84 | .btn--secondary:hover {
85 | background-color: var(--gray-80);
86 | }
87 |
88 | .btn--tertiary {
89 | color: var(--gray-60);
90 | min-height: 1.5rem;
91 | border-radius: var(--rounded-lg);
92 | padding: 0.2rem 0.8rem;
93 | box-shadow: 0 0 0 1px var(--gray-60);
94 | }
95 |
96 | .btn--tertiary:hover {
97 | background-color: var(--gray-80);
98 | box-shadow: inset 0 0 0 2px var(--gray-60);
99 | }
100 |
101 | :is(button, input) {
102 | font-family: inherit;
103 | }
104 |
105 | .comment__container {
106 | margin: 1.2rem 0;
107 | }
108 |
109 | .comment {
110 | background-color: var(--commentBgColor);
111 | padding: 0.8rem;
112 | border-radius: var(--rounded-sm);
113 | font-size: var(--text-sm);
114 | }
115 |
116 | .post__container {
117 | max-width: 540px;
118 | margin: 1rem auto;
119 | background-color: #fff;
120 | border-radius: var(--rounded-sm);
121 | box-shadow: 0px 0px 0px 1px var(--gray-80);
122 | padding: 1rem;
123 | }
124 |
125 | .mt-xs {
126 | margin-top: 0.75rem;
127 | }
128 |
129 | .comment__container .replies {
130 | margin-left: 2.75rem;
131 | }
132 |
133 | .comment__cta {
134 | display: flex;
135 | align-items: center;
136 | margin-top: 0.4rem;
137 | }
138 |
139 | .comment__cta > * {
140 | margin-right: 0.4rem;
141 | }
142 |
143 | .vertical-divider {
144 | height: 1rem;
145 | border-left: 1px solid var(--gray-primary);
146 | }
147 |
148 | .comment__edit-cta {
149 | margin-top: 1rem;
150 | }
151 |
152 | .comment__edit-cta > * {
153 | margin-right: 0.8rem;
154 | }
155 |
156 | .comment__content {
157 | outline: none;
158 | }
159 |
--------------------------------------------------------------------------------