Skip to content

Commit

Permalink
refactor: card comment
Browse files Browse the repository at this point in the history
  • Loading branch information
dewanakl committed Feb 17, 2025
1 parent 0e1afac commit e37b0b4
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 88 deletions.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<meta property="og:keywords" content="undangan, wedding, undangan digital, undangan online, wedding invitation, template undangan, template undangan pernikahan, undangan pernikahan, template undangan online, wedding invitation github, template website, template website undangan pernikahan">
<meta property="og:image" content="https://ulems.my.id/assets/images/bg.webp">
<meta property="og:image:secure_url" content="https://ulems.my.id/assets/images/bg.webp">
<meta property="og:image:type" content="image/png">
<meta property="og:image:type" content="image/webp">
<meta property="og:image:alt" content="Website Undangan Pernikahan Wahyu dan Riski Secara Online">
<meta property="og:image:width" content="980">
<meta property="og:image:height" content="980">
Expand Down
211 changes: 146 additions & 65 deletions js/app/component/card.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,52 @@ import { session } from '../../common/session.js';

export const card = (() => {

/**
* @type {ReturnType<typeof storage>|null}
*/
let user = null;

/**
* @type {ReturnType<typeof storage>|null}
*/
let owns = null;

/**
* @type {ReturnType<typeof storage>|null}
*/
let likes = null;

/**
* @type {ReturnType<typeof storage>|null}
*/
let config = null;

/**
* @type {ReturnType<typeof storage>|null}
*/
let tracker = null;

/**
* @type {ReturnType<typeof storage>|null}
*/
let showHide = null;

const maxCommentLength = 250;

const listsMarkDown = [
['*', `<strong class="text-theme-auto">$1</strong>`],
['_', `<em class="text-theme-auto">$1</em>`],
['~', `<del class="text-theme-auto">$1</del>`],
['```', `<code class="font-monospace text-theme-auto">$1</code>`]
];

/**
* @returns {string}
*/
const renderLoading = () => {
return `
<div class="bg-theme-auto shadow p-3 mx-0 mt-0 mb-3 rounded-4">
<div class="d-flex flex-wrap justify-content-between align-items-center placeholder-wave">
<div class="d-flex justify-content-between align-items-center placeholder-wave">
<span class="placeholder bg-secondary col-5 rounded-3 my-1"></span>
<span class="placeholder bg-secondary col-3 rounded-3 my-1"></span>
</div>
Expand All @@ -29,163 +62,211 @@ export const card = (() => {
</div>`;
};

const convertMarkdownToHTML = (input) => {
const lists = [
['*', `<strong class="text-theme-auto">$1</strong>`],
['_', `<em class="text-theme-auto">$1</em>`],
['~', `<del class="text-theme-auto">$1</del>`],
['```', `<code class="font-monospace text-theme-auto">$1</code>`]
];

lists.forEach((data) => {
/**
* @param {string} str
* @returns {string}
*/
const convertMarkdownToHTML = (str) => {
listsMarkDown.forEach((data) => {
const k = data[0];
const v = data[1];

input = input.replace(new RegExp(`\\${k}(?=\\S)(.*?)(?<!\\s)\\${k}`, 'gs'), v);
str = str.replace(new RegExp(`\\${k}(?=\\S)(.*?)(?<!\\s)\\${k}`, 'gs'), v);
});

return input;
return str;
};

const renderLike = (comment) => {
/**
* @param {ReturnType<typeof dto.getCommentResponse>} c
* @returns {string}
*/
const renderLike = (c) => {
return `
<button style="font-size: 0.8rem;" onclick="undangan.comment.like.love(this)" data-uuid="${comment.uuid}" class="btn btn-sm btn-outline-auto ms-auto rounded-3 p-0 shadow-sm d-flex justify-content-start align-items-center" data-offline-disabled="false">
<span class="my-0 mx-1" data-count-like="${comment.like.love}">${comment.like.love}</span>
<i class="me-1 ${likes.has(comment.uuid) ? 'fa-solid fa-heart text-danger' : 'fa-regular fa-heart'}"></i>
<button style="font-size: 0.8rem;" onclick="undangan.comment.like.love(this)" data-uuid="${c.uuid}" class="btn btn-sm btn-outline-auto ms-auto rounded-3 p-0 shadow-sm d-flex justify-content-start align-items-center" data-offline-disabled="false">
<span class="my-0 mx-1" data-count-like="${c.like.love}">${c.like.love}</span>
<i class="me-1 ${likes.has(c.uuid) ? 'fa-solid fa-heart text-danger' : 'fa-regular fa-heart'}"></i>
</button>`;
};

const renderAction = (comment) => {
let action = `<div class="d-flex flex-wrap justify-content-start align-items-center" data-button-action="${comment.uuid}">`;
/**
* @param {ReturnType<typeof dto.getCommentResponse>} c
* @returns {string}
*/
const renderAction = (c) => {
let action = `<div class="d-flex justify-content-start align-items-center" data-button-action="${c.uuid}">`;

if (config.get('can_reply') === true || config.get('can_reply') === undefined) {
action += `<button style="font-size: 0.8rem;" onclick="undangan.comment.reply(this)" data-uuid="${comment.uuid}" class="btn btn-sm btn-outline-auto rounded-4 py-0 me-1 shadow-sm" data-offline-disabled="false">Reply</button>`;
action += `<button style="font-size: 0.8rem;" onclick="undangan.comment.reply(this)" data-uuid="${c.uuid}" class="btn btn-sm btn-outline-auto rounded-4 py-0 me-1 shadow-sm" data-offline-disabled="false">Reply</button>`;
}

if (owns.has(comment.uuid) && (config.get('can_edit') === true || config.get('can_edit') === undefined)) {
action += `<button style="font-size: 0.8rem;" onclick="undangan.comment.edit(this)" data-uuid="${comment.uuid}" class="btn btn-sm btn-outline-auto rounded-4 py-0 me-1 shadow-sm" data-offline-disabled="false">Edit</button>`;
if (owns.has(c.uuid) && (config.get('can_edit') === true || config.get('can_edit') === undefined)) {
action += `<button style="font-size: 0.8rem;" onclick="undangan.comment.edit(this)" data-uuid="${c.uuid}" class="btn btn-sm btn-outline-auto rounded-4 py-0 me-1 shadow-sm" data-offline-disabled="false">Edit</button>`;
}

if (session.isAdmin()) {
action += `<button style="font-size: 0.8rem;" onclick="undangan.comment.remove(this)" data-uuid="${comment.uuid}" class="btn btn-sm btn-outline-auto rounded-4 py-0 me-1 shadow-sm" data-own="${comment.own}" data-offline-disabled="false">Delete</button>`;
} else if (owns.has(comment.uuid) && (config.get('can_delete') === true || config.get('can_delete') === undefined)) {
action += `<button style="font-size: 0.8rem;" onclick="undangan.comment.remove(this)" data-uuid="${comment.uuid}" class="btn btn-sm btn-outline-auto rounded-4 py-0 me-1 shadow-sm" data-offline-disabled="false">Delete</button>`;
action += `<button style="font-size: 0.8rem;" onclick="undangan.comment.remove(this)" data-uuid="${c.uuid}" class="btn btn-sm btn-outline-auto rounded-4 py-0 me-1 shadow-sm" data-own="${c.own}" data-offline-disabled="false">Delete</button>`;
} else if (owns.has(c.uuid) && (config.get('can_delete') === true || config.get('can_delete') === undefined)) {
action += `<button style="font-size: 0.8rem;" onclick="undangan.comment.remove(this)" data-uuid="${c.uuid}" class="btn btn-sm btn-outline-auto rounded-4 py-0 me-1 shadow-sm" data-offline-disabled="false">Delete</button>`;
}

action += '</div>';

return action;
};

const renderReadMore = (uuid, comments) => {
/**
* @param {string} uuid
* @param {string[]} uuids
* @returns {string}
*/
const renderReadMore = (uuid, uuids) => {
const hasId = showHide.get('show').includes(uuid);
return `<a class="text-theme-auto" style="font-size: 0.8rem;" onclick="undangan.comment.showOrHide(this)" data-uuid="${uuid}" data-uuids="${comments.join(',')}" data-show="${hasId ? 'true' : 'false'}" role="button" class="me-auto ms-1 py-0">${hasId ? 'Hide replies' : `Show replies (${comments.length})`}</a>`;
return `<a class="text-theme-auto" style="font-size: 0.8rem;" onclick="undangan.comment.showOrHide(this)" data-uuid="${uuid}" data-uuids="${uuids.join(',')}" data-show="${hasId ? 'true' : 'false'}" role="button" class="me-auto ms-1 py-0">${hasId ? 'Hide replies' : `Show replies (${uuids.length})`}</a>`;
};

const renderButton = (comment) => {
/**
* @param {ReturnType<typeof dto.getCommentResponse>} c
* @returns {string}
*/
const renderButton = (c) => {
return `
<div class="d-flex flex-wrap justify-content-between align-items-center" id="button-${comment.uuid}">
${renderAction(comment)}
${comment.comments.length > 0 ? renderReadMore(comment.uuid, comment.comments.map((c) => c.uuid)) : ''}
${renderLike(comment)}
<div class="d-flex justify-content-between align-items-center" id="button-${c.uuid}">
${renderAction(c)}
${c.comments.length > 0 ? renderReadMore(c.uuid, c.comments.map((i) => i.uuid)) : ''}
${renderLike(c)}
</div>`;
};

const renderTracker = (comment) => {
if (comment.ip === undefined || comment.user_agent === undefined || comment.is_admin) {
/**
* @param {ReturnType<typeof dto.getCommentResponse>} c
* @returns {string}
*/
const renderTracker = (c) => {
if (c.ip === undefined || c.user_agent === undefined || c.is_admin) {
return '';
}

return `
<div class="mb-1 mt-3">
<p class="text-theme-auto mb-1 mx-0 mt-0 p-0" style="font-size: 0.7rem;" id="ip-${comment.uuid}"><i class="fa-solid fa-location-dot me-1"></i>${util.escapeHtml(comment.ip)} ${tracker.has(comment.ip) ? `<strong>${tracker.get(comment.ip)}</strong>` : `<span class="mb-1 placeholder col-2 rounded-3"></span>`}</p>
<p class="text-theme-auto m-0 p-0" style="font-size: 0.7rem;"><i class="fa-solid fa-mobile-screen-button me-1"></i>${util.parseUserAgent(util.escapeHtml(comment.user_agent))}</p>
<p class="text-theme-auto mb-1 mx-0 mt-0 p-0" style="font-size: 0.7rem;" id="ip-${c.uuid}"><i class="fa-solid fa-location-dot me-1"></i>${util.escapeHtml(c.ip)} ${tracker.has(c.ip) ? `<strong>${tracker.get(c.ip)}</strong>` : `<span class="mb-1 placeholder col-2 rounded-3"></span>`}</p>
<p class="text-theme-auto m-0 p-0" style="font-size: 0.7rem;"><i class="fa-solid fa-mobile-screen-button me-1"></i>${util.parseUserAgent(util.escapeHtml(c.user_agent))}</p>
</div>`;
};

const renderHeader = (comment, is_parent) => {
/**
* @param {ReturnType<typeof dto.getCommentResponse>} c
* @param {boolean} is_parent
* @returns {string}
*/
const renderHeader = (c, is_parent) => {
if (is_parent) {
return `class="bg-theme-auto shadow p-3 mx-0 mt-0 mb-3 rounded-4" data-parent="true"`;
}

return `class="${!showHide.get('hidden').find((item) => item.uuid === comment.uuid)['show'] ? 'd-none' : ''} overflow-x-scroll mw-100 border-start bg-theme-auto py-2 ps-2 pe-0 my-2 ms-2 me-0"`;
return `class="${!showHide.get('hidden').find((i) => i.uuid === c.uuid)['show'] ? 'd-none' : ''} overflow-x-scroll mw-100 border-start bg-theme-auto py-2 ps-2 pe-0 my-2 ms-2 me-0"`;
};

const renderTitle = (comment, is_parent) => {
if (comment.is_admin) {
/**
* @param {ReturnType<typeof dto.getCommentResponse>} c
* @param {boolean} is_parent
* @returns {string}
*/
const renderTitle = (c, is_parent) => {
if (c.is_admin) {
return `<strong class="me-1">${util.escapeHtml(user.get('name') ?? config.get('name'))}</strong><i class="fa-solid fa-certificate text-primary"></i>`;
}

if (is_parent) {
return `<strong class="me-1">${util.escapeHtml(comment.name)}</strong><i id="badge-${comment.uuid}" class="fa-solid ${comment.presence ? 'fa-circle-check text-success' : 'fa-circle-xmark text-danger'}"></i>`;
return `<strong class="me-1">${util.escapeHtml(c.name)}</strong><i id="badge-${c.uuid}" class="fa-solid ${c.presence ? 'fa-circle-check text-success' : 'fa-circle-xmark text-danger'}"></i>`;
}

return `<strong>${util.escapeHtml(comment.name)}</strong>`;
return `<strong>${util.escapeHtml(c.name)}</strong>`;
};

const renderBody = (comment, is_parent) => {
const original = convertMarkdownToHTML(util.escapeHtml(comment.comment));
/**
* @param {ReturnType<typeof dto.getCommentResponse>} c
* @param {boolean} is_parent
* @returns {string}
*/
const renderBody = (c, is_parent) => {
const original = convertMarkdownToHTML(util.escapeHtml(c.comment));
const moreThanMaxLength = original.length > maxCommentLength;

return `
<div class="d-flex flex-wrap justify-content-between align-items-center">
<p class="text-theme-auto text-truncate m-0 p-0" style="font-size: 0.95rem;">${renderTitle(comment, is_parent)}</p>
<small class="text-theme-auto m-0 p-0" style="font-size: 0.75rem;">${comment.created_at}</small>
<div class="d-flex justify-content-between align-items-center">
<p class="text-theme-auto text-truncate m-0 p-0" style="font-size: 0.95rem;">${renderTitle(c, is_parent)}</p>
<small class="text-theme-auto m-0 p-0" style="font-size: 0.75rem;">${c.created_at}</small>
</div>
<hr class="my-1">
<p class="text-theme-auto my-1 mx-0 p-0" style="white-space: pre-wrap !important; font-size: 0.95rem;" ${moreThanMaxLength ? `data-comment="${util.base64Encode(original)}"` : ''} id="content-${comment.uuid}">${moreThanMaxLength ? (original.slice(0, maxCommentLength) + '...') : original}</p>
${moreThanMaxLength ? `<p class="mb-2 mt-0 mx-0 p-0"><a class="text-theme-auto" role="button" style="font-size: 0.85rem; display: block;" data-show="false" onclick="undangan.comment.showMore(this, '${comment.uuid}')">Selengkapnya</a></p>` : ''}`;
<p class="text-theme-auto my-1 mx-0 p-0" style="white-space: pre-wrap !important; font-size: 0.95rem;" ${moreThanMaxLength ? `data-comment="${util.base64Encode(original)}"` : ''} id="content-${c.uuid}">${moreThanMaxLength ? (original.slice(0, maxCommentLength) + '...') : original}</p>
${moreThanMaxLength ? `<p class="mb-2 mt-0 mx-0 p-0"><a class="text-theme-auto" role="button" style="font-size: 0.85rem; display: block;" data-show="false" onclick="undangan.comment.showMore(this, '${c.uuid}')">Selengkapnya</a></p>` : ''}`;
};

const renderContent = (comment, is_parent) => {
/**
* @param {ReturnType<typeof dto.getCommentResponse>} c
* @param {boolean} is_parent
* @returns {string}
*/
const renderContent = (c, is_parent) => {
return `
<div ${renderHeader(comment, is_parent)} id="${comment.uuid}" style="overflow-wrap: break-word !important;">
<div id="body-content-${comment.uuid}" data-tapTime="0" data-liked="false" tabindex="0">
${renderBody(comment, is_parent)}
<div ${renderHeader(c, is_parent)} id="${c.uuid}" style="overflow-wrap: break-word !important;">
<div id="body-content-${c.uuid}" data-tapTime="0" data-liked="false" tabindex="0">
${renderBody(c, is_parent)}
</div>
${renderTracker(comment)}
${renderButton(comment)}
<div id="reply-content-${comment.uuid}">${comment.comments.map((c) => renderInnerContent(c)).join('')}</div>
${renderTracker(c)}
${renderButton(c)}
<div id="reply-content-${c.uuid}">${c.comments.map((i) => renderContent(i, false)).join('')}</div>
</div>`;
};

const renderInnerContent = (comment) => renderContent(comment, false);

/**
* @param {string} id
* @returns {HTMLDivElement}
*/
const renderReply = (id) => {
const inner = document.createElement('div');
inner.classList.add('my-2');
inner.id = `inner-${id}`;
inner.innerHTML = `
<label for="form-inner-${id}" class="form-label my-1" style="font-size: 0.95rem;"><i class="fa-solid fa-reply me-2"></i>Reply</label>
<textarea class="form-control shadow-sm rounded-4 mb-2" id="form-inner-${id}" minlength="1" maxlength="1000" placeholder="Type reply comment" rows="3" data-offline-disabled="false"></textarea>
<div class="d-flex flex-wrap justify-content-end align-items-center mb-0">
<div class="d-flex justify-content-end align-items-center mb-0">
<button style="font-size: 0.8rem;" onclick="undangan.comment.cancel('${id}')" class="btn btn-sm btn-outline-auto rounded-4 py-0 me-1" data-offline-disabled="false">Cancel</button>
<button style="font-size: 0.8rem;" onclick="undangan.comment.send(this)" data-uuid="${id}" class="btn btn-sm btn-outline-auto rounded-4 py-0" data-offline-disabled="false">Send</button>
</div>`;

return inner;
};

const renderEdit = (id, presence) => {
/**
* @param {string} id
* @param {boolean} presence
* @param {boolean} is_parent
* @returns {HTMLDivElement}
*/
const renderEdit = (id, presence, is_parent) => {
const inner = document.createElement('div');
inner.classList.add('my-2');
inner.id = `inner-${id}`;
inner.innerHTML = `
<label for="form-inner-${id}" class="form-label my-1" style="font-size: 0.95rem;"><i class="fa-solid fa-pen me-2"></i>Edit</label>
${document.getElementById(id).getAttribute('data-parent') === 'true' && !session.isAdmin() ? `
${!is_parent ? '' : `
<select class="form-select shadow-sm mb-2 rounded-4" id="form-inner-presence-${id}" data-offline-disabled="false">
<option value="1" ${presence ? 'selected' : ''}>Datang</option>
<option value="2" ${presence ? '' : 'selected'}>Berhalangan</option>
</select>` : ''}
</select>`}
<textarea class="form-control shadow-sm rounded-4 mb-2" id="form-inner-${id}" minlength="1" maxlength="1000" placeholder="Type update comment" rows="3" data-offline-disabled="false"></textarea>
<div class="d-flex flex-wrap justify-content-end align-items-center mb-0">
<div class="d-flex justify-content-end align-items-center mb-0">
<button style="font-size: 0.8rem;" onclick="undangan.comment.cancel('${id}')" class="btn btn-sm btn-outline-auto rounded-4 py-0 me-1" data-offline-disabled="false">Cancel</button>
<button style="font-size: 0.8rem;" onclick="undangan.comment.update(this)" data-uuid="${id}" class="btn btn-sm btn-outline-auto rounded-4 py-0" data-offline-disabled="false">Update</button>
</div>`;

return inner;
};

/**
* @returns {void}
*/
const init = () => {
user = storage('user');
owns = storage('owns');
Expand All @@ -201,8 +282,8 @@ export const card = (() => {
renderReply,
renderLoading,
renderReadMore,
renderInnerContent,
renderContent: (comment) => renderContent(comment, true),
renderInnerContent: (c) => renderContent(c, false),
renderContent: (c) => renderContent(c, true),
convertMarkdownToHTML,
maxCommentLength,
};
Expand Down
Loading

0 comments on commit e37b0b4

Please sign in to comment.