add nsfp tag manager
This commit is contained in:
@@ -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
254
views/admin/nsfp.html
Normal 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> — 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…</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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user