Compare commits
22 Commits
comments-f
...
f0bm
| Author | SHA1 | Date | |
|---|---|---|---|
| ca86cf7c6e | |||
| 734071beab | |||
| 0e9fedc9b4 | |||
| 8fe362c966 | |||
| 8180cdd885 | |||
| b71711aa96 | |||
| 588ab3c3de | |||
| 46fb7067ed | |||
| 5815334e7b | |||
| 6bfb200626 | |||
| 166ba96f0f | |||
| a2ebea2ba3 | |||
| 91546a1f0d | |||
| 017ac4ca4c | |||
| cb31cfd85b | |||
| a5acc22c4a | |||
| dbcf39c3ba | |||
| 4d2fd7561f | |||
| c4bb21bc31 | |||
| 7d58acd5ed | |||
| 286791de0d | |||
| 1b8a9185bb |
1
add_index_tags_assign.sql
Normal file
1
add_index_tags_assign.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS tags_assign_tag_id_idx ON public.tags_assign (tag_id);
|
||||||
@@ -1596,7 +1596,8 @@ div.posts>a:hover::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navbar.scrolled {
|
.navbar.scrolled {
|
||||||
background: #000 !important;
|
background: black !important;
|
||||||
|
transition: background 0.1s ease;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3848,7 +3849,7 @@ input#s_avatar {
|
|||||||
/* Comments System */
|
/* Comments System */
|
||||||
#comments-container {
|
#comments-container {
|
||||||
padding: 0px 5px 5px 5px;
|
padding: 0px 5px 5px 5px;
|
||||||
background: var(--metadata-bg);
|
background: #00000087;
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
}
|
}
|
||||||
@@ -3941,7 +3942,7 @@ input#s_avatar {
|
|||||||
.comment-input textarea {
|
.comment-input textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
background: var(--bg);
|
background: #000000a1;
|
||||||
border: 1px solid var(--gray);
|
border: 1px solid var(--gray);
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -4028,6 +4029,13 @@ input#s_avatar {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#comment-sort,
|
||||||
|
#subscribe-btn,
|
||||||
|
#lock-thread-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Admin buttons */
|
/* Admin buttons */
|
||||||
.admin-edit-btn,
|
.admin-edit-btn,
|
||||||
.admin-delete-btn {
|
.admin-delete-btn {
|
||||||
@@ -4121,7 +4129,7 @@ input#s_avatar {
|
|||||||
|
|
||||||
/* Two-level comment replies */
|
/* Two-level comment replies */
|
||||||
.comment-replies {
|
.comment-replies {
|
||||||
margin-left: 40px;
|
margin-left: 10px;
|
||||||
border-left: 2px solid var(--gray);
|
border-left: 2px solid var(--gray);
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
const delbutton = document.createElement("a");
|
const delbutton = document.createElement("a");
|
||||||
delbutton.innerHTML = " ×";
|
delbutton.innerHTML = " ×";
|
||||||
delbutton.href = "#";
|
delbutton.href = "javascript:void(0)";
|
||||||
// Class for delegation
|
// Class for delegation
|
||||||
delbutton.classList.add("admin-deltag");
|
delbutton.classList.add("admin-deltag");
|
||||||
|
|
||||||
@@ -70,18 +70,62 @@
|
|||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
const { postid } = ctx;
|
const { postid } = ctx;
|
||||||
|
|
||||||
if (!confirm("Do you really want to delete this tag?"))
|
// Use normalized target logic if possible, or assume e.target (wrapped in listener)
|
||||||
return;
|
// In delegation, we passed 'e'. e.target might be text node if not normalized in the handler call?
|
||||||
const tagname = e.target.parentElement.querySelector('a:first-child').innerText;
|
// In listener I normalized 'target', but passed 'e' to deleteEvent.
|
||||||
|
// So 'e.target' is still the raw event target.
|
||||||
|
// I should check nodeType here or use closest properly.
|
||||||
|
|
||||||
const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), {
|
let target = e.target;
|
||||||
method: 'DELETE'
|
if (target.nodeType === 3) target = target.parentElement;
|
||||||
})).json();
|
|
||||||
|
|
||||||
if (!res.success)
|
const badge = target.closest('.badge');
|
||||||
return alert("uff");
|
const tagLink = badge.querySelector('a:first-child');
|
||||||
|
const tagname = tagLink.innerText;
|
||||||
|
|
||||||
renderTags(res.tags);
|
const modal = document.getElementById('delete-tag-modal');
|
||||||
|
const nameEl = document.getElementById('delete-tag-name');
|
||||||
|
const confirmBtn = document.getElementById('delete-tag-confirm');
|
||||||
|
const cancelBtn = document.getElementById('delete-tag-cancel');
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
nameEl.textContent = tagname;
|
||||||
|
confirmBtn.textContent = 'Delete';
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
confirmBtn.onclick = null;
|
||||||
|
cancelBtn.onclick = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelBtn.onclick = closeModal;
|
||||||
|
|
||||||
|
confirmBtn.onclick = async () => {
|
||||||
|
confirmBtn.textContent = 'Deleting...';
|
||||||
|
confirmBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await (await fetch("/api/v2/admin/" + postid + "/tags/" + encodeURIComponent(tagname), {
|
||||||
|
method: 'DELETE'
|
||||||
|
})).json();
|
||||||
|
|
||||||
|
if (!res.success) {
|
||||||
|
alert(res.msg || "uff");
|
||||||
|
confirmBtn.textContent = 'Delete';
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
} else {
|
||||||
|
renderTags(res.tags);
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert("Error deleting tag");
|
||||||
|
confirmBtn.textContent = 'Delete';
|
||||||
|
confirmBtn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addtagClick = (e) => {
|
const addtagClick = (e) => {
|
||||||
@@ -314,29 +358,24 @@
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Event Delegation
|
||||||
// Event Delegation
|
// Event Delegation
|
||||||
document.addEventListener("click", e => {
|
document.addEventListener("click", e => {
|
||||||
if (e.target.matches("a#a_addtag")) {
|
const target = e.target.nodeType === 3 ? e.target.parentElement : e.target;
|
||||||
|
|
||||||
|
if (target.matches("a#a_addtag")) {
|
||||||
addtagClick(e);
|
addtagClick(e);
|
||||||
} else if (e.target.matches("a#a_toggle")) {
|
} else if (target.matches("a#a_toggle")) {
|
||||||
toggleEvent(e);
|
toggleEvent(e);
|
||||||
} else if (e.target.closest("svg#a_favo")) {
|
} else if (target.closest("svg#a_favo")) {
|
||||||
toggleFavEvent(e);
|
toggleFavEvent(e);
|
||||||
} else if (e.target.closest("svg#a_delete")) {
|
} else if (target.closest("svg#a_delete")) {
|
||||||
deleteButtonEvent(e);
|
deleteButtonEvent(e);
|
||||||
} else if (e.target.matches("#tags > .badge > a:first-child")) {
|
} else if (target.matches("#tags > .badge > a:first-child")) {
|
||||||
editTagEvent(e);
|
editTagEvent(e);
|
||||||
} else if (e.target.innerText === " \u00d7" && e.target.closest(".badge")) { // check text " x" or similar for delete?
|
} else if (target.matches("#tags > .badge > a:last-child") || target.closest('.admin-deltag')) {
|
||||||
// Original was " ×" which is × (\u00d7).
|
// Match by structure OR class (added in renderTags)
|
||||||
// Logic in deleteEvent expects match.
|
deleteEvent(e);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -53,9 +53,8 @@ class CommentSystem {
|
|||||||
// If comments are already rendered, we might need to re-render them to show emojis
|
// If comments are already rendered, we might need to re-render them to show emojis
|
||||||
// but usually loadComments also happens async.
|
// but usually loadComments also happens async.
|
||||||
// To be safe, if we just got emojis, trigger a silent update if container exists
|
// To be safe, if we just got emojis, trigger a silent update if container exists
|
||||||
if (this.container && this.container.querySelector('.comment-content')) {
|
if (this.container && this.lastData) {
|
||||||
// This is a bit heavy, but ensures emojis appear if they loaded AFTER comments
|
this.render(this.lastData, this.lastUserId, this.lastIsSubscribed);
|
||||||
// For now let's just let it be.
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.customEmojis = {};
|
this.customEmojis = {};
|
||||||
@@ -188,6 +187,11 @@ class CommentSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(comments, currentUserId, isSubscribed) {
|
render(comments, currentUserId, isSubscribed) {
|
||||||
|
// Store for re-rendering when emojis load
|
||||||
|
this.lastData = comments;
|
||||||
|
this.lastUserId = currentUserId;
|
||||||
|
this.lastIsSubscribed = isSubscribed;
|
||||||
|
|
||||||
// Build two-level tree: top-level comments + all replies at one level
|
// Build two-level tree: top-level comments + all replies at one level
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
const roots = [];
|
const roots = [];
|
||||||
|
|||||||
@@ -126,6 +126,58 @@ window.requestAnimFrame = (function () {
|
|||||||
registerModal.style.display = 'none';
|
registerModal.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const registerForm = registerModal.querySelector('form');
|
||||||
|
if (registerForm) {
|
||||||
|
registerForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const formData = new FormData(registerForm);
|
||||||
|
const params = new URLSearchParams(formData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: params
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.redirected) {
|
||||||
|
window.location.href = res.url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
let json;
|
||||||
|
try {
|
||||||
|
json = JSON.parse(text);
|
||||||
|
} catch (e) { }
|
||||||
|
|
||||||
|
if (json && json.success === false) {
|
||||||
|
let errDiv = registerForm.querySelector('.flash-error');
|
||||||
|
if (!errDiv) {
|
||||||
|
errDiv = document.createElement('div');
|
||||||
|
errDiv.className = 'flash-error';
|
||||||
|
errDiv.style.color = '#ff4444';
|
||||||
|
errDiv.style.textAlign = 'center';
|
||||||
|
errDiv.style.marginBottom = '10px';
|
||||||
|
errDiv.style.fontWeight = 'bold';
|
||||||
|
registerForm.insertBefore(errDiv, registerForm.firstChild); // Insert before h2 or inputs
|
||||||
|
// Actually firstChild is text or h2. Let's insert after H2 if possible?
|
||||||
|
// The form has H2 as first element roughly.
|
||||||
|
// insertBefore firstChild is fine, it puts it at top.
|
||||||
|
}
|
||||||
|
errDiv.textContent = json.msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Registration error:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize background preference
|
// Initialize background preference
|
||||||
@@ -295,7 +347,7 @@ window.requestAnimFrame = (function () {
|
|||||||
const navbar = document.querySelector('.navbar');
|
const navbar = document.querySelector('.navbar');
|
||||||
if (navbar) {
|
if (navbar) {
|
||||||
window.addEventListener('scroll', () => {
|
window.addEventListener('scroll', () => {
|
||||||
if (window.scrollY > 50) {
|
if (window.scrollY > 10) {
|
||||||
navbar.classList.add('scrolled');
|
navbar.classList.add('scrolled');
|
||||||
} else {
|
} else {
|
||||||
navbar.classList.remove('scrolled');
|
navbar.classList.remove('scrolled');
|
||||||
@@ -466,9 +518,10 @@ window.requestAnimFrame = (function () {
|
|||||||
|
|
||||||
// Intercept clicks
|
// Intercept clicks
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
|
const target = e.target.nodeType === 3 ? e.target.parentElement : e.target;
|
||||||
|
|
||||||
// Check for thumbnail links on index page
|
// Check for thumbnail links on index page
|
||||||
const thumbnail = e.target.closest('.posts > a');
|
const thumbnail = target.closest('.posts > a');
|
||||||
if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Thumbnails inherit context (e.g. from Tag Index)
|
// Thumbnails inherit context (e.g. from Tag Index)
|
||||||
@@ -476,7 +529,7 @@ window.requestAnimFrame = (function () {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const link = e.target.closest('#next, #prev, #random, #nav-random, .id-link, .nav-next, .nav-prev');
|
const link = target.closest('#next, #prev, #random, #nav-random, .id-link, .nav-next, .nav-prev');
|
||||||
if (link && link.href && link.hostname === window.location.hostname && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
if (link && link.href && link.hostname === window.location.hostname && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
||||||
// Special check for random
|
// Special check for random
|
||||||
if (link.id === 'random' || link.id === 'nav-random') {
|
if (link.id === 'random' || link.id === 'nav-random') {
|
||||||
@@ -527,7 +580,7 @@ window.requestAnimFrame = (function () {
|
|||||||
} else {
|
} else {
|
||||||
loadItemAjax(link.href, true);
|
loadItemAjax(link.href, true);
|
||||||
}
|
}
|
||||||
} else if (e.target.closest('#togglebg')) {
|
} else if (target.closest('#togglebg')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
background = !background;
|
background = !background;
|
||||||
localStorage.setItem('background', background.toString());
|
localStorage.setItem('background', background.toString());
|
||||||
@@ -552,9 +605,9 @@ window.requestAnimFrame = (function () {
|
|||||||
canvas.classList.add('fader-out');
|
canvas.classList.add('fader-out');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (e.target.closest('.removetag')) {
|
} else if (target.closest('.removetag')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const removeBtn = e.target.closest('.removetag');
|
const removeBtn = target.closest('.removetag');
|
||||||
const tagLink = removeBtn.previousElementSibling;
|
const tagLink = removeBtn.previousElementSibling;
|
||||||
|
|
||||||
if (tagLink) {
|
if (tagLink) {
|
||||||
@@ -570,8 +623,11 @@ window.requestAnimFrame = (function () {
|
|||||||
|
|
||||||
if (modal) {
|
if (modal) {
|
||||||
nameEl.textContent = tagName;
|
nameEl.textContent = tagName;
|
||||||
|
confirmBtn.textContent = 'Delete';
|
||||||
|
confirmBtn.disabled = false;
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
confirmBtn.onclick = null;
|
confirmBtn.onclick = null;
|
||||||
@@ -988,7 +1044,7 @@ class NotificationSystem {
|
|||||||
init() {
|
init() {
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
this.poll();
|
this.poll();
|
||||||
setInterval(() => this.poll(), 60000); // Poll every minute
|
setInterval(() => this.poll(), 10000); // Poll every 10 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
@@ -1065,6 +1121,30 @@ class NotificationSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderItem(n) {
|
renderItem(n) {
|
||||||
|
if (n.type === 'approve') {
|
||||||
|
const link = `/${n.item_id}`;
|
||||||
|
return `
|
||||||
|
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}">
|
||||||
|
<div>
|
||||||
|
<strong>Your Upload has been approved</strong>
|
||||||
|
</div>
|
||||||
|
<small class="notif-time">${new Date(n.created_at).toLocaleString()}</small>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n.type === 'admin_pending') {
|
||||||
|
const link = '/admin/approve';
|
||||||
|
return `
|
||||||
|
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}">
|
||||||
|
<div>
|
||||||
|
<strong>A new upload needs approval</strong>
|
||||||
|
</div>
|
||||||
|
<small class="notif-time">${new Date(n.created_at).toLocaleString()}</small>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
let typeText = 'Start';
|
let typeText = 'Start';
|
||||||
if (n.type === 'comment_reply') typeText = 'replied to you';
|
if (n.type === 'comment_reply') typeText = 'replied to you';
|
||||||
else if (n.type === 'subscription') typeText = 'commented in a thread you follow';
|
else if (n.type === 'subscription') typeText = 'commented in a thread you follow';
|
||||||
|
|||||||
@@ -49,6 +49,17 @@
|
|||||||
|
|
||||||
span.insertAdjacentElement("beforeend", a);
|
span.insertAdjacentElement("beforeend", a);
|
||||||
|
|
||||||
|
if (document.querySelector('a[href^="/admin"]')) {
|
||||||
|
const space = document.createTextNode('\u00A0'); //
|
||||||
|
span.appendChild(space);
|
||||||
|
|
||||||
|
const del = document.createElement("a");
|
||||||
|
del.className = "removetag";
|
||||||
|
del.href = "javascript:void(0)";
|
||||||
|
del.innerHTML = "×";
|
||||||
|
span.insertAdjacentElement("beforeend", del);
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelector("#tags").insertAdjacentElement("afterbegin", span);
|
document.querySelector("#tags").insertAdjacentElement("afterbegin", span);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -209,11 +220,12 @@
|
|||||||
|
|
||||||
// Event Delegation
|
// Event Delegation
|
||||||
document.addEventListener("click", e => {
|
document.addEventListener("click", e => {
|
||||||
if (e.target.matches("a#a_addtag")) {
|
const target = e.target.nodeType === 3 ? e.target.parentElement : e.target;
|
||||||
|
if (target.matches("a#a_addtag")) {
|
||||||
addtagClick(e);
|
addtagClick(e);
|
||||||
} else if (e.target.matches("a#a_toggle")) {
|
} else if (target.matches("a#a_toggle")) {
|
||||||
toggleEvent(e);
|
toggleEvent(e);
|
||||||
} else if (e.target.closest("svg#a_favo")) {
|
} else if (target.closest("svg#a_favo")) {
|
||||||
toggleFavEvent(e);
|
toggleFavEvent(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default new class {
|
|||||||
tmp = "items.id in (select item_id from tags_assign where tag_id = 2 group by item_id)";
|
tmp = "items.id in (select item_id from tags_assign where tag_id = 2 group by item_id)";
|
||||||
break;
|
break;
|
||||||
case 2: // untagged
|
case 2: // untagged
|
||||||
tmp = "items.id not in (select item_id from tags_assign group by item_id)";
|
tmp = "not exists (select 1 from tags_assign where item_id = items.id)";
|
||||||
break;
|
break;
|
||||||
case 3: // all
|
case 3: // all
|
||||||
tmp = "1 = 1";
|
tmp = "1 = 1";
|
||||||
@@ -94,7 +94,7 @@ export default new class {
|
|||||||
const untagged = +(await db`
|
const untagged = +(await db`
|
||||||
select count(*) as total
|
select count(*) as total
|
||||||
from "items"
|
from "items"
|
||||||
where id not in (select item_id from tags_assign group by item_id) and active = true
|
where not exists (select 1 from tags_assign where item_id = items.id) and active = true
|
||||||
`)[0].total;
|
`)[0].total;
|
||||||
const sfw = +(await db`
|
const sfw = +(await db`
|
||||||
select count(*) as total
|
select count(*) as total
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export default {
|
|||||||
${o.fav ? db`and "user".user ilike ${'%' + user + '%'}` : db``}
|
${o.fav ? db`and "user".user ilike ${'%' + user + '%'}` : db``}
|
||||||
${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
|
${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
|
||||||
${mime ? db`and items.mime ilike ${smime}` : db``}
|
${mime ? db`and items.mime ilike ${smime}` : db``}
|
||||||
${!o.session && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
${!o.session && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
||||||
group by items.id, tags.tag
|
group by items.id, tags.tag
|
||||||
`)?.length || 0;
|
`)?.length || 0;
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export default {
|
|||||||
${o.fav ? db`and "user".user ilike ${'%' + user + '%'}` : db``}
|
${o.fav ? db`and "user".user ilike ${'%' + user + '%'}` : db``}
|
||||||
${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
|
${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
|
||||||
${mime ? db`and items.mime ilike ${smime}` : db``}
|
${mime ? db`and items.mime ilike ${smime}` : db``}
|
||||||
${!o.session && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
${!o.session && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
||||||
group by items.id, tags.tag, ta.tag_id
|
group by items.id, tags.tag, ta.tag_id
|
||||||
order by items.id desc
|
order by items.id desc
|
||||||
offset ${offset}
|
offset ${offset}
|
||||||
@@ -131,7 +131,7 @@ export default {
|
|||||||
${o.fav ? db`and "user"."user" ilike ${user}` : db``}
|
${o.fav ? db`and "user"."user" ilike ${user}` : db``}
|
||||||
${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
|
${!o.fav && user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
|
||||||
${mime ? db`and items.mime ilike ${smime}` : db``}
|
${mime ? db`and items.mime ilike ${smime}` : db``}
|
||||||
${!o.session && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
${!o.session && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
||||||
group by items.id, tags.tag, ta.tag_id
|
group by items.id, tags.tag, ta.tag_id
|
||||||
order by items.id desc
|
order by items.id desc
|
||||||
`;
|
`;
|
||||||
@@ -234,7 +234,7 @@ export default {
|
|||||||
and "user".user ilike ${'%' + user + '%'}
|
and "user".user ilike ${'%' + user + '%'}
|
||||||
and items.active = 'true'
|
and items.active = 'true'
|
||||||
${mime ? db`and items.mime ilike ${smime}` : db``}
|
${mime ? db`and items.mime ilike ${smime}` : db``}
|
||||||
${!o.session && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
${!o.session && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
||||||
group by items.id
|
group by items.id
|
||||||
order by random()
|
order by random()
|
||||||
limit 1
|
limit 1
|
||||||
@@ -253,7 +253,7 @@ export default {
|
|||||||
${tag ? db`and tags.normalized ilike '%' || slugify(${tag}) || '%'` : db``}
|
${tag ? db`and tags.normalized ilike '%' || slugify(${tag}) || '%'` : db``}
|
||||||
${user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
|
${user ? db`and items.username ilike ${'%' + user + '%'}` : db``}
|
||||||
${mime ? db`and items.mime ilike ${smime}` : db``}
|
${mime ? db`and items.mime ilike ${smime}` : db``}
|
||||||
${!o.session && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
${!o.session && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
||||||
group by items.id, tags.tag
|
group by items.id, tags.tag
|
||||||
order by random()
|
order by random()
|
||||||
limit 1
|
limit 1
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export default (router, tpl) => {
|
|||||||
if (req.url.qs?.id) {
|
if (req.url.qs?.id) {
|
||||||
const id = +req.url.qs.id;
|
const id = +req.url.qs.id;
|
||||||
const f0ck = await db`
|
const f0ck = await db`
|
||||||
select dest, mime
|
select dest, mime, username, id
|
||||||
from "items"
|
from "items"
|
||||||
where
|
where
|
||||||
id = ${id} and
|
id = ${id} and
|
||||||
@@ -138,6 +138,19 @@ export default (router, tpl) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify User
|
||||||
|
try {
|
||||||
|
const uploader = await db`select id from "user" where login = ${f0ck[0].username} limit 1`;
|
||||||
|
if (uploader.length > 0) {
|
||||||
|
await db`
|
||||||
|
INSERT INTO notifications (user_id, type, reference_id, item_id)
|
||||||
|
VALUES (${uploader[0].id}, 'approve', 0, ${id})
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[ADMIN APPROVE] Failed to notify user:', err);
|
||||||
|
}
|
||||||
|
|
||||||
await db`update "items" set active = 'true', is_deleted = false where id = ${id}`;
|
await db`update "items" set active = 'true', is_deleted = false where id = ${id}`;
|
||||||
|
|
||||||
// Check if files need moving (if they are in deleted/)
|
// Check if files need moving (if they are in deleted/)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default router => {
|
|||||||
active = 'true'
|
active = 'true'
|
||||||
${isFav ? db`and fu."user" = ${user}` : db`and items.username ilike ${user}`}
|
${isFav ? db`and fu."user" = ${user}` : db`and items.username ilike ${user}`}
|
||||||
${tag ? db`and tags.normalized ilike '%' || slugify(${tag}) || '%'` : db``}
|
${tag ? db`and tags.normalized ilike '%' || slugify(${tag}) || '%'` : db``}
|
||||||
${!hasSession && globalfilter ? db`and items.id not in (select item_id from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
${!hasSession && globalfilter ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(globalfilter)}))` : db``}
|
||||||
order by random()
|
order by random()
|
||||||
limit 1
|
limit 1
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ export default (router, tpl) => {
|
|||||||
try {
|
try {
|
||||||
const notifications = await db`
|
const notifications = await db`
|
||||||
SELECT n.id, n.type, n.item_id, n.reference_id, n.created_at, n.is_read,
|
SELECT n.id, n.type, n.item_id, n.reference_id, n.created_at, n.is_read,
|
||||||
u.user as from_user, u.id as from_user_id
|
COALESCE(u.user, 'System') as from_user,
|
||||||
|
COALESCE(u.id, 0) as from_user_id
|
||||||
FROM notifications n
|
FROM notifications n
|
||||||
JOIN comments c ON n.reference_id = c.id
|
LEFT JOIN comments c ON n.reference_id = c.id
|
||||||
JOIN "user" u ON c.user_id = u.id
|
LEFT JOIN "user" u ON c.user_id = u.id
|
||||||
WHERE n.user_id = ${req.session.id} AND n.is_read = false
|
WHERE n.user_id = ${req.session.id} AND n.is_read = false
|
||||||
ORDER BY n.created_at DESC
|
ORDER BY n.created_at DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export default (router, tpl) => {
|
|||||||
const { username, password, password_confirm, token } = req.post;
|
const { username, password, password_confirm, token } = req.post;
|
||||||
|
|
||||||
const renderError = (msg) => {
|
const renderError = (msg) => {
|
||||||
|
if (req.headers['x-requested-with'] === 'XMLHttpRequest' || (req.headers.accept && req.headers.accept.includes('application/json'))) {
|
||||||
|
return res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ success: false, msg }));
|
||||||
|
}
|
||||||
return res.reply({
|
return res.reply({
|
||||||
body: tpl.render("register", { theme: req.cookies.theme ?? "f0ck", error: msg })
|
body: tpl.render("register", { theme: req.cookies.theme ?? "f0ck", error: msg })
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -241,6 +241,19 @@ export const handleUpload = async (req, res) => {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify Admins
|
||||||
|
try {
|
||||||
|
const admins = await db`select id from "user" where admin = true`;
|
||||||
|
for (const admin of admins) {
|
||||||
|
await db`
|
||||||
|
INSERT INTO notifications (user_id, type, reference_id, item_id)
|
||||||
|
VALUES (${admin.id}, 'admin_pending', 0, ${itemid})
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[UPLOAD HANDLER] Failed to notify admins:', err);
|
||||||
|
}
|
||||||
|
|
||||||
return sendJson(res, {
|
return sendJson(res, {
|
||||||
success: true,
|
success: true,
|
||||||
msg: 'Upload successful! Your upload is pending admin approval.',
|
msg: 'Upload successful! Your upload is pending admin approval.',
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<div class="pagewrapper">
|
<div class="pagewrapper">
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<div class="index-container">
|
<div class="index-container">
|
||||||
@if(tmp.user)<h2>user: <a href="/user/{{ tmp.user.toLowerCase() }}">{{ tmp.user.toLowerCase() }}</a>@if(tmp.mime) ({{ tmp.mime }}s)@else (all)@endif</h2>@endif
|
@if(tmp.user)<h2>user: <a href="/user/{{ tmp.user.toLowerCase() }}">{!! tmp.user.toLowerCase() !!}</a>@if(tmp.mime) ({{ tmp.mime }}s)@else (all)@endif</h2>@endif
|
||||||
@if(tmp.tag)<h2>tag: @if(session)<a href="/search?tag={{ tmp.tag.toLowerCase() }}" target="_blank">{{ tmp.tag.toLowerCase() }}</a>@else{{ tmp.tag.toLowerCase() }}@endif@if(tmp.mime) ({{ tmp.mime }}s)@else (all)@endif</h2>@endif
|
@if(tmp.tag)<h2>tag: @if(session)<a href="/search?tag={!! tmp.tag.toLowerCase() !!}" target="_blank">{!! tmp.tag.toLowerCase() !!}</a>@else{!! tmp.tag.toLowerCase() !!}@endif@if(tmp.mime) ({{ tmp.mime }}s)@else (all)@endif</h2>@endif
|
||||||
<div class="posts">
|
<div class="posts">
|
||||||
@each(items as item)
|
@each(items as item)
|
||||||
<a href="{{ link.main }}{{ item.id }}" data-mime="{{ item.mime }}" data-mode="{{ item.tag_id ? ['','sfw','nsfw'][item.tag_id] : 'null' }}" style="background-image: url('/t/{{ item.id }}.webp')"><p></p></a>
|
<a href="{{ link.main }}{{ item.id }}" data-mime="{{ item.mime }}" data-mode="{{ item.tag_id ? ['','sfw','nsfw'][item.tag_id] : 'null' }}" style="background-image: url('/t/{{ item.id }}.webp')"><p></p></a>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
@if(typeof item.tags !== "undefined")
|
@if(typeof item.tags !== "undefined")
|
||||||
@each(item.tags as tag)
|
@each(item.tags as tag)
|
||||||
<span @if(session)tooltip="{{ tag.user }}" @endif class="badge {{ tag.badge }} mr-2">
|
<span @if(session)tooltip="{{ tag.user }}" @endif class="badge {{ tag.badge }} mr-2">
|
||||||
<a href="/tag/{{ tag.normalized }}">{{ tag.tag }}</a>@if(session.admin) <a class="removetag"
|
<a href="/tag/{{ tag.normalized }}">{!! tag.tag !!}</a>@if(session.admin) <a class="removetag"
|
||||||
href="#">×</a>@endif
|
href="#">×</a>@endif
|
||||||
</span>
|
</span>
|
||||||
@endeach
|
@endeach
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>username</td>
|
<td>username</td>
|
||||||
<td>{{ session.user }}</td>
|
<td>{!! session.user !!}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>@if(session.avatar)<a href="/{{ session.avatar }}"><img id="img_avatar"
|
<td>@if(session.avatar)<a href="/{{ session.avatar }}"><img id="img_avatar"
|
||||||
|
|||||||
Reference in New Issue
Block a user