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