adding comment overview page on userprofiles
This commit is contained in:
@@ -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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
};
|
||||
|
||||
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" }) });
|
||||
|
||||
@@ -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
108
views/comments_user.html
Normal 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>
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user