feat: Implement comments, notifications, and custom emojis with new API routes, UI components, and database migrations.
This commit is contained in:
@@ -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
96
views/admin/emojis.html
Normal 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)
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user