add nsfp tag manager

This commit is contained in:
2026-06-19 14:44:56 +02:00
parent 06564af203
commit 8a24564cd9
8 changed files with 543 additions and 14 deletions

View File

@@ -21,6 +21,7 @@
<li><a href="/admin/halls">Hall Manager</a></li>
<li><a href="/admin/motd">MOTD Manager</a></li>
<li><a href="/admin/wordfilter">Wordfilter Manager</a></li>
<li><a href="/admin/nsfp">NSFP Tag Manager</a></li>
@if(enable_cleanup)
<li><a href="/admin/cleanup">Cleanup Manager</a></li>
@endif

254
views/admin/nsfp.html Normal file
View File

@@ -0,0 +1,254 @@
@include(snippets/header)
<div class="pagewrapper">
<div id="main" class="admin-container">
<div class="container">
<div class="admin-header-flex" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<h2 style="margin: 0;">NSFP Tag Manager</h2>
</div>
<p style="color: #aaa; margin-top: 4px; margin-bottom: 20px; font-size: 0.9em;">
Manage which tags are treated as <strong style="color: var(--accent);">Not Safe For Public</strong> &mdash; hidden from logged-out guests.
</p>
<!-- Current NSFP tags -->
<div style="background: rgba(0,0,0,0.15); border: 1px solid rgba(255,255,255,0.06); border-radius: 6px; padding: 20px; margin-bottom: 24px;">
<p style="font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.08em; color: #888; margin: 0 0 8px;">Current NSFP Tags</p>
<div class="nsfp-chips-area" id="nsfp-chips">
<span class="nsfp-chips-empty">Loading&hellip;</span>
</div>
<span id="chip-status" class="nsfp-status-msg"></span>
</div>
<!-- Add tag by search -->
<div style="background: rgba(0,0,0,0.2); padding: 20px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.05);">
<h4 style="color: var(--accent); margin-top: 0; margin-bottom: 14px;">Add Tag to NSFP List</h4>
<div class="nsfp-search-wrap">
<input type="text" id="nsfp-search" class="nsfp-search-input" placeholder="Search tags..." autocomplete="off">
<div class="nsfp-search-dropdown" id="search-dropdown" style="display:none;"></div>
</div>
<span id="add-status" class="nsfp-status-msg"></span>
</div>
</div>
</div>
</div>
<script>
(function () {
var csrf = '{{ csrf_token }}';
var currentIds = new Set();
var chipsEl = document.getElementById('nsfp-chips');
var chipStatus = document.getElementById('chip-status');
var addStatus = document.getElementById('add-status');
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function showStatus(el, msg, ok) {
el.textContent = msg;
el.style.color = ok ? '#28a745' : '#d9534f';
if (ok) setTimeout(function () { el.textContent = ''; }, 3000);
}
function refreshEmptyMsg() {
var existing = chipsEl.querySelector('.nsfp-tag-chip');
var emptyEl = chipsEl.querySelector('.nsfp-chips-empty');
if (!existing) {
if (!emptyEl) {
emptyEl = document.createElement('span');
emptyEl.className = 'nsfp-chips-empty';
emptyEl.textContent = 'No NSFP tags configured \u2014 all content is public.';
chipsEl.appendChild(emptyEl);
}
} else if (emptyEl) {
emptyEl.remove();
}
}
function createChip(tag) {
var chip = document.createElement('span');
chip.className = 'nsfp-tag-chip';
chip.dataset.id = tag.id;
var idSpan = document.createElement('span');
idSpan.className = 'chip-id';
idSpan.textContent = '#' + tag.id;
var nameSpan = document.createElement('span');
nameSpan.textContent = tag.tag;
var removeBtn = document.createElement('button');
removeBtn.className = 'chip-remove';
removeBtn.title = 'Remove from NSFP list';
removeBtn.textContent = '\u00d7';
removeBtn.addEventListener('click', function () { nsfpAdmin.remove(tag.id, removeBtn); });
chip.appendChild(idSpan);
chip.appendChild(nameSpan);
chip.appendChild(removeBtn);
return chip;
}
function addChip(tag) {
var emptyEl = chipsEl.querySelector('.nsfp-chips-empty');
if (emptyEl) emptyEl.remove();
chipsEl.appendChild(createChip(tag));
currentIds.add(tag.id);
rebuildDropdown(document.getElementById('nsfp-search').value);
}
function removeChip(tagId) {
var chip = chipsEl.querySelector('.nsfp-tag-chip[data-id="' + tagId + '"]');
if (chip) chip.remove();
currentIds.delete(tagId);
refreshEmptyMsg();
rebuildDropdown(document.getElementById('nsfp-search').value);
}
function loadNsfp() {
fetch('/api/v2/admin/nsfp')
.then(function (r) { return r.json(); })
.then(function (data) {
chipsEl.innerHTML = '';
currentIds.clear();
if (data.success && data.nsfp && data.nsfp.length > 0) {
data.nsfp.forEach(function (tag) {
chipsEl.appendChild(createChip(tag));
currentIds.add(tag.id);
});
} else {
var emptyEl = document.createElement('span');
emptyEl.className = 'nsfp-chips-empty';
emptyEl.textContent = 'No NSFP tags configured \u2014 all content is public.';
chipsEl.appendChild(emptyEl);
}
})
.catch(function (e) {
chipsEl.innerHTML = '<span class="nsfp-chips-empty" style="color:#d9534f;">Failed to load: ' + escapeHtml(e.message) + '</span>';
});
}
var remove = function (tagId, btn) {
if (!confirm('Remove tag #' + tagId + ' from the NSFP list?')) return;
if (btn) btn.disabled = true;
fetch('/api/v2/admin/nsfp/remove', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': csrf },
body: 'tag_id=' + encodeURIComponent(tagId)
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.success) {
removeChip(tagId);
showStatus(chipStatus, 'Tag #' + tagId + ' removed.', true);
} else {
showStatus(chipStatus, 'Error: ' + (data.msg || 'Remove failed'), false);
if (btn) btn.disabled = false;
}
})
.catch(function (e) {
showStatus(chipStatus, 'Network error: ' + e.message, false);
if (btn) btn.disabled = false;
});
};
var doAdd = function (tagId) {
tagId = parseInt(tagId, 10);
if (currentIds.has(tagId)) {
showStatus(addStatus, 'Tag #' + tagId + ' is already in the list.', false);
return;
}
fetch('/api/v2/admin/nsfp/add', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': csrf },
body: 'tag_id=' + encodeURIComponent(tagId)
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.success) {
addChip(data.added);
showStatus(addStatus, '"' + escapeHtml(data.added.tag) + '" (#' + data.added.id + ') added.', true);
document.getElementById('nsfp-search').value = '';
document.getElementById('search-dropdown').style.display = 'none';
lastResults = [];
} else {
showStatus(addStatus, 'Error: ' + (data.msg || 'Add failed'), false);
}
})
.catch(function (e) { showStatus(addStatus, 'Network error: ' + e.message, false); });
};
var lastResults = [];
function rebuildDropdown(query) {
var dropdown = document.getElementById('search-dropdown');
if (!lastResults.length || !query) { dropdown.style.display = 'none'; return; }
dropdown.innerHTML = '';
lastResults.forEach(function (tag) {
var already = currentIds.has(tag.id);
var item = document.createElement('div');
item.className = 'nsfp-search-result-item' + (already ? ' already-added' : '');
item.dataset.id = tag.id;
var idSpan = document.createElement('span');
idSpan.className = 'result-id';
idSpan.textContent = '#' + tag.id;
var nameSpan = document.createElement('span');
nameSpan.className = 'result-tag';
nameSpan.textContent = tag.tag;
item.appendChild(idSpan);
item.appendChild(nameSpan);
if (tag.normalized && tag.normalized !== tag.tag) {
var normSpan = document.createElement('span');
normSpan.className = 'result-norm';
normSpan.textContent = '(' + tag.normalized + ')';
item.appendChild(normSpan);
}
if (!already) {
item.addEventListener('click', function () { doAdd(tag.id); });
}
dropdown.appendChild(item);
});
dropdown.style.display = 'block';
}
var searchTimer = null;
document.getElementById('nsfp-search').addEventListener('input', function () {
clearTimeout(searchTimer);
var q = this.value.trim();
if (!q) { lastResults = []; document.getElementById('search-dropdown').style.display = 'none'; return; }
var self = this;
searchTimer = setTimeout(function () {
fetch('/api/v2/admin/nsfp/search?q=' + encodeURIComponent(q))
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.success) { lastResults = data.tags; rebuildDropdown(self.value.trim()); }
})
.catch(function () {});
}, 250);
});
document.addEventListener('click', function (e) {
if (!e.target.closest('.nsfp-search-wrap')) {
document.getElementById('search-dropdown').style.display = 'none';
}
});
window.nsfpAdmin = { remove: remove, doAdd: doAdd };
loadNsfp();
})();
</script>
@include(snippets/footer)