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
|
// Post a comment
|
||||||
router.post('/api/comments', async (req, res) => {
|
router.post('/api/comments', async (req, res) => {
|
||||||
if (!req.session) return res.reply({ code: 401, body: JSON.stringify({ success: false, message: "Unauthorized" }) });
|
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;
|
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 = {
|
const data = {
|
||||||
user: query[0],
|
user: query[0],
|
||||||
f0cks,
|
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 id="main">
|
||||||
<div class="profile_head">
|
<div class="profile_head">
|
||||||
@if(user.avatar)
|
@if(user.avatar)
|
||||||
<div class="profile_head_avatar">
|
<div class="profile_head_avatar">
|
||||||
<img src="/t/{{ user.avatar }}.webp" style="display: grid;width: 55px" />
|
<img src="/t/{{ user.avatar }}.webp" style="display: grid;width: 55px" />
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div class="layersoffear">
|
<div class="layersoffear">
|
||||||
<div class="profile_head_username">
|
<div class="profile_head_username">
|
||||||
@@ -23,27 +23,41 @@
|
|||||||
@if(count.f0cks)
|
@if(count.f0cks)
|
||||||
<div class="posts">
|
<div class="posts">
|
||||||
@each(f0cks.items as item)
|
@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
|
@endeach
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
no f0cks given
|
no f0cks given
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="favs">
|
<div class="favs">
|
||||||
<div class="favs-header">
|
<div class="favs-header">
|
||||||
fav{{ count.favs == 1 ? '' : 's' }}: {{ count.favs }} <a href="{{ favs.link?.main }}">view all</a>
|
fav{{ count.favs == 1 ? '' : 's' }}: {{ count.favs }} <a href="{{ favs.link?.main }}">view all</a>
|
||||||
</div>
|
</div>
|
||||||
@if(count.favs)
|
@if(count.favs)
|
||||||
<div class="posts">
|
<div class="posts">
|
||||||
@each(favs.items as item)
|
@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
|
@endeach
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
no favorites
|
no favorites
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
@include(snippets/footer)
|
@include(snippets/footer)
|
||||||
Reference in New Issue
Block a user