feat: implement user comment subscriptions, custom emojis, pinned comments, and thread locking.
This commit is contained in:
57
src/inc/routes/subscriptions.mjs
Normal file
57
src/inc/routes/subscriptions.mjs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import db from "../sql.mjs";
|
||||||
|
|
||||||
|
export default (router, tpl) => {
|
||||||
|
|
||||||
|
// Subscriptions Overview
|
||||||
|
router.get('/subscriptions', async (req, res) => {
|
||||||
|
if (!req.session) return res.redirect('/login');
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[DEBUG SUB] Fetching subscriptions for user', req.session.id);
|
||||||
|
const subs = await db`
|
||||||
|
SELECT
|
||||||
|
s.created_at as sub_date,
|
||||||
|
i.id, i.dest, i.mime, i.username as uploader_name
|
||||||
|
FROM comment_subscriptions s
|
||||||
|
JOIN items i ON s.item_id = i.id
|
||||||
|
WHERE s.user_id = ${req.session.id}
|
||||||
|
ORDER BY s.created_at DESC
|
||||||
|
`;
|
||||||
|
console.log('[DEBUG SUB] Found', subs.length, 'subscriptions');
|
||||||
|
|
||||||
|
const items = subs.map(i => ({
|
||||||
|
id: i.id,
|
||||||
|
user: i.uploader_name || 'System',
|
||||||
|
sub_created: new Date(i.sub_date).toLocaleString(),
|
||||||
|
thumb: `/t/${i.id}.webp`
|
||||||
|
}));
|
||||||
|
|
||||||
|
return res.reply({
|
||||||
|
body: tpl.render('subscriptions', { items }, req)
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[DEBUG SUB ERROR]', e);
|
||||||
|
return res.reply({ code: 500, body: 'Database Error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unsubscribe
|
||||||
|
router.post(/\/api\/subscriptions\/(?<itemid>\d+)\/delete/, async (req, res) => {
|
||||||
|
if (!req.session) return res.reply({ code: 401, body: JSON.stringify({ success: false }) });
|
||||||
|
|
||||||
|
const itemId = req.params.itemid;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db`DELETE FROM comment_subscriptions WHERE user_id = ${req.session.id} AND item_id = ${itemId}`;
|
||||||
|
return res.reply({
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ success: true })
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return res.reply({ code: 500, body: JSON.stringify({ success: false }) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
127
views/subscriptions.html
Normal file
127
views/subscriptions.html
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
@include(snippets/header)
|
||||||
|
|
||||||
|
<div id="main">
|
||||||
|
<div style="padding: 20px; max-width: 1200px; margin: 0 auto;">
|
||||||
|
<h2 style="margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 10px;">My Subscriptions</h2>
|
||||||
|
|
||||||
|
@if(items.length === 0)
|
||||||
|
<div style="padding: 20px; background: rgba(0,0,0,0.2); border-radius: 4px; text-align: center;">
|
||||||
|
You haven't subscribed to any threads yet.
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="subs-grid">
|
||||||
|
@each(items as item)
|
||||||
|
<div class="sub-card" id="sub-{{ item.id }}">
|
||||||
|
<a href="/{{ item.id }}" class="sub-link">
|
||||||
|
<img src="{{ item.thumb }}" loading="lazy" />
|
||||||
|
<div class="sub-info">
|
||||||
|
<span class="sub-id">#{{ item.id }}</span>
|
||||||
|
<span class="sub-user">by {{ item.user }}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<button class="unsub-btn" data-id="{{ item.id }}">Unsubscribe</button>
|
||||||
|
</div>
|
||||||
|
@endeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.subs-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-card {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-card img {
|
||||||
|
width: 100%;
|
||||||
|
height: 110px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-info {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-id {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-user {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsub-btn {
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
background: rgba(200, 50, 50, 0.2);
|
||||||
|
color: #ff6666;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
transition: background 0.2s;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unsub-btn:hover {
|
||||||
|
background: rgba(200, 50, 50, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.unsub-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
if (!confirm('Unsubscribe from this thread?')) return;
|
||||||
|
|
||||||
|
const id = e.target.dataset.id;
|
||||||
|
const card = document.getElementById('sub-' + id);
|
||||||
|
const originalText = e.target.textContent;
|
||||||
|
e.target.textContent = '...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/subscriptions/' + id + '/delete', { method: 'POST' });
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.success) {
|
||||||
|
card.style.opacity = '0';
|
||||||
|
setTimeout(() => {
|
||||||
|
card.remove();
|
||||||
|
if (document.querySelectorAll('.sub-card').length === 0) {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
alert('Error: ' + (json.message || 'Failed'));
|
||||||
|
e.target.textContent = originalText;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert('Error removing subscription');
|
||||||
|
e.target.textContent = originalText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@include(snippets/footer)
|
||||||
Reference in New Issue
Block a user