├── 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 | Screenshot 2023-02-03 at 7 00 36 AM 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 |
31 |
32 |
33 | 34 | 35 |
36 |
37 |
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 | --------------------------------------------------------------------------------