feat: Implement comments, notifications, and custom emojis with new API routes, UI components, and database migrations.

This commit is contained in:
x
2026-01-25 03:48:24 +01:00
parent 595118c2c8
commit d903ce8b98
18 changed files with 1900 additions and 44 deletions

View File

@@ -17,6 +17,7 @@
<li><a href="/admin/approve">Approval Queue</a></li>
<li><a href="/admin/sessions">Sessions</a></li>
<li><a href="/admin/tokens">Invite Tokens</a></li>
<li><a href="/admin/emojis">Emoji Manager</a></li>
</ul>
</div>
</div>

96
views/admin/emojis.html Normal file
View File

@@ -0,0 +1,96 @@
@include(snippets/header)
<div class="container" style="padding-top: 20px;">
<h2>Custom Emojis</h2>
<div class="upload-form"
style="margin-bottom: 20px; text-align: left; background: var(--dropdown-bg); padding: 15px; border: 1px solid var(--nav-border-color);">
<h4>Add New Emoji</h4>
<input type="text" id="emoji-name" placeholder="Name (e.g. pingu)"
style="background: var(--bg); border: 1px solid var(--black); padding: 5px; color: var(--white);">
<input type="text" id="emoji-url" placeholder="URL (e.g. /s/img/pingu.gif)"
style="background: var(--bg); border: 1px solid var(--black); padding: 5px; color: var(--white); width: 300px;">
<button id="add-emoji" class="btn-upload"
style="width: auto; padding: 5px 15px; border: 1px solid var(--nav-border-color); background: var(--bg); color: var(--white); cursor: pointer;">Add</button>
</div>
<div class="upload-form" style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; color: var(--white);">
<thead>
<tr style="border-bottom: 1px solid var(--nav-border-color); text-align: left;">
<th style="padding: 10px;">Preview</th>
<th style="padding: 10px;">Name</th>
<th style="padding: 10px;">URL</th>
<th style="padding: 10px;">Actions</th>
</tr>
</thead>
<tbody id="emoji-list">
<!-- Populated by JS -->
</tbody>
</table>
</div>
</div>
<script>
const loadEmojis = async () => {
try {
const res = await fetch('/api/v2/emojis');
const data = await res.json();
if (data.success) {
const tbody = document.getElementById('emoji-list');
tbody.innerHTML = data.emojis.map(e =>
'<tr style="border-bottom: 1px solid rgba(255,255,255,0.05);">' +
'<td style="padding: 10px;"><img src="' + e.url + '" style="height: 30px; object-fit: contain;"></td>' +
'<td style="padding: 10px; font-family: monospace; font-size: 1.1em; color: var(--accent);">:' + e.name + ':</td>' +
'<td style="padding: 10px; opacity: 0.7;">' + e.url + '</td>' +
'<td style="padding: 10px;">' +
'<button onclick="deleteEmoji(' + e.id + ')" class="btn-remove" style="padding: 5px 10px; font-size: 0.8em; background: #c00; color: white; border: none; cursor: pointer;">Delete</button>' +
'</td>' +
'</tr>'
).join('');
}
} catch (err) { console.error(err); }
};
const addEmoji = async () => {
const name = document.getElementById('emoji-name').value;
const url = document.getElementById('emoji-url').value;
if (!name || !url) return alert('Fill both fields');
try {
const res = await fetch('/api/v2/admin/emojis', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, // Body parsing middleware uses this or JSON? Typically form-encoded in this stack
body: new URLSearchParams({ name, url })
});
const data = await res.json();
if (data.success) {
document.getElementById('emoji-name').value = '';
document.getElementById('emoji-url').value = '';
loadEmojis();
} else {
alert('Failed: ' + data.message);
}
} catch (e) {
alert('Error: ' + e.message);
}
};
const deleteEmoji = async (id) => {
if (!confirm('Delete this emoji?')) return;
try {
const res = await fetch('/api/v2/admin/emojis/' + id + '/delete', { method: 'POST' });
const data = await res.json();
if (data.success) {
loadEmojis();
} else {
alert('Failed');
}
} catch (e) { alert(e); }
};
document.getElementById('add-emoji').addEventListener('click', addEmoji);
loadEmojis();
</script>
@include(snippets/footer)

View File

@@ -95,10 +95,12 @@
<span class="badge badge-dark">
<a href="/{{ item.id }}" class="id-link">{{ item.id }}</a>
@if(session)
(<a id="a_username" href="/user/{{ user.name.toLowerCase() }}/f0cks@if(tmp.mime)/{{ tmp.mime }}@endif">{{ user.name }}</a>)
(<a id="a_username"
href="/user/{{ user.name.toLowerCase() }}/f0cks@if(tmp.mime)/{{ tmp.mime }}@endif">{{user.name }}</a>)
@endif
</span>
<span class="badge badge-dark"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{ item.timestamp.timeago }}</time></span>
<span class="badge badge-dark"><time class="timeago"
tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></span>
<span class="badge badge-dark" id="tags">
@if(typeof item.tags !== "undefined")
@each(item.tags as tag)
@@ -121,6 +123,6 @@
style="height: 32px; width: 32px" /></a>
@endeach
@endif
</span>
</div>
</div>
</div>
<div id="comments-container" data-item-id="{{ item.id }}" @if(session)data-user="{{ session.user }}" @endif></div>

View File

@@ -22,6 +22,7 @@
<script async src="/s/js/theme.js?v=@mtime(/public/s/js/theme.js)"></script>
<script src="/s/js/v0ck.js?v=@mtime(/public/s/js/v0ck.js)"></script>
<script src="/s/js/f0ck.js?v=@mtime(/public/s/js/f0ck.js)"></script>
<script src="/s/js/comments.js?v=@mtime(/public/s/js/comments.js)"></script>
@if(session && session.admin)
<script src="/s/js/admin.js?v=@mtime(/public/s/js/admin.js)"></script>
@elseif(session && !session.admin)

View File

@@ -7,6 +7,8 @@
<link rel="icon" type="image/gif" href="/s/img/favicon.png" />
<link rel="stylesheet" href="/s/css/f0ck.css?v=@mtime(/public/s/css/f0ck.css)">
<link rel="stylesheet" href="/s/css/w0bm.css?v=@mtime(/public/s/css/w0bm.css)">
@if(typeof item !== 'undefined')
<script src="/s/js/marked.min.js"></script>@endif
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
@if(typeof item !== 'undefined')

View File

@@ -36,6 +36,24 @@
d="M13 5.466V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192zm0 9v-3.932a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192z" />
</svg></a>
@endif
<div id="nav-notifications" class="nav-item-rel">
<a href="#" id="nav-notif-btn" title="Notifications">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" viewBox="0 0 16 16">
<path
d="M8 16a2 2 0 0 0 2-2H6a2 2 0 0 0 2 2zm.995-14.901a1 1 0 1 0-1.99 0A5.002 5.002 0 0 0 3 6c0 1.098-.5 6-2 7h14c-1.5-1-2-5.902-2-7 0-2.42-1.72-4.44-4.005-4.901z" />
</svg>
<span class="notif-count" style="display:none">0</span>
</a>
<div id="notif-dropdown" class="notif-dropdown">
<div class="notif-header">
<span>Notifications</span>
<button id="mark-all-read">Mark all read</button>
</div>
<div class="notif-list">
<div class="notif-empty">No new notifications</div>
</div>
</div>
</div>
<a href="#" id="nav-search-btn" title="Search"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13"
fill="currentColor" viewBox="0 0 16 16">
<path