Skip to content

Commit

Permalink
JS: Comment box revamp
Browse files Browse the repository at this point in the history
  • Loading branch information
sadanandpai committed Sep 27, 2024
1 parent 6c08205 commit bce34b9
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 159 deletions.
32 changes: 32 additions & 0 deletions apps/javascript/src/challenges/comment-box/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const initialCommentState = {
username: 'John Doe',
time: 1727458711867,
text: 'Welcome to comment box challenge!',
counter: 1,
parentCounter: 0,
comments: {
'0': {
username: 'User 1',
text: 'You can reply to the comments or edit the existing comments',
counter: 1,
parentCounter: 0,
comments: {
'0': {
username: 'User 2',
text: 'You can reply nested or delete any comment. Refresh & see the changes persist',
counter: 0,
parentCounter: 0,
comments: {},
},
},
},
},
};

const storage = localStorage.getItem('state');
export const initialState = storage
? JSON.parse(storage)
: {
initialCommentState,
comments: { ...initialCommentState.comments },
};
52 changes: 52 additions & 0 deletions apps/javascript/src/challenges/comment-box/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const commentTemplate = document.querySelector('#comment-template');

export function cloneAddCommentTemplate({ username, text }) {
const commentEl = commentTemplate.content.cloneNode(true);
commentEl.querySelector('.username').textContent = username;
commentEl.querySelector('.comment-text').innerText = text;
commentEl.querySelector('.user-info').classList.remove('hide');
setDefaultControls(commentEl);

return commentEl;
}

export function cloneNewCommentTemplate() {
const commentEl = commentTemplate.content.cloneNode(true);
commentEl.querySelector('.username-input').classList.remove('hide');
setNewCommentControls(commentEl);
return commentEl;
}

export function setDefaultControls(commentEl) {
const commentTextEl = commentEl.querySelector('.comment-text');
commentTextEl.contentEditable = false;
commentTextEl.classList.remove('editable');
commentEl.querySelector('.reply').classList.remove('hide');
commentEl.querySelector('.delete').classList.remove('hide');
commentEl.querySelector('.edit').classList.remove('hide');
commentEl.querySelector('.submit').classList.add('hide');
commentEl.querySelector('.cancel').classList.add('hide');
}

export function setEditControls(commentEl) {
const commentTextEl = commentEl.querySelector('.comment-text');
commentTextEl.contentEditable = true;
commentTextEl.classList.add('editable');
commentTextEl.focus();

commentEl.querySelector('.reply').classList.add('hide');
commentEl.querySelector('.delete').classList.add('hide');
commentEl.querySelector('.edit').classList.add('hide');
commentEl.querySelector('.submit').classList.remove('hide');
commentEl.querySelector('.cancel').classList.remove('hide');
}

export function setNewCommentControls(commentEl) {
commentEl.querySelector('.comment-text').contentEditable = true;
commentEl.querySelector('.comment-text').classList.add('editable');
commentEl.querySelector('.comment-text').focus();

commentEl.querySelector('.username-input').classList.remove('hide');
commentEl.querySelector('.cancel').classList.remove('hide');
commentEl.querySelector('.submit').classList.remove('hide');
}
47 changes: 43 additions & 4 deletions apps/javascript/src/challenges/comment-box/index.html
Original file line number Diff line number Diff line change
@@ -1,17 +1,56 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="../../logo.svg" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="index.js" type="module"></script>
<script src="../../helpers/header.js" type="module"></script>
<link rel="stylesheet" href="style.css" />
<script src="index.js" type="module"></script>
<link rel="stylesheet" href="style.scss" />
</head>
<body>
<div class="container">
<div id="commentContainer"></div>
<div id="commentContainer">
<div class="reset-wrapper">
<button class="btn btn-primary" id="reset">Reset</button>
</div>
<div class="sub-comments"></div>
</div>
</div>

<template id="comment-template">
<div class="comment-wrapper">
<div class="main-comment">
<img
src="https://icon-library.com/images/anonymous-icon/anonymous-icon-28.jpg"
alt="profile"
class="profile-pic"
loading="lazy"
/>

<div class="content">
<div class="header">
<input type="text" class="username-input hide" placeholder="User name" />
<div class="user-info hide">
<h3 class="username"></h3>
</div>
</div>

<p class="comment-text"></p>

<div class="footer">
<button class="reply hide">Reply</button>
<button class="edit hide">Edit</button>
<button class="cancel hide">Cancel</button>
<button class="submit hide">Post</button>
<button class="delete hide">Delete</button>
</div>
</div>
</div>

<div class="sub-comments"></div>
</div>
</template>
</body>
</html>
185 changes: 97 additions & 88 deletions apps/javascript/src/challenges/comment-box/index.js
Original file line number Diff line number Diff line change
@@ -1,109 +1,118 @@
import { initialCommentState, initialState } from './config';
import {
cloneAddCommentTemplate,
cloneNewCommentTemplate,
setDefaultControls,
setEditControls,
} from './helpers';

const commentContainer = document.querySelector('#commentContainer');
const commentTemplate = document.querySelector('#comment-template');
const resetButton = document.querySelector('#reset');
let rootState;

function addComment(parentEl, commentState, parentState) {
parentEl.querySelector(':scope > .sub-comments').appendChild(
cloneAddCommentTemplate({
username: commentState.username,
text: commentState.text,
})
);
const commentEl = parentEl.querySelector(':scope > .sub-comments > .comment-wrapper:last-child');

commentEl.querySelector('.profile-pic').src =
`https://i.pravatar.cc/32?u=${commentState.username}`;

commentEl.querySelector('.reply').addEventListener('click', () => {
if (!commentEl.querySelector(':scope > .sub-comments > .new-comment')) {
newComment(commentEl, commentState);
}
});

const createElement = (elementType = 'div', properties, ...children) => {
const element = document.createElement(elementType);
for (let key in properties) {
element[key] = properties[key];
if (!parentState) {
return commentEl;
}

children.forEach(child => element.appendChild(child));
return element;
};

const createComment = (name, text, settings) => {
text = text.replaceAll('\n', '<br>');
const p1 = createElement('p', { textContent: name, className: 'text-bold name' });
const p2 = createElement('p', { innerHTML: text, className: 'comment-text' });

const buttons = [];
buttons.push(createElement('button', { textContent: 'Reply', className: 'btn btn-primary small reply' }));
if (!settings?.hasNoEdit)
buttons.push(createElement('button', { textContent: 'Edit', className: 'btn btn-primary small edit' }));
if (!settings?.hasNoDelete)
buttons.push(createElement('button', { textContent: 'Delete', className: 'btn btn-primary small delete' }));

const btnHolder = createElement('div', { className: 'btn-holder' }, ...buttons);
const mainComment = createElement('div', { className: 'main-comment' }, p1, p2, btnHolder);
const subComments = createElement('div', { className: 'sub-comments' });

return createElement('div', { className: 'comment' }, mainComment, subComments);
};

const createCommentInput = () => {
const nameInput = createElement('input', { placeholder: 'Your name', className: 'text-bold name ' });
const commentInput = createElement('textarea', {
placeholder: 'comment',
className: 'comment-text',
rows: 2,
cols: 30,
commentEl.querySelector('.delete').addEventListener('click', () => {
commentEl.remove();
delete parentState.comments[commentState.parentCounter];
});
const postBtn = createElement('button', { textContent: 'Post', className: 'btn btn-primary small post' });
const cancelBtn = createElement('button', { textContent: 'Cancel', className: 'btn btn-primary small cancel' });
const btnHolder = createElement('div', { className: 'btn-holder' }, postBtn, cancelBtn);

return createElement('div', { className: 'comment' }, nameInput, commentInput, btnHolder);
};

const toggleNeighbours = target => {
target.nextElementSibling.disabled = !target.nextElementSibling.disabled;
target.previousElementSibling.disabled = !target.previousElementSibling.disabled;
};

const comment = createComment('Sadanand', 'Hello, world', { hasNoDelete: true, hasNoEdit: true });
commentContainer.appendChild(comment);

let isCommentOn = false;
commentContainer.addEventListener('click', e => {
const target = e.target;
if (target.tagName.toLowerCase() === 'button') {
if (target.classList.contains('reply') && !isCommentOn) {
target.closest('.main-comment').nextElementSibling.appendChild(createCommentInput());
isCommentOn = true;
return;
}

if (target.classList.contains('edit')) {
target.textContent = 'Save';
target.className = 'btn btn-primary small save';
toggleNeighbours(target);
target.closest('.main-comment').children[1].contentEditable = true;
commentEl.querySelector('.edit').addEventListener('click', () => {
setEditControls(commentEl);
});

commentEl.querySelector('.cancel').addEventListener('click', () => {
commentEl.querySelector('.comment-text').innerText = commentState.text;
setDefaultControls(commentEl);
});

commentEl.querySelector('.submit').addEventListener('click', () => {
const innerText = commentEl.querySelector('.comment-text').innerText;
if (!innerText) {
return;
}

if (target.classList.contains('save')) {
const commentText = target.closest('.main-comment').children[1];
commentState.text = innerText;
commentEl.querySelector('.comment-text').innerText = innerText;
setDefaultControls(commentEl);
});

if (!commentText.textContent) return;
target.textContent = 'Edit';
target.className = 'btn btn-primary small edit';
return commentEl;
}

commentText.contentEditable = false;
toggleNeighbours(target);
return;
}
function newComment(parentEl, parentState) {
parentEl.querySelector(':scope > .sub-comments').appendChild(cloneNewCommentTemplate());
const commentEl = parentEl.querySelector(':scope > .sub-comments > .comment-wrapper:last-child');
commentEl.classList.add('new-comment');

if (target.classList.contains('delete')) {
target.closest('.comment').remove();
return;
}
commentEl.querySelector('.cancel').addEventListener('click', () => {
commentEl.remove();
});

commentEl.querySelector('.submit').addEventListener('click', () => {
const username = commentEl.querySelector('.username-input').value;
const text = commentEl.querySelector('.comment-text').innerText;

if (target.classList.contains('cancel')) {
target.closest('.comment').remove();
isCommentOn = false;
if (!username || !text) {
return;
}

if (target.classList.contains('post')) {
const comment = target.closest('.comment');
const name = comment.children[0].value;
const text = comment.children[1].value;
const commentState = {
username: username,
text: text,
counter: 0,
parentCounter: parentState.counter,
comments: {},
};

addComment(parentEl, commentState, parentState);
parentState.comments[parentState.counter++] = commentState;
commentEl.remove();
});
}

if (!name || !text) return;
function init(parentEl, parentState) {
for (const commentState of Object.values(parentState.comments)) {
const commentEl = addComment(parentEl, commentState, parentState);

target.closest('.sub-comments').appendChild(createComment(name, text));
comment.remove();
isCommentOn = false;
return;
if (commentState.comments) {
init(commentEl, commentState);
}
}
}

function loadState(state = initialCommentState) {
commentContainer.querySelector('.sub-comments').innerHTML = '';
rootState = state;
const rootEl = addComment(commentContainer, rootState);
init(rootEl, rootState);
}

resetButton.addEventListener('click', () => loadState());

window.addEventListener('beforeunload', () => {
localStorage.setItem('state', JSON.stringify(rootState));
});

loadState(initialState);
35 changes: 0 additions & 35 deletions apps/javascript/src/challenges/comment-box/style.css

This file was deleted.

Loading

0 comments on commit bce34b9

Please sign in to comment.