1
0
forked from w0bm/f0bm
Files
f0bm/public/s/js/admin.js
2026-01-23 13:18:55 +00:00

343 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(async () => {
// Helper to get dynamic context
const getContext = () => {
const idLink = document.querySelector("a.id-link");
if (!idLink) return null;
return {
postid: +idLink.innerText,
poster: document.querySelector("a#a_username")?.innerText,
tags: [...document.querySelectorAll("#tags > .badge")].map(t => t.innerText.slice(0, -2))
};
};
const queryapi = async (url, data, method = 'GET') => {
let req;
if (method == 'POST') {
req = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
}
else {
let s = [];
for (const [key, val] of Object.entries(data))
s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val));
req = await fetch(url + '?' + s.join('&'));
}
return await req.json();
};
const get = async (url, data) => queryapi(url, data, 'GET');
const post = async (url, data) => queryapi(url, data, 'POST');
const renderTags = _tags => {
[...document.querySelectorAll("#tags > .badge")].forEach(tag => tag.parentElement.removeChild(tag));
_tags.reverse().forEach(tag => {
const a = document.createElement("a");
a.href = `/tag/${tag.normalized}`;
a.style = "color: inherit !important";
a.innerHTML = tag.tag;
// Admin specific: edit event
// Note: delegation handles this now if we set it up, OR we can attach here since elements are new.
// But delegation is cleaner if possible. However, editTagEvent relies on 'e.target'.
const span = document.createElement("span");
span.classList.add("badge", "mr-2");
span.setAttribute('tooltip', tag.user);
tag.badge.split(" ").forEach(b => span.classList.add(b));
const delbutton = document.createElement("a");
delbutton.innerHTML = " ×";
delbutton.href = "#";
// Class for delegation
delbutton.classList.add("admin-deltag");
span.insertAdjacentElement("beforeend", a);
span.innerHTML += ' ';
span.insertAdjacentElement("beforeend", delbutton);
document.querySelector("#tags").insertAdjacentElement("afterbegin", span);
});
};
const deleteEvent = async e => {
e.preventDefault();
const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
if (!confirm("Do you really want to delete this tag?"))
return;
const tagname = e.target.parentElement.querySelector('a:first-child').innerText;
const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), {
method: 'DELETE'
})).json();
if (!res.success)
return alert("uff");
renderTags(res.tags);
};
const addtagClick = (e) => {
if (e) e.preventDefault();
const ctx = getContext();
if (!ctx) return;
const { postid, tags } = ctx;
const insert = document.querySelector("a#a_addtag");
if (insert.previousElementSibling && insert.previousElementSibling.querySelector('input')) {
insert.previousElementSibling.querySelector('input').focus();
return;
}
const span = document.createElement("span");
span.classList.add("badge", "badge-light", "mr-2");
const input = document.createElement("input");
input.size = "10";
input.value = "";
input.setAttribute("list", "testlist");
input.setAttribute("autoComplete", "off");
span.insertAdjacentElement("afterbegin", input);
insert.insertAdjacentElement("beforebegin", span);
input.focus();
let tt = null;
let lastInput = '';
const testList = document.querySelector('#testlist');
input.addEventListener("keyup", async e => {
if (e.key === "Enter") {
const tmptag = input.value?.trim();
// We should re-check tags from DOM? Or trust captured tags?
// Captured 'tags' is safe for immediate check.
if (tags.includes(tmptag))
return alert("tag already exists");
const res = await post("/api/v2/admin/" + postid + "/tags", {
tagname: tmptag
});
if (!res.success) {
alert(res.msg);
return false;
}
renderTags(res.tags);
span.parentElement.removeChild(span);
testList.innerText = "";
addtagClick();
}
else if (e.key === "Escape") {
span.parentElement.removeChild(span);
testList.innerText = "";
}
else {
if (tt != null) clearTimeout(tt);
tt = setTimeout(async () => {
tt = null;
const tmptag = input.value?.trim();
if (tmptag == lastInput || tmptag.length <= 1) return false;
testList.innerText = "";
lastInput = tmptag;
const res = await get('/api/v2/admin/tags/suggest', { q: tmptag });
for (const entry of res.suggestions) {
const option = document.createElement('option');
option.value = entry.tag;
if (!/fox/.test(navigator.userAgent))
option.innerText = `tagged ${entry.tagged} times (score: ${entry.score.toFixed(2)})`;
testList.insertAdjacentElement('beforeEnd', option);
};
}, 500);
}
return true;
});
input.addEventListener("focusout", ie => {
if (input.value.length === 0)
input.parentElement.parentElement.removeChild(input.parentElement);
});
};
const toggleEvent = async (e) => {
if (e) e.preventDefault();
const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
const res = await (await fetch('/api/v2/admin/' + encodeURIComponent(postid) + '/tags/toggle', {
method: 'PUT'
})).json();
renderTags(res.tags);
};
const deleteButtonEvent = async e => {
if (e) e.preventDefault();
const ctx = getContext();
if (!ctx) return;
const { postid, poster } = ctx;
if (!confirm(`Reason for deleting f0ckpost ${postid} by ${poster} (Weihnachten™)`))
return;
const res = await post("/api/v2/admin/deletepost", {
postid: postid
});
if (!res.success) {
alert(res.msg);
}
};
const toggleFavEvent = async (e) => {
const ctx = getContext();
if (!ctx) return;
const { postid } = ctx;
const res = await post('/api/v2/admin/togglefav', {
postid: postid
});
if (res.success) {
const fav = document.querySelector("svg#a_favo > use").href;
fav.baseVal = '/s/img/iconset.svg#heart_' + (fav.baseVal.match(/heart_(regular|solid)$/)[1] == "solid" ? "regular" : "solid");
const favcontainer = document.querySelector('span#favs');
favcontainer.innerHTML = "";
favcontainer.hidden = !(favcontainer.hidden || res.favs.length > 0);
res.favs.forEach(f => {
const a = document.createElement('a');
a.href = `/user/${f.user}/favs`;
a.setAttribute('tooltip', f.user);
a.setAttribute('flow', 'up');
const img = document.createElement('img');
img.src = `/t/${f.avatar}.webp`;
img.style.height = "32px";
img.style.width = "32px";
a.insertAdjacentElement('beforeend', img);
favcontainer.insertAdjacentElement('beforeend', a);
favcontainer.innerHTML += "&nbsp;";
});
}
};
let tmptt = null;
const editTagEvent = async e => {
e.preventDefault();
if (e.detail === 2) { // Double click
clearTimeout(tmptt);
const old = e.target;
const parent = e.target.parentElement;
const oldtag = e.target.innerText;
const textfield = document.createElement('input');
textfield.value = e.target.innerText;
textfield.size = 10;
parent.insertAdjacentElement('afterbegin', textfield);
textfield.focus();
parent.removeChild(e.target);
// Hide delete button while editing
const delBtn = parent.querySelector('a:last-child');
if (delBtn) delBtn.style.display = 'none';
textfield.addEventListener("keyup", async e => {
if (e.key === 'Enter') {
parent.removeChild(textfield);
let res = await fetch('/api/v2/admin/tags/' + encodeURIComponent(oldtag), {
method: 'PUT',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ newtag: textfield.value })
});
const status = res.status;
res = await res.json();
switch (status) {
case 200:
case 201:
parent.insertAdjacentElement('afterbegin', old);
if (delBtn) delBtn.style.display = '';
old.href = `/tag/${res.tag}`;
old.innerText = res.tag.trim();
break;
default:
console.log(res);
break;
}
}
else if (e.key === 'Escape') {
parent.removeChild(textfield);
parent.insertAdjacentElement('afterbegin', old);
if (delBtn) delBtn.style.display = '';
}
});
}
else
tmptt = setTimeout(() => location.href = e.target.href, 250);
return false;
};
// Event Delegation
document.addEventListener("click", e => {
if (e.target.matches("a#a_addtag")) {
addtagClick(e);
} else if (e.target.matches("a#a_toggle")) {
toggleEvent(e);
} else if (e.target.closest("svg#a_favo")) {
toggleFavEvent(e);
} else if (e.target.closest("svg#a_delete")) {
deleteButtonEvent(e);
} else if (e.target.matches("#tags > .badge > a:first-child")) {
editTagEvent(e);
} else if (e.target.innerText === " \u00d7" && e.target.closest(".badge")) { // check text " x" or similar for delete?
// Original was " &#215;" which is × (\u00d7).
// Logic in deleteEvent expects match.
// Let's rely on class or structure.
// In renderTags I added class 'admin-deltag'.
// Existing tags in HTML might NOT have this class unless rendered by JS?
// But existing tags are just HTML. We should match structure.
// selector: "#tags > .badge > a:last-child"
if (e.target.matches("#tags > .badge > a:last-child")) {
deleteEvent(e);
}
}
});
document.addEventListener("keyup", e => {
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
const ctx = getContext();
if (!ctx) return;
if (e.key === "p") toggleEvent();
else if (e.key === "i") addtagClick();
else if (e.key === "x") deleteButtonEvent();
else if (e.key === "f") toggleFavEvent();
});
// Settings page
if (document.location.pathname === '/settings') {
const saveAvatar = async e => {
e.preventDefault();
const avatar = +document.querySelector('input[name="i_avatar"]').value;
let res = await fetch('/api/v2/settings/setAvatar', {
method: 'PUT',
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ avatar })
});
const code = res.status;
res = await res.json();
if (code === 200) {
document.querySelector('#img_avatar').src = `/t/${avatar}.webp`;
document.querySelector('img.avatar').src = `/t/${avatar}.webp`;
}
};
const sAvatar = document.querySelector('input#s_avatar');
if (sAvatar) sAvatar.addEventListener('click', saveAvatar);
const iAvatar = document.querySelector('input[name="i_avatar"]');
if (iAvatar) iAvatar.addEventListener('keyup', async e => { if (e.key === 'Enter') await saveAvatar(e); });
}
})();