adding comment overview page on userprofiles

This commit is contained in:
2026-01-26 22:54:59 +01:00
parent 002dfeded3
commit dfc8ebba89
4 changed files with 219 additions and 9 deletions

View File

@@ -58,6 +58,87 @@ export default (router, tpl) => {
}
});
// Browse User Comments
router.get(/\/user\/(?<user>[^\/]+)\/comments/, async (req, res) => {
const user = decodeURIComponent(req.params.user);
try {
// Check if user exists and get ID + avatar
const u = await db`
SELECT "user".id, "user".user, user_options.avatar
FROM "user"
LEFT JOIN user_options ON "user".id = user_options.user_id
WHERE "user".user ILIKE ${user}
`;
if (!u.length) {
return res.reply({ code: 404, body: "User not found" });
}
const userId = u[0].id;
const sort = req.url.qs?.sort || 'new';
const page = +(req.url.qs?.page || 1);
const limit = 20;
const offset = (page - 1) * limit;
const isJson = req.url.qs?.json === 'true';
const comments = await db`
SELECT c.*, i.mime, i.id as item_id
FROM comments c
LEFT JOIN items i ON c.item_id = i.id
WHERE c.user_id = ${userId} AND c.is_deleted = false
ORDER BY c.created_at DESC
LIMIT ${limit} OFFSET ${offset}
`;
const emojis = await db`SELECT name, url FROM custom_emojis`;
const emojiMap = new Map();
emojis.forEach(e => emojiMap.set(e.name, e.url));
const escapeHtml = (unsafe) => {
return (unsafe || '')
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
};
const processedComments = comments.map(c => {
let safeContent = escapeHtml(c.content);
// Replace :emoji: with img
safeContent = safeContent.replace(/:([a-z0-9_]+):/g, (match, name) => {
if (emojiMap.has(name)) {
return `<img src="${emojiMap.get(name)}" style="height:20px;vertical-align:middle;" alt="${name}">`;
}
return match;
});
return {
...c,
content: safeContent
};
});
if (isJson) {
return res.reply({
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ success: true, comments: processedComments })
});
}
const data = {
user: u[0],
comments: processedComments,
hidePagination: true,
tmp: null // for header/footer
};
return res.reply({ body: tpl.render('comments_user', data, req) });
} catch (e) {
console.error(e);
return res.reply({ code: 500, body: "Error" });
}
});
// Post a comment
router.post('/api/comments', async (req, res) => {
if (!req.session) return res.reply({ code: 401, body: JSON.stringify({ success: false, message: "Unauthorized" }) });

View File

@@ -69,6 +69,13 @@ export default (router, tpl) => {
count.favs = 0;
}
try {
const comms = await db`select count(*) from comments where user_id = ${query[0].user_id}`;
count.comments = +comms[0].count;
} catch (e) {
count.comments = 0;
}
const data = {
user: query[0],
f0cks,

108
views/comments_user.html Normal file
View File

@@ -0,0 +1,108 @@
@include(snippets/header)
<div id="main">
<div class="profile_head">
@if(user.avatar)
<div class="profile_head_avatar">
<img src="/t/{{ user.avatar }}.webp" style="display: grid;width: 55px" />
</div>
@endif
<div class="layersoffear">
<div class="profile_head_username">
<span>{{ user.user }}'s Comments</span>
</div>
<div class="profile_head_user_stats">
<a href="/user/{{ user.user }}">Back to Profile</a>
</div>
</div>
</div>
<div class="user_content_wrapper" style="display: block;">
<div class="comments-list-page" style="max-width: 800px; margin: 0 auto;">
@each(comments as c)
<div class="user-comment-row"
style="display: flex; gap: 10px; margin-bottom: 10px; background: rgba(255,255,255,0.05); padding: 10px; border-radius: 4px;">
<div class="comment-thumbnail" style="flex-shrink: 0;">
<a href="/{{ c.item_id }}#c{{ c.id }}">
<img src="/t/{{ c.item_id }}.webp"
style="width: 80px; height: 80px; object-fit: cover; border-radius: 4px;">
</a>
</div>
<div class="comment-preview" style="flex-grow: 1;">
<div style="font-size: 0.8em; color: #888; margin-bottom: 5px;">
On <a href="/{{ c.item_id }}">Item #{{ c.item_id }}</a> - {{ c.created_at }}
</div>
<div class="comment-text">
{{ c.content }}
</div>
</div>
</div>
@endeach
</div>
</div>
</div>
@include(snippets/footer)
<script>
let page = 1;
let loading = false;
let finished = false;
const user = "{{ user.user }}";
const container = document.querySelector('.comments-list-page');
window.addEventListener('scroll', () => {
if (loading || finished) return;
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 500) {
loadMore();
}
});
async function loadMore() {
loading = true;
page++;
// Show loading indicator?
const loader = document.createElement('div');
loader.className = 'loader-placeholder';
loader.innerText = 'Loading...';
loader.style.textAlign = 'center';
loader.style.padding = '10px';
container.appendChild(loader);
try {
const res = await fetch('/user/' + encodeURIComponent(user) + '/comments?page=' + page + '&json=true');
const json = await res.json();
loader.remove();
if (json.success && json.comments.length > 0) {
json.comments.forEach(c => {
const div = document.createElement('div');
div.className = 'user-comment-row';
div.style.cssText = 'display: flex; gap: 10px; margin-bottom: 10px; background: rgba(255,255,255,0.05); padding: 10px; border-radius: 4px;';
let html = '<div class="comment-thumbnail" style="flex-shrink: 0;">';
html += '<a href="/' + c.item_id + '#c' + c.id + '">';
html += '<img src="/t/' + c.item_id + '.webp" style="width: 80px; height: 80px; object-fit: cover; border-radius: 4px;">';
html += '</a></div>';
html += '<div class="comment-preview" style="flex-grow: 1;">';
html += '<div style="font-size: 0.8em; color: #888; margin-bottom: 5px;">';
html += 'On <a href="/' + c.item_id + '">Item #' + c.item_id + '</a> - ' + new Date(c.created_at).toLocaleString();
html += '</div>';
html += '<div class="comment-text">' + c.content + '</div>';
html += '</div>';
div.innerHTML = html;
container.appendChild(div);
});
} else {
finished = true;
}
} catch (e) {
console.error(e);
loader.remove();
} finally {
loading = false;
}
}
</script>

View File

@@ -2,9 +2,9 @@
<div id="main">
<div class="profile_head">
@if(user.avatar)
<div class="profile_head_avatar">
<img src="/t/{{ user.avatar }}.webp" style="display: grid;width: 55px" />
</div>
<div class="profile_head_avatar">
<img src="/t/{{ user.avatar }}.webp" style="display: grid;width: 55px" />
</div>
@endif
<div class="layersoffear">
<div class="profile_head_username">
@@ -23,27 +23,41 @@
@if(count.f0cks)
<div class="posts">
@each(f0cks.items as item)
<a href="{{ f0cks.link.main }}{{ item.id }}" data-mime="{{ item.mime }}" data-mode="{{ item.tag_id ? ['','sfw','nsfw'][item.tag_id] : 'null' }}" style="background-image: url('/t/{{ item.id }}.webp')"><p></p></a>
<a href="{{ f0cks.link.main }}{{ item.id }}" data-mime="{{ item.mime }}"
data-mode="{{ item.tag_id ? ['','sfw','nsfw'][item.tag_id] : 'null' }}"
style="background-image: url('/t/{{ item.id }}.webp')">
<p></p>
</a>
@endeach
</div>
@else
no f0cks given
no f0cks given
@endif
</div>
<div class="favs">
<div class="favs-header">
fav{{ count.favs == 1 ? '' : 's' }}: {{ count.favs }} <a href="{{ favs.link?.main }}">view all</a>
</div>
@if(count.favs)
@if(count.favs)
<div class="posts">
@each(favs.items as item)
<a href="{{ favs.link.main }}{{ item.id }}" data-mime="{{ item.mime }}" data-mode="{{ item.tag_id ? ['','sfw','nsfw'][item.tag_id] : 'null' }}" style="background-image: url('/t/{{ item.id }}.webp')"><p></p></a>
<a href="{{ favs.link.main }}{{ item.id }}" data-mime="{{ item.mime }}"
data-mode="{{ item.tag_id ? ['','sfw','nsfw'][item.tag_id] : 'null' }}"
style="background-image: url('/t/{{ item.id }}.webp')">
<p></p>
</a>
@endeach
</div>
@else
no favorites
no favorites
@endif
</div>
<div class="comments-section">
<div class="comments-header">
comment{{ count.comments == 1 ? '' : 's' }}: {{ count.comments }} <a href="/user/{{ user.user }}/comments">view
all</a>
</div>
</div>
</div>
</div>
@include(snippets/footer)